@consilioweb/payload-support 0.9.6 → 0.9.8
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.
|
@@ -138,6 +138,10 @@ const TicketDetailClient = () => {
|
|
|
138
138
|
const [replyHtml, setReplyHtml] = React.useState("");
|
|
139
139
|
const [isInternal, setIsInternal] = React.useState(false);
|
|
140
140
|
const [notifyClient, setNotifyClient] = React.useState(true);
|
|
141
|
+
const [sendAsClient, setSendAsClient] = React.useState(false);
|
|
142
|
+
const [editingMsgId, setEditingMsgId] = React.useState(null);
|
|
143
|
+
const [editingBody, setEditingBody] = React.useState("");
|
|
144
|
+
const [editSaving, setEditSaving] = React.useState(false);
|
|
141
145
|
const [sending, setSending] = React.useState(false);
|
|
142
146
|
const [showMenu, setShowMenu] = React.useState(false);
|
|
143
147
|
const [clientTyping, setClientTyping] = React.useState(false);
|
|
@@ -419,12 +423,21 @@ ${uploadedLinks.join("\n")}` : replyBody.trim() || "[Contenu enrichi]";
|
|
|
419
423
|
method: "POST",
|
|
420
424
|
headers: { "Content-Type": "application/json" },
|
|
421
425
|
credentials: "include",
|
|
422
|
-
body: JSON.stringify({
|
|
426
|
+
body: JSON.stringify({
|
|
427
|
+
ticket: Number(ticketId),
|
|
428
|
+
body: finalBody,
|
|
429
|
+
...finalHtml ? { bodyHtml: finalHtml } : {},
|
|
430
|
+
authorType: sendAsClient ? "client" : "admin",
|
|
431
|
+
...sendAsClient && client ? { authorClient: client.id } : {},
|
|
432
|
+
isInternal: sendAsClient ? false : isInternal,
|
|
433
|
+
skipNotification: sendAsClient || isInternal || !notifyClient
|
|
434
|
+
})
|
|
423
435
|
});
|
|
424
436
|
if (res.ok) {
|
|
425
437
|
setReplyBody("");
|
|
426
438
|
setReplyHtml("");
|
|
427
439
|
setIsInternal(false);
|
|
440
|
+
setSendAsClient(false);
|
|
428
441
|
setPendingFiles([]);
|
|
429
442
|
editorRef.current?.clear();
|
|
430
443
|
fetchAll();
|
|
@@ -467,6 +480,34 @@ ${uploadedLinks.join("\n")}` : replyBody.trim() || "[Contenu enrichi]";
|
|
|
467
480
|
setUndoToast(null);
|
|
468
481
|
}
|
|
469
482
|
};
|
|
483
|
+
const startEditMessage = (msg) => {
|
|
484
|
+
setEditingMsgId(msg.id);
|
|
485
|
+
setEditingBody(msg.body || "");
|
|
486
|
+
};
|
|
487
|
+
const cancelEditMessage = () => {
|
|
488
|
+
setEditingMsgId(null);
|
|
489
|
+
setEditingBody("");
|
|
490
|
+
};
|
|
491
|
+
const saveEditMessage = async () => {
|
|
492
|
+
if (editingMsgId === null) return;
|
|
493
|
+
setEditSaving(true);
|
|
494
|
+
try {
|
|
495
|
+
const res = await fetch(`/api/ticket-messages/${editingMsgId}`, {
|
|
496
|
+
method: "PATCH",
|
|
497
|
+
headers: { "Content-Type": "application/json" },
|
|
498
|
+
credentials: "include",
|
|
499
|
+
body: JSON.stringify({ body: editingBody, bodyHtml: null, skipNotification: true })
|
|
500
|
+
});
|
|
501
|
+
if (res.ok) {
|
|
502
|
+
setEditingMsgId(null);
|
|
503
|
+
setEditingBody("");
|
|
504
|
+
fetchAll();
|
|
505
|
+
}
|
|
506
|
+
} catch {
|
|
507
|
+
} finally {
|
|
508
|
+
setEditSaving(false);
|
|
509
|
+
}
|
|
510
|
+
};
|
|
470
511
|
const handleSplitConfirm = async () => {
|
|
471
512
|
if (!splitModal || !splitSubject.trim()) return;
|
|
472
513
|
try {
|
|
@@ -676,7 +717,21 @@ ${uploadedLinks.join("\n")}` : replyBody.trim() || "[Contenu enrichi]";
|
|
|
676
717
|
return /* @__PURE__ */ jsxRuntime.jsx("span", { style: { color: "#94a3b8" }, children: "\u2713" });
|
|
677
718
|
})() })
|
|
678
719
|
] }),
|
|
679
|
-
msg.
|
|
720
|
+
editingMsgId === msg.id ? /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 6 }, children: [
|
|
721
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
722
|
+
"textarea",
|
|
723
|
+
{
|
|
724
|
+
value: editingBody,
|
|
725
|
+
onChange: (e) => setEditingBody(e.target.value),
|
|
726
|
+
style: { width: "100%", minHeight: 120, padding: 10, fontSize: 13, lineHeight: 1.5, fontFamily: "inherit", border: "1px solid var(--theme-elevation-200)", borderRadius: 6, background: "var(--theme-elevation-0)", color: "var(--theme-text)" },
|
|
727
|
+
autoFocus: true
|
|
728
|
+
}
|
|
729
|
+
),
|
|
730
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: 6, justifyContent: "flex-end" }, children: [
|
|
731
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { onClick: cancelEditMessage, disabled: editSaving, style: { padding: "5px 12px", fontSize: 12, borderRadius: 5, border: "1px solid var(--theme-elevation-200)", background: "var(--theme-elevation-0)", color: "var(--theme-text)", cursor: "pointer" }, children: "Annuler" }),
|
|
732
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { onClick: saveEditMessage, disabled: editSaving || !editingBody.trim(), style: { padding: "5px 12px", fontSize: 12, fontWeight: 600, borderRadius: 5, border: "none", background: "#2563eb", color: "#fff", cursor: editSaving ? "wait" : "pointer", opacity: editSaving ? 0.6 : 1 }, children: editSaving ? "Sauvegarde\u2026" : "Enregistrer" })
|
|
733
|
+
] })
|
|
734
|
+
] }) : msg.deletedAt ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: s__default.default.messageBody, style: { color: "#94a3b8", fontStyle: "italic" }, children: t("detail.messageDeleted") }) : msg.bodyHtml && CodeBlock.hasCodeBlocks(msg.bodyHtml.replace(/<[^>]+>/g, "")) ? /* @__PURE__ */ jsxRuntime.jsx(CodeBlock.CodeBlockRendererHtml, { html: msg.bodyHtml }) : msg.bodyHtml ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: `${s__default.default.messageBody} ${s__default.default.rteDisplay}`, dangerouslySetInnerHTML: { __html: msg.bodyHtml } }) : CodeBlock.hasCodeBlocks(msg.body) ? /* @__PURE__ */ jsxRuntime.jsx(CodeBlock.MessageWithCodeBlocks, { text: msg.body, style: { fontSize: "13px", lineHeight: 1.5 } }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: s__default.default.messageBody, children: msg.body }),
|
|
680
735
|
Array.isArray(msg.attachments) && msg.attachments.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: s__default.default.attachments, children: msg.attachments.map((att, i) => {
|
|
681
736
|
const file = typeof att.file === "object" ? att.file : null;
|
|
682
737
|
if (!file) return null;
|
|
@@ -686,7 +741,8 @@ ${uploadedLinks.join("\n")}` : replyBody.trim() || "[Contenu enrichi]";
|
|
|
686
741
|
] }, i);
|
|
687
742
|
}) })
|
|
688
743
|
] }),
|
|
689
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.messageActions, children: [
|
|
744
|
+
editingMsgId !== msg.id && !msg.deletedAt && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.messageActions, children: [
|
|
745
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { className: s__default.default.actionIcon, title: "\xC9diter", "aria-label": "\xC9diter le message", onClick: () => startEditMessage(msg), style: { fontSize: 11, width: "auto", padding: "4px 8px" }, children: "\xC9diter" }),
|
|
690
746
|
/* @__PURE__ */ jsxRuntime.jsx("button", { className: `${s__default.default.actionIcon} ${s__default.default.danger}`, title: t("actions.deleteMessage"), "aria-label": t("actions.deleteMessage"), onClick: () => handleDeleteMessage(msg.id), style: { fontSize: 11, width: "auto", padding: "4px 8px" }, children: t("actions.deleteMessage") }),
|
|
691
747
|
features.splitTicket && !msg.isInternal && /* @__PURE__ */ jsxRuntime.jsx("button", { className: s__default.default.actionIcon, title: t("actions.extractMessage"), "aria-label": t("actions.extractToNewTicket"), onClick: () => {
|
|
692
748
|
setSplitModal({ messageId: msg.id, preview: msg.body.slice(0, 200) });
|
|
@@ -789,18 +845,39 @@ ${uploadedLinks.join("\n")}` : replyBody.trim() || "[Contenu enrichi]";
|
|
|
789
845
|
] }, i)) }),
|
|
790
846
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.composerFooter, children: [
|
|
791
847
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.composerOptions, children: [
|
|
792
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
848
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
849
|
+
"select",
|
|
850
|
+
{
|
|
851
|
+
value: sendAsClient ? "client" : "admin",
|
|
852
|
+
onChange: (e) => {
|
|
853
|
+
const asClient = e.target.value === "client";
|
|
854
|
+
setSendAsClient(asClient);
|
|
855
|
+
if (asClient) {
|
|
856
|
+
setIsInternal(false);
|
|
857
|
+
setNotifyClient(false);
|
|
858
|
+
}
|
|
859
|
+
},
|
|
860
|
+
style: { fontSize: "12px", padding: "4px 8px", fontWeight: 600, borderRadius: 6, border: "1px solid var(--theme-elevation-200)", background: "var(--theme-elevation-0)", color: "var(--theme-text)" },
|
|
861
|
+
children: [
|
|
862
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "admin", children: "En tant que : Support" }),
|
|
863
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "client", children: "En tant que : Client" })
|
|
864
|
+
]
|
|
865
|
+
}
|
|
866
|
+
),
|
|
867
|
+
!sendAsClient && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
868
|
+
/* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
|
|
869
|
+
/* @__PURE__ */ jsxRuntime.jsx("input", { type: "checkbox", checked: isInternal, onChange: (e) => setIsInternal(e.target.checked) }),
|
|
870
|
+
" ",
|
|
871
|
+
t("detail.internalNote")
|
|
872
|
+
] }),
|
|
873
|
+
/* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
|
|
874
|
+
/* @__PURE__ */ jsxRuntime.jsx("input", { type: "checkbox", checked: notifyClient, onChange: (e) => setNotifyClient(e.target.checked), disabled: isInternal }),
|
|
875
|
+
" ",
|
|
876
|
+
t("detail.notify")
|
|
877
|
+
] })
|
|
801
878
|
] })
|
|
802
879
|
] }),
|
|
803
|
-
/* @__PURE__ */ jsxRuntime.jsx("button", { className: `${s__default.default.sendBtn} ${isInternal ? s__default.default.sendBtnInternal : ""}`, onClick: handleSend, disabled: sending || !replyBody.trim(), "data-action": "send-reply", children: sending ? t("detail.sending") : isInternal ? t("detail.sendNote") : t("detail.sendReply") })
|
|
880
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { className: `${s__default.default.sendBtn} ${isInternal ? s__default.default.sendBtnInternal : ""}`, onClick: handleSend, disabled: sending || !replyBody.trim(), "data-action": "send-reply", children: sending ? t("detail.sending") : sendAsClient ? "Ajouter message client" : isInternal ? t("detail.sendNote") : t("detail.sendReply") })
|
|
804
881
|
] })
|
|
805
882
|
]
|
|
806
883
|
}
|
|
@@ -131,6 +131,10 @@ const TicketDetailClient = () => {
|
|
|
131
131
|
const [replyHtml, setReplyHtml] = useState("");
|
|
132
132
|
const [isInternal, setIsInternal] = useState(false);
|
|
133
133
|
const [notifyClient, setNotifyClient] = useState(true);
|
|
134
|
+
const [sendAsClient, setSendAsClient] = useState(false);
|
|
135
|
+
const [editingMsgId, setEditingMsgId] = useState(null);
|
|
136
|
+
const [editingBody, setEditingBody] = useState("");
|
|
137
|
+
const [editSaving, setEditSaving] = useState(false);
|
|
134
138
|
const [sending, setSending] = useState(false);
|
|
135
139
|
const [showMenu, setShowMenu] = useState(false);
|
|
136
140
|
const [clientTyping, setClientTyping] = useState(false);
|
|
@@ -412,12 +416,21 @@ ${uploadedLinks.join("\n")}` : replyBody.trim() || "[Contenu enrichi]";
|
|
|
412
416
|
method: "POST",
|
|
413
417
|
headers: { "Content-Type": "application/json" },
|
|
414
418
|
credentials: "include",
|
|
415
|
-
body: JSON.stringify({
|
|
419
|
+
body: JSON.stringify({
|
|
420
|
+
ticket: Number(ticketId),
|
|
421
|
+
body: finalBody,
|
|
422
|
+
...finalHtml ? { bodyHtml: finalHtml } : {},
|
|
423
|
+
authorType: sendAsClient ? "client" : "admin",
|
|
424
|
+
...sendAsClient && client ? { authorClient: client.id } : {},
|
|
425
|
+
isInternal: sendAsClient ? false : isInternal,
|
|
426
|
+
skipNotification: sendAsClient || isInternal || !notifyClient
|
|
427
|
+
})
|
|
416
428
|
});
|
|
417
429
|
if (res.ok) {
|
|
418
430
|
setReplyBody("");
|
|
419
431
|
setReplyHtml("");
|
|
420
432
|
setIsInternal(false);
|
|
433
|
+
setSendAsClient(false);
|
|
421
434
|
setPendingFiles([]);
|
|
422
435
|
editorRef.current?.clear();
|
|
423
436
|
fetchAll();
|
|
@@ -460,6 +473,34 @@ ${uploadedLinks.join("\n")}` : replyBody.trim() || "[Contenu enrichi]";
|
|
|
460
473
|
setUndoToast(null);
|
|
461
474
|
}
|
|
462
475
|
};
|
|
476
|
+
const startEditMessage = (msg) => {
|
|
477
|
+
setEditingMsgId(msg.id);
|
|
478
|
+
setEditingBody(msg.body || "");
|
|
479
|
+
};
|
|
480
|
+
const cancelEditMessage = () => {
|
|
481
|
+
setEditingMsgId(null);
|
|
482
|
+
setEditingBody("");
|
|
483
|
+
};
|
|
484
|
+
const saveEditMessage = async () => {
|
|
485
|
+
if (editingMsgId === null) return;
|
|
486
|
+
setEditSaving(true);
|
|
487
|
+
try {
|
|
488
|
+
const res = await fetch(`/api/ticket-messages/${editingMsgId}`, {
|
|
489
|
+
method: "PATCH",
|
|
490
|
+
headers: { "Content-Type": "application/json" },
|
|
491
|
+
credentials: "include",
|
|
492
|
+
body: JSON.stringify({ body: editingBody, bodyHtml: null, skipNotification: true })
|
|
493
|
+
});
|
|
494
|
+
if (res.ok) {
|
|
495
|
+
setEditingMsgId(null);
|
|
496
|
+
setEditingBody("");
|
|
497
|
+
fetchAll();
|
|
498
|
+
}
|
|
499
|
+
} catch {
|
|
500
|
+
} finally {
|
|
501
|
+
setEditSaving(false);
|
|
502
|
+
}
|
|
503
|
+
};
|
|
463
504
|
const handleSplitConfirm = async () => {
|
|
464
505
|
if (!splitModal || !splitSubject.trim()) return;
|
|
465
506
|
try {
|
|
@@ -669,7 +710,21 @@ ${uploadedLinks.join("\n")}` : replyBody.trim() || "[Contenu enrichi]";
|
|
|
669
710
|
return /* @__PURE__ */ jsx("span", { style: { color: "#94a3b8" }, children: "\u2713" });
|
|
670
711
|
})() })
|
|
671
712
|
] }),
|
|
672
|
-
msg.
|
|
713
|
+
editingMsgId === msg.id ? /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 6 }, children: [
|
|
714
|
+
/* @__PURE__ */ jsx(
|
|
715
|
+
"textarea",
|
|
716
|
+
{
|
|
717
|
+
value: editingBody,
|
|
718
|
+
onChange: (e) => setEditingBody(e.target.value),
|
|
719
|
+
style: { width: "100%", minHeight: 120, padding: 10, fontSize: 13, lineHeight: 1.5, fontFamily: "inherit", border: "1px solid var(--theme-elevation-200)", borderRadius: 6, background: "var(--theme-elevation-0)", color: "var(--theme-text)" },
|
|
720
|
+
autoFocus: true
|
|
721
|
+
}
|
|
722
|
+
),
|
|
723
|
+
/* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 6, justifyContent: "flex-end" }, children: [
|
|
724
|
+
/* @__PURE__ */ jsx("button", { onClick: cancelEditMessage, disabled: editSaving, style: { padding: "5px 12px", fontSize: 12, borderRadius: 5, border: "1px solid var(--theme-elevation-200)", background: "var(--theme-elevation-0)", color: "var(--theme-text)", cursor: "pointer" }, children: "Annuler" }),
|
|
725
|
+
/* @__PURE__ */ jsx("button", { onClick: saveEditMessage, disabled: editSaving || !editingBody.trim(), style: { padding: "5px 12px", fontSize: 12, fontWeight: 600, borderRadius: 5, border: "none", background: "#2563eb", color: "#fff", cursor: editSaving ? "wait" : "pointer", opacity: editSaving ? 0.6 : 1 }, children: editSaving ? "Sauvegarde\u2026" : "Enregistrer" })
|
|
726
|
+
] })
|
|
727
|
+
] }) : msg.deletedAt ? /* @__PURE__ */ jsx("div", { className: s.messageBody, style: { color: "#94a3b8", fontStyle: "italic" }, children: t("detail.messageDeleted") }) : msg.bodyHtml && hasCodeBlocks(msg.bodyHtml.replace(/<[^>]+>/g, "")) ? /* @__PURE__ */ jsx(CodeBlockRendererHtml, { html: msg.bodyHtml }) : msg.bodyHtml ? /* @__PURE__ */ jsx("div", { className: `${s.messageBody} ${s.rteDisplay}`, dangerouslySetInnerHTML: { __html: msg.bodyHtml } }) : hasCodeBlocks(msg.body) ? /* @__PURE__ */ jsx(MessageWithCodeBlocks, { text: msg.body, style: { fontSize: "13px", lineHeight: 1.5 } }) : /* @__PURE__ */ jsx("div", { className: s.messageBody, children: msg.body }),
|
|
673
728
|
Array.isArray(msg.attachments) && msg.attachments.length > 0 && /* @__PURE__ */ jsx("div", { className: s.attachments, children: msg.attachments.map((att, i) => {
|
|
674
729
|
const file = typeof att.file === "object" ? att.file : null;
|
|
675
730
|
if (!file) return null;
|
|
@@ -679,7 +734,8 @@ ${uploadedLinks.join("\n")}` : replyBody.trim() || "[Contenu enrichi]";
|
|
|
679
734
|
] }, i);
|
|
680
735
|
}) })
|
|
681
736
|
] }),
|
|
682
|
-
/* @__PURE__ */ jsxs("div", { className: s.messageActions, children: [
|
|
737
|
+
editingMsgId !== msg.id && !msg.deletedAt && /* @__PURE__ */ jsxs("div", { className: s.messageActions, children: [
|
|
738
|
+
/* @__PURE__ */ jsx("button", { className: s.actionIcon, title: "\xC9diter", "aria-label": "\xC9diter le message", onClick: () => startEditMessage(msg), style: { fontSize: 11, width: "auto", padding: "4px 8px" }, children: "\xC9diter" }),
|
|
683
739
|
/* @__PURE__ */ jsx("button", { className: `${s.actionIcon} ${s.danger}`, title: t("actions.deleteMessage"), "aria-label": t("actions.deleteMessage"), onClick: () => handleDeleteMessage(msg.id), style: { fontSize: 11, width: "auto", padding: "4px 8px" }, children: t("actions.deleteMessage") }),
|
|
684
740
|
features.splitTicket && !msg.isInternal && /* @__PURE__ */ jsx("button", { className: s.actionIcon, title: t("actions.extractMessage"), "aria-label": t("actions.extractToNewTicket"), onClick: () => {
|
|
685
741
|
setSplitModal({ messageId: msg.id, preview: msg.body.slice(0, 200) });
|
|
@@ -782,18 +838,39 @@ ${uploadedLinks.join("\n")}` : replyBody.trim() || "[Contenu enrichi]";
|
|
|
782
838
|
] }, i)) }),
|
|
783
839
|
/* @__PURE__ */ jsxs("div", { className: s.composerFooter, children: [
|
|
784
840
|
/* @__PURE__ */ jsxs("div", { className: s.composerOptions, children: [
|
|
785
|
-
/* @__PURE__ */ jsxs(
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
841
|
+
/* @__PURE__ */ jsxs(
|
|
842
|
+
"select",
|
|
843
|
+
{
|
|
844
|
+
value: sendAsClient ? "client" : "admin",
|
|
845
|
+
onChange: (e) => {
|
|
846
|
+
const asClient = e.target.value === "client";
|
|
847
|
+
setSendAsClient(asClient);
|
|
848
|
+
if (asClient) {
|
|
849
|
+
setIsInternal(false);
|
|
850
|
+
setNotifyClient(false);
|
|
851
|
+
}
|
|
852
|
+
},
|
|
853
|
+
style: { fontSize: "12px", padding: "4px 8px", fontWeight: 600, borderRadius: 6, border: "1px solid var(--theme-elevation-200)", background: "var(--theme-elevation-0)", color: "var(--theme-text)" },
|
|
854
|
+
children: [
|
|
855
|
+
/* @__PURE__ */ jsx("option", { value: "admin", children: "En tant que : Support" }),
|
|
856
|
+
/* @__PURE__ */ jsx("option", { value: "client", children: "En tant que : Client" })
|
|
857
|
+
]
|
|
858
|
+
}
|
|
859
|
+
),
|
|
860
|
+
!sendAsClient && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
861
|
+
/* @__PURE__ */ jsxs("label", { children: [
|
|
862
|
+
/* @__PURE__ */ jsx("input", { type: "checkbox", checked: isInternal, onChange: (e) => setIsInternal(e.target.checked) }),
|
|
863
|
+
" ",
|
|
864
|
+
t("detail.internalNote")
|
|
865
|
+
] }),
|
|
866
|
+
/* @__PURE__ */ jsxs("label", { children: [
|
|
867
|
+
/* @__PURE__ */ jsx("input", { type: "checkbox", checked: notifyClient, onChange: (e) => setNotifyClient(e.target.checked), disabled: isInternal }),
|
|
868
|
+
" ",
|
|
869
|
+
t("detail.notify")
|
|
870
|
+
] })
|
|
794
871
|
] })
|
|
795
872
|
] }),
|
|
796
|
-
/* @__PURE__ */ jsx("button", { className: `${s.sendBtn} ${isInternal ? s.sendBtnInternal : ""}`, onClick: handleSend, disabled: sending || !replyBody.trim(), "data-action": "send-reply", children: sending ? t("detail.sending") : isInternal ? t("detail.sendNote") : t("detail.sendReply") })
|
|
873
|
+
/* @__PURE__ */ jsx("button", { className: `${s.sendBtn} ${isInternal ? s.sendBtnInternal : ""}`, onClick: handleSend, disabled: sending || !replyBody.trim(), "data-action": "send-reply", children: sending ? t("detail.sending") : sendAsClient ? "Ajouter message client" : isInternal ? t("detail.sendNote") : t("detail.sendReply") })
|
|
797
874
|
] })
|
|
798
875
|
]
|
|
799
876
|
}
|
package/package.json
CHANGED
|
@@ -132,6 +132,11 @@ export const TicketDetailClient: React.FC = () => {
|
|
|
132
132
|
const [replyHtml, setReplyHtml] = useState('')
|
|
133
133
|
const [isInternal, setIsInternal] = useState(false)
|
|
134
134
|
const [notifyClient, setNotifyClient] = useState(true)
|
|
135
|
+
const [sendAsClient, setSendAsClient] = useState(false)
|
|
136
|
+
// Inline message edit
|
|
137
|
+
const [editingMsgId, setEditingMsgId] = useState<string | number | null>(null)
|
|
138
|
+
const [editingBody, setEditingBody] = useState('')
|
|
139
|
+
const [editSaving, setEditSaving] = useState(false)
|
|
135
140
|
const [sending, setSending] = useState(false)
|
|
136
141
|
|
|
137
142
|
const [showMenu, setShowMenu] = useState(false)
|
|
@@ -396,8 +401,16 @@ const [clientTyping, setClientTyping] = useState(false)
|
|
|
396
401
|
const finalHtml = uploadedLinks.length > 0 ? `${replyHtml || replyBody.trim()}<br/><br/>${uploadedLinks.map((l) => l.replace(/\[(.+?)\]\((.+?)\)/g, '<a href="$2">$1</a>')).join('<br/>')}` : (replyHtml || undefined)
|
|
397
402
|
|
|
398
403
|
const res = await fetch('/api/ticket-messages', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include',
|
|
399
|
-
body: JSON.stringify({
|
|
400
|
-
|
|
404
|
+
body: JSON.stringify({
|
|
405
|
+
ticket: Number(ticketId),
|
|
406
|
+
body: finalBody,
|
|
407
|
+
...(finalHtml ? { bodyHtml: finalHtml } : {}),
|
|
408
|
+
authorType: sendAsClient ? 'client' : 'admin',
|
|
409
|
+
...(sendAsClient && client ? { authorClient: client.id } : {}),
|
|
410
|
+
isInternal: sendAsClient ? false : isInternal,
|
|
411
|
+
skipNotification: sendAsClient || isInternal || !notifyClient,
|
|
412
|
+
}) })
|
|
413
|
+
if (res.ok) { setReplyBody(''); setReplyHtml(''); setIsInternal(false); setSendAsClient(false); setPendingFiles([]); editorRef.current?.clear(); fetchAll() }
|
|
401
414
|
} catch {} finally { setSending(false) }
|
|
402
415
|
}
|
|
403
416
|
|
|
@@ -432,6 +445,37 @@ const [clientTyping, setClientTyping] = useState(false)
|
|
|
432
445
|
}
|
|
433
446
|
}
|
|
434
447
|
|
|
448
|
+
// Inline edit message (body only, clears bodyHtml so the edit shows as plain text)
|
|
449
|
+
const startEditMessage = (msg: Message) => {
|
|
450
|
+
setEditingMsgId(msg.id)
|
|
451
|
+
setEditingBody(msg.body || '')
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const cancelEditMessage = () => {
|
|
455
|
+
setEditingMsgId(null)
|
|
456
|
+
setEditingBody('')
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const saveEditMessage = async () => {
|
|
460
|
+
if (editingMsgId === null) return
|
|
461
|
+
setEditSaving(true)
|
|
462
|
+
try {
|
|
463
|
+
const res = await fetch(`/api/ticket-messages/${editingMsgId}`, {
|
|
464
|
+
method: 'PATCH',
|
|
465
|
+
headers: { 'Content-Type': 'application/json' },
|
|
466
|
+
credentials: 'include',
|
|
467
|
+
body: JSON.stringify({ body: editingBody, bodyHtml: null, skipNotification: true }),
|
|
468
|
+
})
|
|
469
|
+
if (res.ok) {
|
|
470
|
+
setEditingMsgId(null)
|
|
471
|
+
setEditingBody('')
|
|
472
|
+
fetchAll()
|
|
473
|
+
}
|
|
474
|
+
} catch { /* silent */ } finally {
|
|
475
|
+
setEditSaving(false)
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
435
479
|
// #2 — Split ticket with modal
|
|
436
480
|
const handleSplitConfirm = async () => {
|
|
437
481
|
if (!splitModal || !splitSubject.trim()) return
|
|
@@ -608,7 +652,20 @@ const [clientTyping, setClientTyping] = useState(false)
|
|
|
608
652
|
})()}
|
|
609
653
|
</span>
|
|
610
654
|
</div>
|
|
611
|
-
{
|
|
655
|
+
{editingMsgId === msg.id ? (
|
|
656
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
|
657
|
+
<textarea
|
|
658
|
+
value={editingBody}
|
|
659
|
+
onChange={(e) => setEditingBody(e.target.value)}
|
|
660
|
+
style={{ width: '100%', minHeight: 120, padding: 10, fontSize: 13, lineHeight: 1.5, fontFamily: 'inherit', border: '1px solid var(--theme-elevation-200)', borderRadius: 6, background: 'var(--theme-elevation-0)', color: 'var(--theme-text)' }}
|
|
661
|
+
autoFocus
|
|
662
|
+
/>
|
|
663
|
+
<div style={{ display: 'flex', gap: 6, justifyContent: 'flex-end' }}>
|
|
664
|
+
<button onClick={cancelEditMessage} disabled={editSaving} style={{ padding: '5px 12px', fontSize: 12, borderRadius: 5, border: '1px solid var(--theme-elevation-200)', background: 'var(--theme-elevation-0)', color: 'var(--theme-text)', cursor: 'pointer' }}>Annuler</button>
|
|
665
|
+
<button onClick={saveEditMessage} disabled={editSaving || !editingBody.trim()} style={{ padding: '5px 12px', fontSize: 12, fontWeight: 600, borderRadius: 5, border: 'none', background: '#2563eb', color: '#fff', cursor: editSaving ? 'wait' : 'pointer', opacity: editSaving ? 0.6 : 1 }}>{editSaving ? 'Sauvegarde…' : 'Enregistrer'}</button>
|
|
666
|
+
</div>
|
|
667
|
+
</div>
|
|
668
|
+
) : (msg as unknown as { deletedAt?: string }).deletedAt ? (
|
|
612
669
|
<div className={s.messageBody} style={{ color: '#94a3b8', fontStyle: 'italic' }}>{t('detail.messageDeleted')}</div>
|
|
613
670
|
) : msg.bodyHtml && hasCodeBlocks(msg.bodyHtml.replace(/<[^>]+>/g, '')) ? (
|
|
614
671
|
<CodeBlockRendererHtml html={msg.bodyHtml} />
|
|
@@ -632,13 +689,16 @@ const [clientTyping, setClientTyping] = useState(false)
|
|
|
632
689
|
)}
|
|
633
690
|
</div>
|
|
634
691
|
{/* Hover actions — icon buttons with aria-labels (#7) */}
|
|
635
|
-
|
|
636
|
-
{
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
692
|
+
{editingMsgId !== msg.id && !(msg as unknown as { deletedAt?: string }).deletedAt && (
|
|
693
|
+
<div className={s.messageActions}>
|
|
694
|
+
<button className={s.actionIcon} title="Éditer" aria-label="Éditer le message" onClick={() => startEditMessage(msg)} style={{ fontSize: 11, width: 'auto', padding: '4px 8px' }}>Éditer</button>
|
|
695
|
+
{/* #2 — Undo toast instead of confirm() */}
|
|
696
|
+
<button className={`${s.actionIcon} ${s.danger}`} title={t('actions.deleteMessage')} aria-label={t('actions.deleteMessage')} onClick={() => handleDeleteMessage(msg.id)} style={{ fontSize: 11, width: 'auto', padding: '4px 8px' }}>{t('actions.deleteMessage')}</button>
|
|
697
|
+
{/* #2 — Split modal instead of prompt() */}
|
|
698
|
+
{features.splitTicket && !msg.isInternal && <button className={s.actionIcon} title={t('actions.extractMessage')} aria-label={t('actions.extractToNewTicket')} onClick={() => { setSplitModal({ messageId: msg.id, preview: msg.body.slice(0, 200) }); setSplitSubject(`Split: ${ticket.subject}`) }} style={{ fontSize: 11, width: 'auto', padding: '4px 8px' }}>{t('actions.extractMessage')}</button>}
|
|
699
|
+
{isAdmin && !msg.isInternal && <button className={s.actionIcon} title={t('actions.resendEmail')} aria-label={t('actions.resendEmail')} onClick={() => fetch('/api/support/resend-notification', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ messageId: msg.id }) })} style={{ fontSize: 11, width: 'auto', padding: '4px 8px' }}>{t('actions.resendEmail')}</button>}
|
|
700
|
+
</div>
|
|
701
|
+
)}
|
|
642
702
|
</div>
|
|
643
703
|
</React.Fragment>
|
|
644
704
|
)
|
|
@@ -718,13 +778,27 @@ const [clientTyping, setClientTyping] = useState(false)
|
|
|
718
778
|
)}
|
|
719
779
|
<div className={s.composerFooter}>
|
|
720
780
|
<div className={s.composerOptions}>
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
781
|
+
<select
|
|
782
|
+
value={sendAsClient ? 'client' : 'admin'}
|
|
783
|
+
onChange={(e) => {
|
|
784
|
+
const asClient = e.target.value === 'client'
|
|
785
|
+
setSendAsClient(asClient)
|
|
786
|
+
if (asClient) { setIsInternal(false); setNotifyClient(false) }
|
|
787
|
+
}}
|
|
788
|
+
style={{ fontSize: '12px', padding: '4px 8px', fontWeight: 600, borderRadius: 6, border: '1px solid var(--theme-elevation-200)', background: 'var(--theme-elevation-0)', color: 'var(--theme-text)' }}
|
|
789
|
+
>
|
|
790
|
+
<option value="admin">En tant que : Support</option>
|
|
791
|
+
<option value="client">En tant que : Client</option>
|
|
792
|
+
</select>
|
|
793
|
+
{!sendAsClient && (
|
|
794
|
+
<>
|
|
795
|
+
<label><input type="checkbox" checked={isInternal} onChange={(e) => setIsInternal(e.target.checked)} /> {t('detail.internalNote')}</label>
|
|
796
|
+
<label><input type="checkbox" checked={notifyClient} onChange={(e) => setNotifyClient(e.target.checked)} disabled={isInternal} /> {t('detail.notify')}</label>
|
|
797
|
+
</>
|
|
798
|
+
)}
|
|
724
799
|
</div>
|
|
725
|
-
{/* #9 — "Send ->" -> "Envoyer ->" */}
|
|
726
800
|
<button className={`${s.sendBtn} ${isInternal ? s.sendBtnInternal : ''}`} onClick={handleSend} disabled={sending || !replyBody.trim()} data-action="send-reply">
|
|
727
|
-
{sending ? t('detail.sending') : isInternal ? t('detail.sendNote') : t('detail.sendReply')}
|
|
801
|
+
{sending ? t('detail.sending') : sendAsClient ? 'Ajouter message client' : isInternal ? t('detail.sendNote') : t('detail.sendReply')}
|
|
728
802
|
</button>
|
|
729
803
|
</div>
|
|
730
804
|
</div>
|