@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({ ticket: Number(ticketId), body: finalBody, ...finalHtml ? { bodyHtml: finalHtml } : {}, authorType: "admin", isInternal, skipNotification: isInternal || !notifyClient })
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.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 }),
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("label", { children: [
793
- /* @__PURE__ */ jsxRuntime.jsx("input", { type: "checkbox", checked: isInternal, onChange: (e) => setIsInternal(e.target.checked) }),
794
- " ",
795
- t("detail.internalNote")
796
- ] }),
797
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
798
- /* @__PURE__ */ jsxRuntime.jsx("input", { type: "checkbox", checked: notifyClient, onChange: (e) => setNotifyClient(e.target.checked), disabled: isInternal }),
799
- " ",
800
- t("detail.notify")
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({ ticket: Number(ticketId), body: finalBody, ...finalHtml ? { bodyHtml: finalHtml } : {}, authorType: "admin", isInternal, skipNotification: isInternal || !notifyClient })
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.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 }),
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("label", { children: [
786
- /* @__PURE__ */ jsx("input", { type: "checkbox", checked: isInternal, onChange: (e) => setIsInternal(e.target.checked) }),
787
- " ",
788
- t("detail.internalNote")
789
- ] }),
790
- /* @__PURE__ */ jsxs("label", { children: [
791
- /* @__PURE__ */ jsx("input", { type: "checkbox", checked: notifyClient, onChange: (e) => setNotifyClient(e.target.checked), disabled: isInternal }),
792
- " ",
793
- t("detail.notify")
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@consilioweb/payload-support",
3
- "version": "0.9.6",
3
+ "version": "0.9.8",
4
4
  "description": "Payload CMS plugin — professional support & ticketing system with AI, SLA, time tracking, live chat, and more",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -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({ ticket: Number(ticketId), body: finalBody, ...(finalHtml ? { bodyHtml: finalHtml } : {}), authorType: 'admin', isInternal, skipNotification: isInternal || !notifyClient }) })
400
- if (res.ok) { setReplyBody(''); setReplyHtml(''); setIsInternal(false); setPendingFiles([]); editorRef.current?.clear(); fetchAll() }
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
- {(msg as unknown as { deletedAt?: string }).deletedAt ? (
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
- <div className={s.messageActions}>
636
- {/* #2 — Undo toast instead of confirm() */}
637
- <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>
638
- {/* #2 — Split modal instead of prompt() */}
639
- {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>}
640
- {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>}
641
- </div>
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
- {/* #9 — "Internal" -> "Note interne", "Notify" -> "Notifier" */}
722
- <label><input type="checkbox" checked={isInternal} onChange={(e) => setIsInternal(e.target.checked)} /> {t('detail.internalNote')}</label>
723
- <label><input type="checkbox" checked={notifyClient} onChange={(e) => setNotifyClient(e.target.checked)} disabled={isInternal} /> {t('detail.notify')}</label>
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>