@consilioweb/payload-support 0.6.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/dist/{styles/theme.css → components/TicketConversation.css} +13 -326
  2. package/dist/components/TicketConversation.js +3004 -0
  3. package/dist/index.cjs +11 -11
  4. package/dist/index.js +11 -11
  5. package/dist/views/BillingView/client.cjs +209 -0
  6. package/dist/views/BillingView/index.cjs +34 -0
  7. package/dist/views/ChatView/client.cjs +253 -0
  8. package/dist/views/ChatView/index.cjs +34 -0
  9. package/dist/views/CrmView/client.cjs +233 -0
  10. package/dist/views/CrmView/index.cjs +34 -0
  11. package/dist/views/EmailTrackingView/client.cjs +159 -0
  12. package/dist/views/EmailTrackingView/index.cjs +34 -0
  13. package/dist/views/ImportConversationView/client.cjs +205 -0
  14. package/dist/views/ImportConversationView/index.cjs +34 -0
  15. package/dist/views/LogsView/client.cjs +149 -0
  16. package/dist/views/LogsView/index.cjs +32 -0
  17. package/dist/views/NewTicketView/client.cjs +229 -0
  18. package/dist/views/NewTicketView/index.cjs +32 -0
  19. package/dist/views/PendingEmailsView/client.cjs +173 -0
  20. package/dist/views/PendingEmailsView/index.cjs +34 -0
  21. package/dist/views/SupportDashboardView/client.cjs +301 -0
  22. package/dist/views/SupportDashboardView/index.cjs +34 -0
  23. package/dist/views/TicketDetailView/client.cjs +850 -0
  24. package/dist/views/TicketDetailView/index.cjs +34 -0
  25. package/dist/views/TicketInboxView/client.cjs +299 -0
  26. package/dist/views/TicketInboxView/index.cjs +32 -0
  27. package/dist/views/TicketingSettingsView/client.cjs +733 -0
  28. package/dist/views/TicketingSettingsView/index.cjs +34 -0
  29. package/dist/views/TimeDashboardView/client.cjs +169 -0
  30. package/dist/views/TimeDashboardView/index.cjs +34 -0
  31. package/dist/views/shared/AdminViewHeader.cjs +68 -0
  32. package/dist/views/shared/ErrorBoundary.cjs +55 -0
  33. package/dist/views/shared/Skeleton.cjs +77 -0
  34. package/dist/views/shared/adminTokens.cjs +34 -0
  35. package/dist/views/shared/adminTokens.js +0 -1
  36. package/dist/{components/TicketConversation/config.js → views/shared/config.cjs} +5 -2
  37. package/dist/views/shared/config.js +0 -1
  38. package/dist/views/shared/index.cjs +58 -0
  39. package/dist/views/shared/index.js +0 -1
  40. package/dist/views.cjs +86 -0
  41. package/dist/views.d.cts +13 -0
  42. package/dist/views.d.ts +13 -0
  43. package/dist/views.js +13 -0
  44. package/package.json +14 -7
  45. package/src/plugin.ts +11 -11
  46. package/src/views.ts +17 -0
  47. package/dist/components/TicketConversation/components/AISummaryPanel.js +0 -76
  48. package/dist/components/TicketConversation/components/ActionPanels.js +0 -118
  49. package/dist/components/TicketConversation/components/ActivityLog.js +0 -23
  50. package/dist/components/TicketConversation/components/ClientBar.js +0 -42
  51. package/dist/components/TicketConversation/components/ClientHistory.js +0 -137
  52. package/dist/components/TicketConversation/components/CodeBlock.js +0 -152
  53. package/dist/components/TicketConversation/components/CodeBlockInserter.js +0 -155
  54. package/dist/components/TicketConversation/components/QuickActions.js +0 -65
  55. package/dist/components/TicketConversation/components/TicketHeader.js +0 -92
  56. package/dist/components/TicketConversation/components/TimeTrackingPanel.js +0 -133
  57. package/dist/components/TicketConversation/constants.js +0 -96
  58. package/dist/components/TicketConversation/context.js +0 -11
  59. package/dist/components/TicketConversation/hooks/useAI.js +0 -176
  60. package/dist/components/TicketConversation/hooks/useMessageActions.js +0 -135
  61. package/dist/components/TicketConversation/hooks/useReply.js +0 -187
  62. package/dist/components/TicketConversation/hooks/useTicketActions.js +0 -230
  63. package/dist/components/TicketConversation/hooks/useTimeTracking.js +0 -122
  64. package/dist/components/TicketConversation/hooks/useTranslation.js +0 -70
  65. package/dist/components/TicketConversation/index.js +0 -1123
  66. package/dist/components/TicketConversation/locales/en.json +0 -878
  67. package/dist/components/TicketConversation/locales/fr.json +0 -878
  68. package/dist/components/TicketConversation/types.js +0 -2
  69. package/dist/components/TicketConversation/utils.js +0 -25
  70. package/dist/styles/BillingView.module.scss +0 -311
  71. package/dist/styles/ChatView.module.scss +0 -438
  72. package/dist/styles/CommandPalette.module.scss +0 -160
  73. package/dist/styles/CrmView.module.scss +0 -554
  74. package/dist/styles/EmailTracking.module.scss +0 -238
  75. package/dist/styles/ImportConversation.module.scss +0 -267
  76. package/dist/styles/Layout.module.scss +0 -55
  77. package/dist/styles/Logs.module.scss +0 -164
  78. package/dist/styles/NewTicket.module.scss +0 -143
  79. package/dist/styles/PendingEmails.module.scss +0 -629
  80. package/dist/styles/SupportDashboard.module.scss +0 -649
  81. package/dist/styles/TicketDetail.module.scss +0 -1050
  82. package/dist/styles/TicketInbox.module.scss +0 -296
  83. package/dist/styles/TicketingSettings.module.scss +0 -358
  84. package/dist/styles/TimeDashboard.module.scss +0 -287
  85. package/dist/styles/_tokens.scss +0 -78
@@ -1,96 +0,0 @@
1
- "use client";
2
- const statusLabels = {
3
- open: { label: "Ouvert", bg: "#dbeafe", color: "#1e40af" },
4
- waiting_client: { label: "Attente", bg: "#fef3c7", color: "#92400e" },
5
- resolved: { label: "R\xE9solu", bg: "#dcfce7", color: "#166534" }
6
- };
7
- const projectStatusLabels = {
8
- active: { label: "Actif", bg: "#dcfce7", color: "#166534" },
9
- paused: { label: "En pause", bg: "#fef3c7", color: "#92400e" },
10
- completed: { label: "Termin\xE9", bg: "#e5e7eb", color: "#374151" }
11
- };
12
- const C = {
13
- blue: "#2563eb",
14
- amber: "#d97706",
15
- orange: "#ea580c",
16
- black: "var(--theme-text)",
17
- white: "#fff",
18
- bg: "#fafafa",
19
- textPrimary: "var(--theme-text)",
20
- textSecondary: "#6b7280",
21
- textMuted: "#9ca3af",
22
- border: "#e2e8f0",
23
- statusOpen: "#2563eb",
24
- statusProgress: "#ea580c",
25
- statusWaiting: "#d97706",
26
- statusResolved: "#16a34a",
27
- statusClosed: "#6b7280",
28
- adminBg: "#f8fafc",
29
- adminBorder: "#2563eb",
30
- clientBg: "#fafafa",
31
- clientBorder: "#e2e8f0",
32
- emailBg: "#fffbeb",
33
- emailBorder: "#ea580c",
34
- internalBg: "#fefce8",
35
- internalBorder: "#d97706"
36
- };
37
- const s = {
38
- section: { borderTop: `1px solid ${C.border}`, paddingTop: "18px", marginTop: "20px" },
39
- sectionTitle: { fontSize: "14px", fontWeight: 600, marginBottom: "12px", display: "flex", alignItems: "center", gap: "8px" },
40
- btn: (bg, disabled) => {
41
- const hex = bg.replace("#", "");
42
- const r = parseInt(hex.substring(0, 2), 16) || 0;
43
- const g = parseInt(hex.substring(2, 4), 16) || 0;
44
- const b = parseInt(hex.substring(4, 6), 16) || 0;
45
- const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
46
- const textColor = luminance > 0.6 ? "#374151" : C.white;
47
- return {
48
- padding: "7px 14px",
49
- borderRadius: "6px",
50
- border: `1px solid ${C.border}`,
51
- backgroundColor: bg,
52
- color: textColor,
53
- fontWeight: 600,
54
- fontSize: "13px",
55
- cursor: disabled ? "not-allowed" : "pointer",
56
- opacity: disabled ? 0.5 : 1,
57
- whiteSpace: "nowrap"
58
- };
59
- },
60
- ghostBtn: (color, disabled) => ({
61
- padding: "7px 14px",
62
- borderRadius: "6px",
63
- border: `1px solid ${C.border}`,
64
- backgroundColor: "transparent",
65
- color,
66
- fontWeight: 600,
67
- fontSize: "13px",
68
- cursor: disabled ? "not-allowed" : "pointer",
69
- opacity: disabled ? 0.5 : 1,
70
- whiteSpace: "nowrap"
71
- }),
72
- outlineBtn: (color, disabled) => ({
73
- padding: "7px 14px",
74
- borderRadius: "6px",
75
- border: `1px solid ${color}`,
76
- backgroundColor: "transparent",
77
- color,
78
- fontWeight: 600,
79
- fontSize: "13px",
80
- cursor: disabled ? "not-allowed" : "pointer",
81
- opacity: disabled ? 0.5 : 1,
82
- whiteSpace: "nowrap"
83
- }),
84
- input: { padding: "8px 12px", borderRadius: "6px", border: `1px solid ${C.border}`, fontSize: "14px", color: "#374151", backgroundColor: C.white },
85
- badge: (bg, color) => ({
86
- display: "inline-block",
87
- padding: "2px 8px",
88
- borderRadius: "4px",
89
- fontSize: "11px",
90
- fontWeight: 600,
91
- backgroundColor: bg,
92
- color
93
- })
94
- };
95
-
96
- export { C, projectStatusLabels, s, statusLabels };
@@ -1,11 +0,0 @@
1
- "use client";
2
- import { createContext, useContext } from 'react';
3
-
4
- const TicketContext = createContext(null);
5
- function useTicketContext() {
6
- const ctx = useContext(TicketContext);
7
- if (!ctx) throw new Error("useTicketContext must be used within TicketContext.Provider");
8
- return ctx;
9
- }
10
-
11
- export { TicketContext, useTicketContext };
@@ -1,176 +0,0 @@
1
- "use client";
2
- import { useState, useEffect } from 'react';
3
-
4
- function useAI(messages, client, ticketSubject, replyBody, setReplyBody, setReplyHtml, replyEditorRef) {
5
- const [clientSentiment, setClientSentiment] = useState(null);
6
- const [aiReplying, setAiReplying] = useState(false);
7
- const [aiRewriting, setAiRewriting] = useState(false);
8
- const [showAiSummary, setShowAiSummary] = useState(false);
9
- const [aiSummary, setAiSummary] = useState("");
10
- const [aiGenerating, setAiGenerating] = useState(false);
11
- const [aiSaving, setAiSaving] = useState(false);
12
- const [aiSaved, setAiSaved] = useState(false);
13
- useEffect(() => {
14
- if (messages.length === 0) return;
15
- const lastClientMsg = [...messages].reverse().find((m) => m.authorType === "client" || m.authorType === "email");
16
- if (!lastClientMsg) return;
17
- const analyze = async () => {
18
- try {
19
- const res = await fetch("/api/support/ai", {
20
- method: "POST",
21
- headers: { "Content-Type": "application/json" },
22
- credentials: "include",
23
- body: JSON.stringify({ action: "sentiment", text: lastClientMsg.body.slice(0, 500) })
24
- });
25
- if (res.ok) {
26
- const data = await res.json();
27
- const raw = (data.sentiment || "").toLowerCase().trim();
28
- const sentimentMap = {
29
- "frustr\xE9": { emoji: "\u{1F624}", label: "Frustr\xE9", color: "#dc2626" },
30
- "frustre": { emoji: "\u{1F624}", label: "Frustr\xE9", color: "#dc2626" },
31
- "m\xE9content": { emoji: "\u{1F620}", label: "M\xE9content", color: "#ea580c" },
32
- "mecontent": { emoji: "\u{1F620}", label: "M\xE9content", color: "#ea580c" },
33
- "urgent": { emoji: "\u{1F525}", label: "Urgent", color: "#dc2626" },
34
- "neutre": { emoji: "\u{1F610}", label: "Neutre", color: "#6b7280" },
35
- "satisfait": { emoji: "\u{1F60A}", label: "Satisfait", color: "#16a34a" }
36
- };
37
- const match = Object.keys(sentimentMap).find((k) => raw.includes(k));
38
- if (match) setClientSentiment(sentimentMap[match]);
39
- else setClientSentiment({ emoji: "\u{1F610}", label: "Neutre", color: "#6b7280" });
40
- }
41
- } catch {
42
- }
43
- };
44
- analyze();
45
- }, [messages.length]);
46
- const handleAiSuggestReply = async () => {
47
- if (messages.length === 0) return;
48
- setAiReplying(true);
49
- try {
50
- const res = await fetch("/api/support/ai", {
51
- method: "POST",
52
- headers: { "Content-Type": "application/json" },
53
- credentials: "include",
54
- body: JSON.stringify({
55
- action: "suggest_reply",
56
- messages: messages.slice(-10).map((m) => ({ authorType: m.authorType, body: m.body })),
57
- clientName: `${client?.firstName || ""} ${client?.lastName || ""}`.trim(),
58
- clientCompany: client?.company
59
- })
60
- });
61
- if (res.ok) {
62
- const data = await res.json();
63
- const suggestion = data.reply || "";
64
- if (suggestion) {
65
- setReplyBody(suggestion);
66
- setReplyHtml(suggestion.replace(/\n/g, "<br/>"));
67
- if (replyEditorRef.current?.setContent) {
68
- replyEditorRef.current.setContent(suggestion.replace(/\n/g, "<br/>"));
69
- }
70
- }
71
- }
72
- } catch (err) {
73
- console.error("AI suggest error:", err);
74
- }
75
- setAiReplying(false);
76
- };
77
- const handleAiRewrite = async () => {
78
- if (!replyBody.trim()) return;
79
- setAiRewriting(true);
80
- try {
81
- const res = await fetch("/api/support/ai", {
82
- method: "POST",
83
- headers: { "Content-Type": "application/json" },
84
- credentials: "include",
85
- body: JSON.stringify({ action: "rewrite", text: replyBody })
86
- });
87
- if (res.ok) {
88
- const data = await res.json();
89
- const rewritten = data.rewritten || "";
90
- if (rewritten) {
91
- setReplyBody(rewritten);
92
- setReplyHtml(rewritten.replace(/\n/g, "<br/>"));
93
- if (replyEditorRef.current?.setContent) {
94
- replyEditorRef.current.setContent(rewritten.replace(/\n/g, "<br/>"));
95
- }
96
- }
97
- }
98
- } catch (err) {
99
- console.error("AI rewrite error:", err);
100
- }
101
- setAiRewriting(false);
102
- };
103
- const handleAiGenerate = async () => {
104
- if (messages.length === 0) return;
105
- setAiGenerating(true);
106
- setAiSummary("");
107
- setAiSaved(false);
108
- try {
109
- const res = await fetch("/api/support/ai", {
110
- method: "POST",
111
- headers: { "Content-Type": "application/json" },
112
- credentials: "include",
113
- body: JSON.stringify({
114
- action: "synthesis",
115
- messages: messages.map((m) => ({ authorType: m.authorType, body: m.body, createdAt: m.createdAt })),
116
- ticketSubject,
117
- clientName: `${client?.firstName || ""} ${client?.lastName || ""}`.trim(),
118
- clientCompany: client?.company
119
- })
120
- });
121
- if (res.ok) {
122
- const data = await res.json();
123
- setAiSummary(data.synthesis || "Aucune r\xE9ponse g\xE9n\xE9r\xE9e");
124
- } else {
125
- setAiSummary("Erreur lors de la g\xE9n\xE9ration de la synth\xE8se.");
126
- }
127
- } catch (err) {
128
- setAiSummary(`Erreur de connexion : ${err instanceof Error ? err.message : "erreur inconnue"}`);
129
- } finally {
130
- setAiGenerating(false);
131
- }
132
- };
133
- const handleAiSave = async (id, fetchAll) => {
134
- if (!id || !aiSummary) return;
135
- setAiSaving(true);
136
- try {
137
- const res = await fetch("/api/ticket-messages", {
138
- method: "POST",
139
- headers: { "Content-Type": "application/json" },
140
- credentials: "include",
141
- body: JSON.stringify({
142
- ticket: id,
143
- body: `\u{1F4CB} **Synth\xE8se IA (${(/* @__PURE__ */ new Date()).toLocaleDateString("fr-FR")})**
144
-
145
- ${aiSummary}`,
146
- authorType: "admin",
147
- isInternal: true
148
- })
149
- });
150
- if (res.ok) {
151
- setAiSaved(true);
152
- fetchAll();
153
- }
154
- } catch {
155
- } finally {
156
- setAiSaving(false);
157
- }
158
- };
159
- return {
160
- clientSentiment,
161
- aiReplying,
162
- handleAiSuggestReply,
163
- aiRewriting,
164
- handleAiRewrite,
165
- showAiSummary,
166
- setShowAiSummary,
167
- aiSummary,
168
- aiGenerating,
169
- handleAiGenerate,
170
- aiSaving,
171
- aiSaved,
172
- handleAiSave
173
- };
174
- }
175
-
176
- export { useAI };
@@ -1,135 +0,0 @@
1
- "use client";
2
- import { useState } from 'react';
3
-
4
- function useMessageActions(id, client, fetchAll) {
5
- const [togglingAuthor, setTogglingAuthor] = useState(null);
6
- const [editingMsg, setEditingMsg] = useState(null);
7
- const [editBody, setEditBody] = useState("");
8
- const [editHtml, setEditHtml] = useState("");
9
- const [savingEdit, setSavingEdit] = useState(false);
10
- const [deletingMsg, setDeletingMsg] = useState(null);
11
- const [resendingMsg, setResendingMsg] = useState(null);
12
- const [resendSuccess, setResendSuccess] = useState(null);
13
- const handleEditStart = (msg) => {
14
- setEditingMsg(msg.id);
15
- setEditBody(msg.body);
16
- setEditHtml(msg.bodyHtml || msg.body.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/\n/g, "<br />"));
17
- };
18
- const handleEditSave = async (msgId) => {
19
- if (!editBody.trim()) return;
20
- setSavingEdit(true);
21
- try {
22
- const res = await fetch(`/api/ticket-messages/${msgId}`, {
23
- method: "PATCH",
24
- headers: { "Content-Type": "application/json" },
25
- credentials: "include",
26
- body: JSON.stringify({ body: editBody.trim(), ...editHtml ? { bodyHtml: editHtml } : {} })
27
- });
28
- if (res.ok) {
29
- setEditingMsg(null);
30
- setEditBody("");
31
- fetchAll();
32
- }
33
- } catch {
34
- } finally {
35
- setSavingEdit(false);
36
- }
37
- };
38
- const handleEditCancel = () => {
39
- setEditingMsg(null);
40
- setEditBody("");
41
- setEditHtml("");
42
- };
43
- const handleDelete = async (msgId) => {
44
- if (!window.confirm("Supprimer ce message ?")) return;
45
- setDeletingMsg(msgId);
46
- try {
47
- const res = await fetch(`/api/ticket-messages/${msgId}`, {
48
- method: "DELETE",
49
- credentials: "include"
50
- });
51
- if (res.ok) fetchAll();
52
- } catch {
53
- } finally {
54
- setDeletingMsg(null);
55
- }
56
- };
57
- const handleResend = async (msgId) => {
58
- setResendingMsg(msgId);
59
- setResendSuccess(null);
60
- try {
61
- const res = await fetch("/api/support/resend-notification", {
62
- method: "POST",
63
- headers: { "Content-Type": "application/json" },
64
- credentials: "include",
65
- body: JSON.stringify({ messageId: msgId })
66
- });
67
- if (res.ok) {
68
- setResendSuccess(msgId);
69
- setTimeout(() => setResendSuccess(null), 3e3);
70
- }
71
- } catch {
72
- } finally {
73
- setResendingMsg(null);
74
- }
75
- };
76
- const handleToggleAuthor = async (msgId, currentType) => {
77
- const newType = currentType === "admin" ? "client" : "admin";
78
- setTogglingAuthor(msgId);
79
- try {
80
- const patchData = { authorType: newType };
81
- if (newType === "client" && client) {
82
- patchData.authorClient = client.id;
83
- }
84
- const res = await fetch(`/api/ticket-messages/${msgId}`, {
85
- method: "PATCH",
86
- headers: { "Content-Type": "application/json" },
87
- credentials: "include",
88
- body: JSON.stringify(patchData)
89
- });
90
- if (res.ok) fetchAll();
91
- } catch {
92
- } finally {
93
- setTogglingAuthor(null);
94
- }
95
- };
96
- const handleSplitMessage = async (msgId, ticketSubject) => {
97
- const subject = prompt("Sujet du nouveau ticket :", `Split: ${ticketSubject}`);
98
- if (!subject) return;
99
- try {
100
- const res = await fetch("/api/support/split-ticket", {
101
- method: "POST",
102
- headers: { "Content-Type": "application/json" },
103
- credentials: "include",
104
- body: JSON.stringify({ messageId: msgId, subject })
105
- });
106
- if (res.ok) {
107
- const data = await res.json();
108
- alert(`Ticket ${data.ticketNumber} cr\xE9\xE9`);
109
- fetchAll();
110
- }
111
- } catch {
112
- }
113
- };
114
- return {
115
- togglingAuthor,
116
- editingMsg,
117
- editBody,
118
- editHtml,
119
- setEditHtml,
120
- savingEdit,
121
- handleEditStart,
122
- handleEditSave,
123
- handleEditCancel,
124
- deletingMsg,
125
- handleDelete,
126
- resendingMsg,
127
- resendSuccess,
128
- handleResend,
129
- handleToggleAuthor,
130
- handleSplitMessage,
131
- setEditBody
132
- };
133
- }
134
-
135
- export { useMessageActions };
@@ -1,187 +0,0 @@
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 };