@consilioweb/payload-support 0.10.1 → 0.12.0

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 (39) hide show
  1. package/dist/components/TicketConversation/locales/en.json +25 -1
  2. package/dist/components/TicketConversation/locales/fr.json +25 -1
  3. package/dist/index.cjs +799 -4
  4. package/dist/index.d.cts +15 -1
  5. package/dist/index.d.ts +15 -1
  6. package/dist/index.js +800 -6
  7. package/dist/styles/TicketDetail.module.scss +899 -353
  8. package/dist/views/TicketDetailView/client.cjs +504 -282
  9. package/dist/views/TicketDetailView/client.js +505 -283
  10. package/package.json +1 -1
  11. package/src/collections/ClientSummaries.ts +16 -0
  12. package/src/collections/TicketCollaborators.ts +93 -0
  13. package/src/collections/TicketFeedback.ts +116 -0
  14. package/src/collections/TicketMessages.ts +9 -0
  15. package/src/collections/Tickets.ts +31 -1
  16. package/src/collections/index.ts +2 -0
  17. package/src/components/TicketConversation/locales/en.json +25 -1
  18. package/src/components/TicketConversation/locales/fr.json +25 -1
  19. package/src/endpoints/escalate.ts +44 -0
  20. package/src/endpoints/index.ts +15 -0
  21. package/src/endpoints/invite-collaborator.ts +215 -0
  22. package/src/endpoints/kb-search.ts +156 -0
  23. package/src/endpoints/ticket-feedback.ts +104 -0
  24. package/src/endpoints/transfer-ticket.ts +248 -0
  25. package/src/index.ts +1 -0
  26. package/src/plugin.ts +4 -0
  27. package/src/portal/auth/ChatWidget.tsx +11 -0
  28. package/src/portal/auth/dashboard/DashboardClient.tsx +85 -99
  29. package/src/portal/auth/dashboard/page.tsx +11 -2
  30. package/src/portal/auth/tickets/detail/TransferAndInviteActions.tsx +402 -0
  31. package/src/portal/auth/tickets/detail/page.tsx +122 -23
  32. package/src/portal/auth/tickets/new/KbDeflection.tsx +128 -0
  33. package/src/portal/auth/tickets/new/page.tsx +203 -100
  34. package/src/portal/locales/en.json +5 -0
  35. package/src/portal/locales/fr.json +5 -0
  36. package/src/styles/TicketDetail.module.scss +899 -353
  37. package/src/types.ts +19 -0
  38. package/src/utils/slugs.ts +2 -0
  39. package/src/views/TicketDetailView/client.tsx +404 -121
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
3
- import React, { useRef, useState, useCallback, useEffect } from 'react';
3
+ import React, { useRef, useState, useEffect, useCallback } from 'react';
4
4
  import { useSearchParams } from 'next/navigation';
5
5
  import Link from 'next/link';
6
6
  import { RichTextEditor } from '../../components/RichTextEditor/index.js';
@@ -11,6 +11,10 @@ import { computeSlaState, formatSlaRemaining } from '../shared/sla.js';
11
11
  import { useTranslation } from '../../components/TicketConversation/hooks/useTranslation.js';
12
12
  import s from '../../styles/TicketDetail.module.scss';
13
13
 
14
+ const ALIASES = [
15
+ { value: "Support Consilioweb", label: "En tant que : Support Consilioweb" },
16
+ { value: "contact@consilioweb.fr", label: "En tant que : contact@consilioweb.fr" }
17
+ ];
14
18
  const STATUS_STYLE = {
15
19
  open: { bg: "#dbeafe", color: "#1e40af" },
16
20
  waiting_client: { bg: "#fef3c7", color: "#92400e" },
@@ -112,6 +116,34 @@ const RewriteDropdown = ({ disabled, loading, onSelect, toolbarBtnClass }) => {
112
116
  )) })
113
117
  ] });
114
118
  };
119
+ function NextActionItem({ action, index, onToggle }) {
120
+ const [pending, setPending] = useState(false);
121
+ const labelMap = { now: "Maintenant", today: "Aujourd'hui", "this-week": "Cette semaine" };
122
+ const done = !!action.done;
123
+ return /* @__PURE__ */ jsxs("li", { className: s.aiCardNextAction, children: [
124
+ /* @__PURE__ */ jsx(
125
+ "button",
126
+ {
127
+ type: "button",
128
+ className: `${s.aiCardNextCheck} ${done ? s.aiCardNextCheckDone : ""}`,
129
+ onClick: async () => {
130
+ setPending(true);
131
+ try {
132
+ await onToggle(!done);
133
+ } finally {
134
+ setPending(false);
135
+ }
136
+ },
137
+ "aria-pressed": done,
138
+ "aria-label": done ? "Marquer non fait" : "Marquer fait",
139
+ disabled: pending,
140
+ children: done && /* @__PURE__ */ jsx("span", { "aria-hidden": true, children: "\u2713" })
141
+ }
142
+ ),
143
+ /* @__PURE__ */ jsx("span", { className: `${s.aiCardNextLabel} ${done ? s.aiCardNextLabelDone : ""}`, children: action.label }),
144
+ /* @__PURE__ */ jsx("span", { className: s.aiCardNextWhen, children: labelMap[action.priority] || action.priority })
145
+ ] });
146
+ }
115
147
  const TicketDetailClient = () => {
116
148
  const { t } = useTranslation();
117
149
  const searchParams = useSearchParams();
@@ -133,6 +165,7 @@ const TicketDetailClient = () => {
133
165
  const [isInternal, setIsInternal] = useState(false);
134
166
  const [notifyClient, setNotifyClient] = useState(true);
135
167
  const [sendAsClient, setSendAsClient] = useState(false);
168
+ const [fromAlias, setFromAlias] = useState("");
136
169
  const [editingMsgId, setEditingMsgId] = useState(null);
137
170
  const [editingBody, setEditingBody] = useState("");
138
171
  const [editingHtml, setEditingHtml] = useState("");
@@ -142,10 +175,20 @@ const TicketDetailClient = () => {
142
175
  const [showMenu, setShowMenu] = useState(false);
143
176
  const [clientTyping, setClientTyping] = useState(false);
144
177
  const [aiReplying, setAiReplying] = useState(false);
178
+ const [aiSuggestion, setAiSuggestion] = useState(null);
145
179
  const [aiRewriting, setAiRewriting] = useState(false);
146
180
  const [sentiment, setSentiment] = useState(null);
147
181
  const [statusUpdating, setStatusUpdating] = useState(false);
148
- const [showActivity, setShowActivity] = useState(false);
182
+ const [sidebarTab, setSidebarTab] = useState(() => {
183
+ if (typeof window === "undefined") return "overview";
184
+ const v = localStorage.getItem("support_sidebar_tab");
185
+ return v === "client" || v === "activity" ? v : "overview";
186
+ });
187
+ useEffect(() => {
188
+ if (typeof window === "undefined") return;
189
+ localStorage.setItem("support_sidebar_tab", sidebarTab);
190
+ }, [sidebarTab]);
191
+ const [previousTickets, setPreviousTickets] = useState([]);
149
192
  const [clientSummary, setClientSummary] = useState(null);
150
193
  const [summaryLoading, setSummaryLoading] = useState(false);
151
194
  const [timerRunning, setTimerRunning] = useState(() => {
@@ -222,6 +265,22 @@ const TicketDetailClient = () => {
222
265
  }).catch(() => {
223
266
  }).finally(() => setSummaryLoading(false));
224
267
  }, [ticket?.id]);
268
+ useEffect(() => {
269
+ if (sidebarTab !== "client" || !ticket || !ticketId) return;
270
+ const clientId = typeof ticket.client === "object" ? ticket.client?.id : ticket.client;
271
+ if (!clientId) return;
272
+ let aborted = false;
273
+ fetch(
274
+ `/api/tickets?where[client][equals]=${clientId}&where[id][not_equals]=${ticketId}&limit=5&sort=-createdAt&depth=0`,
275
+ { credentials: "include" }
276
+ ).then((r) => r.ok ? r.json() : null).then((d) => {
277
+ if (!aborted && d?.docs) setPreviousTickets(d.docs);
278
+ }).catch(() => {
279
+ });
280
+ return () => {
281
+ aborted = true;
282
+ };
283
+ }, [sidebarTab, ticket?.id, ticketId]);
225
284
  useEffect(() => {
226
285
  if (!ticketId || loading) return;
227
286
  const iv = setInterval(async () => {
@@ -425,6 +484,7 @@ ${uploadedLinks.join("\n")}` : replyBody.trim() || "[Contenu enrichi]";
425
484
  ...finalHtml ? { bodyHtml: finalHtml } : {},
426
485
  authorType: sendAsClient ? "client" : "admin",
427
486
  ...sendAsClient && client ? { authorClient: client.id } : {},
487
+ ...fromAlias && !sendAsClient ? { fromAlias } : {},
428
488
  isInternal: sendAsClient ? false : isInternal,
429
489
  skipNotification: sendAsClient || isInternal || !notifyClient
430
490
  })
@@ -434,6 +494,7 @@ ${uploadedLinks.join("\n")}` : replyBody.trim() || "[Contenu enrichi]";
434
494
  setReplyHtml("");
435
495
  setIsInternal(false);
436
496
  setSendAsClient(false);
497
+ setFromAlias("");
437
498
  setPendingFiles([]);
438
499
  editorRef.current?.clear();
439
500
  fetchAll();
@@ -528,21 +589,29 @@ ${uploadedLinks.join("\n")}` : replyBody.trim() || "[Contenu enrichi]";
528
589
  method: "POST",
529
590
  headers: { "Content-Type": "application/json" },
530
591
  credentials: "include",
531
- body: JSON.stringify({ action: "suggest_reply", messages: messages.slice(-10).map((m) => ({ authorType: m.authorType, body: m.body })), clientName: `${client?.firstName || ""} ${client?.lastName || ""}`.trim(), clientCompany: client?.company })
592
+ body: JSON.stringify({
593
+ action: "suggest_reply",
594
+ messages: messages.slice(-10).map((m) => ({ authorType: m.authorType, body: m.body })),
595
+ clientName: `${client?.firstName || ""} ${client?.lastName || ""}`.trim(),
596
+ clientCompany: client?.company
597
+ })
532
598
  });
533
599
  if (r.ok) {
534
600
  const d = await r.json();
535
- if (d.reply) {
536
- setReplyBody(d.reply);
537
- setReplyHtml(d.reply.replace(/\n/g, "<br/>"));
538
- editorRef.current?.setContent(d.reply.replace(/\n/g, "<br/>"));
539
- }
601
+ if (d.reply) setAiSuggestion(d.reply);
540
602
  }
541
603
  } catch {
542
604
  } finally {
543
605
  setAiReplying(false);
544
606
  }
545
607
  };
608
+ const handleAiSuggestionInsert = () => {
609
+ if (!aiSuggestion) return;
610
+ setReplyBody(aiSuggestion);
611
+ setReplyHtml(aiSuggestion.replace(/\n/g, "<br/>"));
612
+ editorRef.current?.setContent(aiSuggestion.replace(/\n/g, "<br/>"));
613
+ setAiSuggestion(null);
614
+ };
546
615
  const handleAiRewrite = async (style = "auto") => {
547
616
  if (!replyBody.trim()) return;
548
617
  setAiRewriting(true);
@@ -636,8 +705,22 @@ ${uploadedLinks.join("\n")}` : replyBody.trim() || "[Contenu enrichi]";
636
705
  /* @__PURE__ */ jsxs("div", { className: s.topBar, children: [
637
706
  /* @__PURE__ */ jsx(Link, { href: "/admin/support/inbox", className: s.backLink, "aria-label": "Retour \xE0 la bo\xEEte de r\xE9ception", children: "\u2190" }),
638
707
  /* @__PURE__ */ jsxs("div", { className: s.ticketMeta, children: [
639
- /* @__PURE__ */ jsx("span", { className: s.ticketNumber, children: ticket.ticketNumber }),
640
- /* @__PURE__ */ jsx("span", { className: s.ticketSubject, children: ticket.subject })
708
+ /* @__PURE__ */ jsxs("span", { className: s.ticketNumber, children: [
709
+ /* @__PURE__ */ jsx("span", { children: ticket.ticketNumber }),
710
+ ticket.category ? /* @__PURE__ */ jsxs(Fragment, { children: [
711
+ /* @__PURE__ */ jsx("span", { className: s.ticketNumberSep, children: "\xB7" }),
712
+ /* @__PURE__ */ jsx("span", { className: s.ticketNumberCat, children: ticket.category })
713
+ ] }) : null,
714
+ /* @__PURE__ */ jsx("span", { className: s.ticketNumberSep, children: "\xB7" }),
715
+ /* @__PURE__ */ jsxs("span", { className: s.ticketNumberSource, "aria-label": t("detail.source"), children: [
716
+ /* @__PURE__ */ jsxs("svg", { width: "11", height: "11", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: [
717
+ /* @__PURE__ */ jsx("rect", { x: "3", y: "5", width: "18", height: "14", rx: "2" }),
718
+ /* @__PURE__ */ jsx("path", { d: "m3 7 9 6 9-6" })
719
+ ] }),
720
+ ticket.source === "email" ? "Email" : ticket.source || t("ticket.source.portal")
721
+ ] })
722
+ ] }),
723
+ /* @__PURE__ */ jsx("h1", { className: s.ticketSubject, children: ticket.subject })
641
724
  ] }),
642
725
  /* @__PURE__ */ jsxs("div", { className: s.topBarRight, children: [
643
726
  /* @__PURE__ */ jsxs("select", { className: s.statusChip, style: { background: st.bg, color: st.color }, value: ticket.status || "open", onChange: (e) => handleStatusChange(e.target.value), disabled: statusUpdating, "aria-label": t("ticket.status.label"), children: [
@@ -742,7 +825,7 @@ ${uploadedLinks.join("\n")}` : replyBody.trim() || "[Contenu enrichi]";
742
825
  ),
743
826
  /* @__PURE__ */ jsxs("div", { className: s.messageContent, children: [
744
827
  /* @__PURE__ */ jsxs("div", { className: s.messageHeader, children: [
745
- /* @__PURE__ */ jsx("span", { className: s.messageAuthor, children: isAdmin ? "Support" : msg.authorType === "email" ? "Email" : client?.firstName || "Client" }),
828
+ /* @__PURE__ */ jsx("span", { className: s.messageAuthor, children: msg.fromAlias || (isAdmin ? "Support" : msg.authorType === "email" ? "Email" : client?.firstName || "Client") }),
746
829
  /* @__PURE__ */ jsx("span", { className: s.messageTime, children: timeAgo(msg.createdAt) }),
747
830
  msg.isInternal && /* @__PURE__ */ jsx("span", { className: s.badge, style: { background: "#fef3c7", color: "#92400e" }, children: "Interne" }),
748
831
  msg.isSolution && /* @__PURE__ */ jsx("span", { className: s.badge, style: { background: "#dcfce7", color: "#166534" }, children: "Solution" }),
@@ -828,6 +911,27 @@ ${uploadedLinks.join("\n")}` : replyBody.trim() || "[Contenu enrichi]";
828
911
  ] }),
829
912
  t("detail.typing", { name: client?.firstName || "Client" })
830
913
  ] }),
914
+ aiSuggestion && /* @__PURE__ */ jsxs("div", { className: s.aiSuggestionPreview, role: "region", "aria-label": t("composer.aiSuggestion.label"), children: [
915
+ /* @__PURE__ */ jsxs("div", { className: s.aiSuggestionPreviewHeader, children: [
916
+ /* @__PURE__ */ jsx("span", { className: s.aiSuggestionIcon, "aria-hidden": true, children: "\u2728" }),
917
+ /* @__PURE__ */ jsx("span", { className: s.aiSuggestionLabel, children: t("composer.aiSuggestion.label") }),
918
+ /* @__PURE__ */ jsx(
919
+ "button",
920
+ {
921
+ type: "button",
922
+ className: s.aiSuggestionDismiss,
923
+ onClick: () => setAiSuggestion(null),
924
+ "aria-label": t("composer.aiSuggestion.ignore"),
925
+ children: "\xD7"
926
+ }
927
+ )
928
+ ] }),
929
+ /* @__PURE__ */ jsx("div", { className: s.aiSuggestionBody, children: aiSuggestion }),
930
+ /* @__PURE__ */ jsxs("div", { className: s.aiSuggestionActions, children: [
931
+ /* @__PURE__ */ jsx("button", { type: "button", className: s.aiSuggestionIgnore, onClick: () => setAiSuggestion(null), children: t("composer.aiSuggestion.ignore") }),
932
+ /* @__PURE__ */ jsx("button", { type: "button", className: s.aiSuggestionInsert, onClick: handleAiSuggestionInsert, children: t("composer.aiSuggestion.insert") })
933
+ ] })
934
+ ] }),
831
935
  /* @__PURE__ */ jsxs(
832
936
  "div",
833
937
  {
@@ -849,7 +953,7 @@ ${uploadedLinks.join("\n")}` : replyBody.trim() || "[Contenu enrichi]";
849
953
  "aria-label": t("detail.iaSuggestion"),
850
954
  onClick: handleAiSuggest,
851
955
  disabled: aiReplying || messages.length === 0,
852
- children: aiReplying ? "\u2026" : `\u2728 ${t("detail.iaSuggestion")}`
956
+ children: aiReplying ? `\u23F3 ${t("composer.aiSuggestion.loading")}` : `\u2728 ${t("detail.iaSuggestion")}`
853
957
  }
854
958
  ),
855
959
  /* @__PURE__ */ jsx(RewriteDropdown, { disabled: aiRewriting || !replyBody.trim(), loading: aiRewriting, onSelect: (style) => handleAiRewrite(style), toolbarBtnClass: s.toolbarBtn })
@@ -907,39 +1011,41 @@ ${uploadedLinks.join("\n")}` : replyBody.trim() || "[Contenu enrichi]";
907
1011
  cannedResponses.map((cr) => /* @__PURE__ */ jsx("option", { value: String(cr.id), children: cr.title }, cr.id))
908
1012
  ] })
909
1013
  ] }),
910
- /* @__PURE__ */ jsx(
911
- RichTextEditor,
912
- {
913
- ref: editorRef,
914
- onChange: (html, text) => {
915
- setReplyHtml(html);
916
- setReplyBody(text);
917
- },
918
- placeholder: isInternal ? t("composer.placeholderInternal") : t("composer.placeholderReplyTo", { name: client?.firstName || "client" }),
919
- minHeight: 100,
920
- borderColor: "transparent",
921
- onFileUpload: async (file) => {
922
- try {
923
- const formData = new FormData();
924
- formData.append("file", file);
925
- formData.append("_payload", JSON.stringify({ alt: file.name }));
926
- const ur = await fetch("/api/media", { method: "POST", credentials: "include", body: formData });
927
- if (!ur.ok) return null;
928
- const ud = await ur.json();
929
- return ud.doc?.url || null;
930
- } catch {
931
- return null;
1014
+ /* @__PURE__ */ jsxs("div", { className: s.composerBox, children: [
1015
+ /* @__PURE__ */ jsx(
1016
+ RichTextEditor,
1017
+ {
1018
+ ref: editorRef,
1019
+ onChange: (html, text) => {
1020
+ setReplyHtml(html);
1021
+ setReplyBody(text);
1022
+ },
1023
+ placeholder: isInternal ? t("composer.placeholderInternal") : t("composer.placeholderReplyTo", { name: client?.firstName || "client" }),
1024
+ minHeight: 100,
1025
+ borderColor: "transparent",
1026
+ onFileUpload: async (file) => {
1027
+ try {
1028
+ const formData = new FormData();
1029
+ formData.append("file", file);
1030
+ formData.append("_payload", JSON.stringify({ alt: file.name }));
1031
+ const ur = await fetch("/api/media", { method: "POST", credentials: "include", body: formData });
1032
+ if (!ur.ok) return null;
1033
+ const ud = await ur.json();
1034
+ return ud.doc?.url || null;
1035
+ } catch {
1036
+ return null;
1037
+ }
932
1038
  }
933
1039
  }
934
- }
935
- ),
936
- pendingFiles.length > 0 && /* @__PURE__ */ jsx("div", { className: s.uploadPreview, children: pendingFiles.map((f, i) => /* @__PURE__ */ jsxs("div", { className: s.uploadPreviewItem, children: [
937
- /* @__PURE__ */ jsxs("span", { children: [
938
- "PJ ",
939
- f.name
940
- ] }),
941
- /* @__PURE__ */ jsx("button", { className: s.uploadRemoveBtn, "aria-label": `Retirer ${f.name}`, onClick: () => setPendingFiles((prev) => prev.filter((_, j) => j !== i)), children: "\xD7" })
942
- ] }, i)) }),
1040
+ ),
1041
+ pendingFiles.length > 0 && /* @__PURE__ */ jsx("div", { className: s.uploadPreview, children: pendingFiles.map((f, i) => /* @__PURE__ */ jsxs("div", { className: s.uploadPreviewItem, children: [
1042
+ /* @__PURE__ */ jsxs("span", { children: [
1043
+ "PJ ",
1044
+ f.name
1045
+ ] }),
1046
+ /* @__PURE__ */ jsx("button", { className: s.uploadRemoveBtn, "aria-label": `Retirer ${f.name}`, onClick: () => setPendingFiles((prev) => prev.filter((_, j) => j !== i)), children: "\xD7" })
1047
+ ] }, i)) })
1048
+ ] }),
943
1049
  /* @__PURE__ */ jsxs("div", { className: s.composerFooter, children: [
944
1050
  /* @__PURE__ */ jsxs("div", { className: s.composerOptions, children: [
945
1051
  /* @__PURE__ */ jsxs(
@@ -961,6 +1067,20 @@ ${uploadedLinks.join("\n")}` : replyBody.trim() || "[Contenu enrichi]";
961
1067
  ]
962
1068
  }
963
1069
  ),
1070
+ !sendAsClient && /* @__PURE__ */ jsxs(
1071
+ "select",
1072
+ {
1073
+ className: s.composerAliasSelect,
1074
+ value: fromAlias,
1075
+ onChange: (e) => setFromAlias(e.target.value),
1076
+ "aria-label": t("composer.aliases.label"),
1077
+ title: t("composer.aliases.tooltip"),
1078
+ children: [
1079
+ /* @__PURE__ */ jsx("option", { value: "", children: t("composer.aliases.self") }),
1080
+ ALIASES.map((a) => /* @__PURE__ */ jsx("option", { value: a.value, children: a.label }, a.value))
1081
+ ]
1082
+ }
1083
+ ),
964
1084
  !sendAsClient && /* @__PURE__ */ jsxs(Fragment, { children: [
965
1085
  /* @__PURE__ */ jsxs("label", { children: [
966
1086
  /* @__PURE__ */ jsx("input", { type: "checkbox", checked: isInternal, onChange: (e) => setIsInternal(e.target.checked) }),
@@ -985,294 +1105,396 @@ ${uploadedLinks.join("\n")}` : replyBody.trim() || "[Contenu enrichi]";
985
1105
  )
986
1106
  ] }),
987
1107
  /* @__PURE__ */ jsxs("div", { className: s.sidebar, children: [
988
- client && /* @__PURE__ */ jsxs("div", { className: s.sideSection, children: [
989
- /* @__PURE__ */ jsxs("div", { className: s.clientCard, children: [
990
- /* @__PURE__ */ jsx("div", { className: s.clientAvatar, children: initials }),
991
- /* @__PURE__ */ jsxs("div", { className: s.clientInfo, children: [
992
- /* @__PURE__ */ jsxs("div", { className: s.clientName, children: [
993
- client.firstName,
994
- " ",
995
- client.lastName
996
- ] }),
997
- /* @__PURE__ */ jsx("div", { className: s.clientCompany, children: client.company }),
998
- /* @__PURE__ */ jsx("a", { href: `mailto:${client.email}`, className: s.clientEmail, children: client.email })
999
- ] })
1000
- ] }),
1001
- /* @__PURE__ */ jsxs("div", { className: s.clientActions, children: [
1002
- /* @__PURE__ */ jsx(Link, { href: `/admin/collections/support-clients/${client.id}`, className: s.smallBtn, children: t("client.clientSheet") }),
1003
- /* @__PURE__ */ jsx("button", { className: s.smallBtn, onClick: () => window.open(`/api/admin/impersonate?clientId=${client.id}`, "_blank"), children: t("client.clientPortal") })
1004
- ] })
1005
- ] }),
1006
- clientSummary && clientSummary.summary && /* @__PURE__ */ jsxs("div", { className: `${s.sideSection} ${s.aiCard}`, children: [
1007
- /* @__PURE__ */ jsxs("div", { className: s.aiCardTitle, children: [
1008
- /* @__PURE__ */ jsx("span", { className: s.aiCardIcon, "aria-hidden": true, children: "\u2728" }),
1009
- "Synth\xE8se client"
1010
- ] }),
1011
- /* @__PURE__ */ jsx("p", { className: s.aiCardLead, children: clientSummary.summary }),
1012
- clientSummary.recurringTopics && clientSummary.recurringTopics.length > 0 && /* @__PURE__ */ jsx("div", { className: s.aiCardChips, children: clientSummary.recurringTopics.slice(0, 5).map((tp, i) => /* @__PURE__ */ jsx("span", { className: s.aiChip, children: tp.topic }, `tp-${i}`)) }),
1013
- clientSummary.keyFacts && clientSummary.keyFacts.length > 0 && /* @__PURE__ */ jsx("ul", { className: s.aiCardFacts, children: clientSummary.keyFacts.slice(0, 4).map((f, i) => /* @__PURE__ */ jsxs("li", { className: s.aiCardFact, children: [
1014
- /* @__PURE__ */ jsx("span", { className: s.aiCardDot, "aria-hidden": true }),
1015
- f
1016
- ] }, `kf-${i}`)) })
1108
+ /* @__PURE__ */ jsxs("nav", { role: "tablist", "aria-label": t("detail.sidebar.tabs.overview"), className: s.sidebarTabs, children: [
1109
+ /* @__PURE__ */ jsx(
1110
+ "button",
1111
+ {
1112
+ type: "button",
1113
+ role: "tab",
1114
+ id: "sidebar-tab-overview",
1115
+ "aria-selected": sidebarTab === "overview",
1116
+ "aria-controls": "sidebar-panel-overview",
1117
+ tabIndex: sidebarTab === "overview" ? 0 : -1,
1118
+ className: `${s.sidebarTab} ${sidebarTab === "overview" ? s.sidebarTabActive : ""}`,
1119
+ onClick: () => setSidebarTab("overview"),
1120
+ children: t("detail.sidebar.tabs.overview")
1121
+ }
1122
+ ),
1123
+ /* @__PURE__ */ jsx(
1124
+ "button",
1125
+ {
1126
+ type: "button",
1127
+ role: "tab",
1128
+ id: "sidebar-tab-client",
1129
+ "aria-selected": sidebarTab === "client",
1130
+ "aria-controls": "sidebar-panel-client",
1131
+ tabIndex: sidebarTab === "client" ? 0 : -1,
1132
+ className: `${s.sidebarTab} ${sidebarTab === "client" ? s.sidebarTabActive : ""}`,
1133
+ onClick: () => setSidebarTab("client"),
1134
+ children: t("detail.sidebar.tabs.client")
1135
+ }
1136
+ ),
1137
+ /* @__PURE__ */ jsx(
1138
+ "button",
1139
+ {
1140
+ type: "button",
1141
+ role: "tab",
1142
+ id: "sidebar-tab-activity",
1143
+ "aria-selected": sidebarTab === "activity",
1144
+ "aria-controls": "sidebar-panel-activity",
1145
+ tabIndex: sidebarTab === "activity" ? 0 : -1,
1146
+ className: `${s.sidebarTab} ${sidebarTab === "activity" ? s.sidebarTabActive : ""}`,
1147
+ onClick: () => setSidebarTab("activity"),
1148
+ children: t("detail.sidebar.tabs.activity")
1149
+ }
1150
+ )
1017
1151
  ] }),
1018
- /* @__PURE__ */ jsxs("div", { className: s.sideSection, children: [
1019
- /* @__PURE__ */ jsx("div", { className: s.sideSectionTitle, children: t("detail.details") }),
1020
- /* @__PURE__ */ jsxs("div", { className: s.sideField, children: [
1021
- /* @__PURE__ */ jsx("span", { className: s.sideLabel, children: t("detail.priority") }),
1022
- /* @__PURE__ */ jsxs("select", { className: s.sideSelect, value: ticket.priority || "normal", onChange: (e) => handleFieldPatch("priority", e.target.value), "aria-label": t("detail.priority"), children: [
1023
- /* @__PURE__ */ jsx("option", { value: "low", children: t("ticket.priority.low") }),
1024
- /* @__PURE__ */ jsx("option", { value: "normal", children: t("ticket.priority.normal") }),
1025
- /* @__PURE__ */ jsx("option", { value: "high", children: t("ticket.priority.high") }),
1026
- /* @__PURE__ */ jsx("option", { value: "urgent", children: t("ticket.priority.urgent") })
1152
+ sidebarTab === "overview" && /* @__PURE__ */ jsxs("div", { role: "tabpanel", id: "sidebar-panel-overview", "aria-labelledby": "sidebar-tab-overview", children: [
1153
+ client && /* @__PURE__ */ jsxs("div", { className: s.sideSection, children: [
1154
+ /* @__PURE__ */ jsxs("div", { className: s.clientCard, children: [
1155
+ /* @__PURE__ */ jsx("div", { className: s.clientAvatar, children: initials }),
1156
+ /* @__PURE__ */ jsxs("div", { className: s.clientInfo, children: [
1157
+ /* @__PURE__ */ jsxs("div", { className: s.clientName, children: [
1158
+ client.firstName,
1159
+ " ",
1160
+ client.lastName
1161
+ ] }),
1162
+ /* @__PURE__ */ jsx("div", { className: s.clientCompany, children: client.company }),
1163
+ /* @__PURE__ */ jsx("a", { href: `mailto:${client.email}`, className: s.clientEmail, children: client.email })
1164
+ ] })
1165
+ ] }),
1166
+ /* @__PURE__ */ jsxs("div", { className: s.clientActions, children: [
1167
+ /* @__PURE__ */ jsx(Link, { href: `/admin/collections/support-clients/${client.id}`, className: s.smallBtn, children: t("client.clientSheet") }),
1168
+ /* @__PURE__ */ jsx("button", { className: s.smallBtn, onClick: () => window.open(`/api/admin/impersonate?clientId=${client.id}`, "_blank"), children: t("client.clientPortal") })
1027
1169
  ] })
1028
1170
  ] }),
1029
- /* @__PURE__ */ jsxs("div", { className: s.sideField, children: [
1030
- /* @__PURE__ */ jsx("span", { className: s.sideLabel, children: t("detail.category") }),
1031
- /* @__PURE__ */ jsxs("select", { className: s.sideSelect, value: ticket.category || "", onChange: (e) => handleFieldPatch("category", e.target.value), "aria-label": t("detail.category"), children: [
1032
- /* @__PURE__ */ jsx("option", { value: "", children: "\u2014" }),
1033
- /* @__PURE__ */ jsx("option", { value: "bug", children: t("ticket.category.bug") }),
1034
- /* @__PURE__ */ jsx("option", { value: "content", children: t("ticket.category.content") }),
1035
- /* @__PURE__ */ jsx("option", { value: "feature", children: t("ticket.category.feature") }),
1036
- /* @__PURE__ */ jsx("option", { value: "question", children: t("ticket.category.question") }),
1037
- /* @__PURE__ */ jsx("option", { value: "hosting", children: t("ticket.category.hosting") })
1171
+ clientSummary && clientSummary.summary && /* @__PURE__ */ jsxs("div", { className: `${s.sideSection} ${s.aiCard}`, children: [
1172
+ /* @__PURE__ */ jsxs("div", { className: s.aiCardTitle, children: [
1173
+ /* @__PURE__ */ jsx("span", { className: s.aiCardIcon, "aria-hidden": true, children: "\u2728" }),
1174
+ "Synth\xE8se client"
1175
+ ] }),
1176
+ /* @__PURE__ */ jsx("p", { className: s.aiCardLead, children: clientSummary.summary }),
1177
+ clientSummary.recurringTopics && clientSummary.recurringTopics.length > 0 && /* @__PURE__ */ jsx("div", { className: s.aiCardChips, children: clientSummary.recurringTopics.slice(0, 5).map((tp, i) => /* @__PURE__ */ jsx("span", { className: s.aiChip, children: tp.topic }, `tp-${i}`)) }),
1178
+ clientSummary.keyFacts && clientSummary.keyFacts.length > 0 && /* @__PURE__ */ jsx("ul", { className: s.aiCardFacts, children: clientSummary.keyFacts.slice(0, 4).map((f, i) => /* @__PURE__ */ jsxs("li", { className: s.aiCardFact, children: [
1179
+ /* @__PURE__ */ jsx("span", { className: s.aiCardDot, "aria-hidden": true }),
1180
+ f
1181
+ ] }, `kf-${i}`)) }),
1182
+ clientSummary?.nextActions && clientSummary.nextActions.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
1183
+ /* @__PURE__ */ jsx("h5", { className: s.aiCardNextTitle, children: t("detail.aiCard.nextActions.title") }),
1184
+ /* @__PURE__ */ jsx("ul", { className: s.aiCardNextActions, children: clientSummary.nextActions.map((action, i) => /* @__PURE__ */ jsx(
1185
+ NextActionItem,
1186
+ {
1187
+ action,
1188
+ index: i,
1189
+ onToggle: async (done) => {
1190
+ const updatedActions = (clientSummary.nextActions || []).map(
1191
+ (a, j) => j === i ? { ...a, done } : a
1192
+ );
1193
+ if (clientSummary.id != null) {
1194
+ try {
1195
+ await fetch(`/api/client-summaries/${clientSummary.id}`, {
1196
+ method: "PATCH",
1197
+ credentials: "include",
1198
+ headers: { "Content-Type": "application/json" },
1199
+ body: JSON.stringify({ nextActions: updatedActions })
1200
+ });
1201
+ } catch {
1202
+ }
1203
+ }
1204
+ setClientSummary({ ...clientSummary, nextActions: updatedActions });
1205
+ }
1206
+ },
1207
+ action.id || `na-${i}`
1208
+ )) })
1038
1209
  ] })
1039
1210
  ] }),
1040
- /* @__PURE__ */ jsxs("div", { className: s.sideField, children: [
1041
- /* @__PURE__ */ jsx("span", { className: s.sideLabel, children: t("detail.source") }),
1042
- /* @__PURE__ */ jsx("span", { className: s.sideValue, children: ticket.source || t("ticket.source.portal") })
1211
+ /* @__PURE__ */ jsxs("div", { className: s.sideSection, children: [
1212
+ /* @__PURE__ */ jsx("div", { className: s.sideSectionTitle, children: t("detail.details") }),
1213
+ /* @__PURE__ */ jsxs("div", { className: s.sideField, children: [
1214
+ /* @__PURE__ */ jsx("span", { className: s.sideLabel, children: t("detail.priority") }),
1215
+ /* @__PURE__ */ jsxs("select", { className: s.sideSelect, value: ticket.priority || "normal", onChange: (e) => handleFieldPatch("priority", e.target.value), "aria-label": t("detail.priority"), children: [
1216
+ /* @__PURE__ */ jsx("option", { value: "low", children: t("ticket.priority.low") }),
1217
+ /* @__PURE__ */ jsx("option", { value: "normal", children: t("ticket.priority.normal") }),
1218
+ /* @__PURE__ */ jsx("option", { value: "high", children: t("ticket.priority.high") }),
1219
+ /* @__PURE__ */ jsx("option", { value: "urgent", children: t("ticket.priority.urgent") })
1220
+ ] })
1221
+ ] }),
1222
+ /* @__PURE__ */ jsxs("div", { className: s.sideField, children: [
1223
+ /* @__PURE__ */ jsx("span", { className: s.sideLabel, children: t("detail.category") }),
1224
+ /* @__PURE__ */ jsxs("select", { className: s.sideSelect, value: ticket.category || "", onChange: (e) => handleFieldPatch("category", e.target.value), "aria-label": t("detail.category"), children: [
1225
+ /* @__PURE__ */ jsx("option", { value: "", children: "\u2014" }),
1226
+ /* @__PURE__ */ jsx("option", { value: "bug", children: t("ticket.category.bug") }),
1227
+ /* @__PURE__ */ jsx("option", { value: "content", children: t("ticket.category.content") }),
1228
+ /* @__PURE__ */ jsx("option", { value: "feature", children: t("ticket.category.feature") }),
1229
+ /* @__PURE__ */ jsx("option", { value: "question", children: t("ticket.category.question") }),
1230
+ /* @__PURE__ */ jsx("option", { value: "hosting", children: t("ticket.category.hosting") })
1231
+ ] })
1232
+ ] }),
1233
+ /* @__PURE__ */ jsxs("div", { className: s.sideField, children: [
1234
+ /* @__PURE__ */ jsx("span", { className: s.sideLabel, children: t("detail.source") }),
1235
+ /* @__PURE__ */ jsx("span", { className: s.sideValue, children: ticket.source || t("ticket.source.portal") })
1236
+ ] }),
1237
+ /* @__PURE__ */ jsxs("div", { className: s.sideField, children: [
1238
+ /* @__PURE__ */ jsx("span", { className: s.sideLabel, children: t("detail.assigned") }),
1239
+ /* @__PURE__ */ jsx("span", { className: s.sideValue, children: typeof ticket.assignedTo === "object" && ticket.assignedTo ? ticket.assignedTo.firstName || "Admin" : "\u2014" })
1240
+ ] })
1043
1241
  ] }),
1044
- /* @__PURE__ */ jsxs("div", { className: s.sideField, children: [
1045
- /* @__PURE__ */ jsx("span", { className: s.sideLabel, children: t("detail.assigned") }),
1046
- /* @__PURE__ */ jsx("span", { className: s.sideValue, children: typeof ticket.assignedTo === "object" && ticket.assignedTo ? ticket.assignedTo.firstName || "Admin" : "\u2014" })
1047
- ] })
1048
- ] }),
1049
- /* @__PURE__ */ jsxs("div", { className: s.sideSection, children: [
1050
- /* @__PURE__ */ jsx("div", { className: s.sideSectionTitle, children: t("detail.tags") }),
1051
- /* @__PURE__ */ jsxs("div", { className: s.tagsWrap, children: [
1052
- tags.map((tag) => /* @__PURE__ */ jsxs("span", { className: s.tagChip, children: [
1053
- tag,
1054
- /* @__PURE__ */ jsx("button", { className: s.tagRemove, "aria-label": `Retirer le tag ${tag}`, onClick: () => handleRemoveTag(tag), children: "\xD7" })
1055
- ] }, tag)),
1056
- addingTag ? /* @__PURE__ */ jsx(
1057
- "input",
1058
- {
1059
- className: s.tagInput,
1060
- value: newTagValue,
1061
- onChange: (e) => setNewTagValue(e.target.value),
1062
- onKeyDown: (e) => {
1063
- if (e.key === "Enter") handleAddTag();
1064
- if (e.key === "Escape") {
1065
- setAddingTag(false);
1066
- setNewTagValue("");
1242
+ features.timeTracking && /* @__PURE__ */ jsxs("div", { className: s.sideSection, children: [
1243
+ /* @__PURE__ */ jsxs("div", { className: s.sideSectionTitle, children: [
1244
+ /* @__PURE__ */ jsx("span", { children: t("detail.time") }),
1245
+ /* @__PURE__ */ jsx("span", { style: { color: "var(--theme-text)", fontWeight: 600, textTransform: "none", letterSpacing: 0 }, children: totalMin > 0 ? `${Math.floor(totalMin / 60)}h${String(totalMin % 60).padStart(2, "0")} ${t("detail.total")}` : "0min" })
1246
+ ] }),
1247
+ /* @__PURE__ */ jsxs("div", { className: s.timer, children: [
1248
+ /* @__PURE__ */ jsxs("span", { className: `${s.timerDisplay} ${timerRunning ? s.timerActive : ""}`, children: [
1249
+ String(Math.floor(timerSeconds / 60)).padStart(2, "0"),
1250
+ ":",
1251
+ String(timerSeconds % 60).padStart(2, "0")
1252
+ ] }),
1253
+ !timerRunning ? /* @__PURE__ */ jsx("button", { className: s.timerBtn, onClick: () => setTimerRunning(true), "aria-label": "D\xE9marrer le timer", children: timerSeconds > 0 ? "\u25B6" : "\u25B6 Go" }) : /* @__PURE__ */ jsx("button", { className: s.timerBtn, onClick: () => setTimerRunning(false), "aria-label": "Mettre en pause le timer", children: "\u23F8 Pause" }),
1254
+ timerSeconds >= 60 && !timerRunning && /* @__PURE__ */ jsxs("button", { className: s.timerBtn, onClick: () => {
1255
+ handleTimerSave();
1256
+ localStorage.removeItem(`timer-sec-${ticketId}`);
1257
+ localStorage.removeItem(`timer-run-${ticketId}`);
1258
+ }, "aria-label": "Sauvegarder le temps", children: [
1259
+ "\u{1F4BE} ",
1260
+ Math.round(timerSeconds / 60),
1261
+ "m"
1262
+ ] })
1263
+ ] }),
1264
+ /* @__PURE__ */ jsx("div", { className: s.timerBar, "aria-hidden": true, children: /* @__PURE__ */ jsx("div", { className: s.timerBarFill, style: { width: `${Math.min(100, (totalMin + Math.floor(timerSeconds / 60)) / 120 * 100)}%` } }) }),
1265
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 6, marginTop: 8, alignItems: "center" }, children: [
1266
+ /* @__PURE__ */ jsx("input", { type: "number", min: "1", placeholder: "min", style: { width: 60, padding: "4px 8px", borderRadius: 6, border: "1px solid var(--theme-elevation-200)", fontSize: 12, color: "var(--theme-text)", background: "var(--theme-elevation-0)" }, id: "manual-time-input" }),
1267
+ /* @__PURE__ */ jsx("button", { className: s.timerBtn, onClick: async () => {
1268
+ const input = document.getElementById("manual-time-input");
1269
+ const mins = Number(input?.value);
1270
+ if (!mins || mins < 1 || !ticketId) return;
1271
+ try {
1272
+ await fetch("/api/time-entries", { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ ticket: Number(ticketId), duration: mins, date: (/* @__PURE__ */ new Date()).toISOString(), description: "Saisie manuelle" }) });
1273
+ if (input) input.value = "";
1274
+ fetchAll();
1275
+ } catch {
1276
+ }
1277
+ }, style: { fontSize: 11 }, children: "+ Ajouter" })
1278
+ ] }),
1279
+ /* @__PURE__ */ jsxs("div", { style: { marginTop: 8, fontSize: 11, color: "var(--theme-elevation-500)" }, children: [
1280
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", padding: "2px 0", alignItems: "center" }, children: [
1281
+ /* @__PURE__ */ jsx("span", { children: "Facturable" }),
1282
+ /* @__PURE__ */ jsx(
1283
+ "button",
1284
+ {
1285
+ onClick: async () => {
1286
+ const newVal = ticket.billable === false ? true : false;
1287
+ try {
1288
+ await fetch(`/api/tickets/${ticketId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ billable: newVal }) });
1289
+ fetchAll();
1290
+ } catch {
1291
+ }
1292
+ },
1293
+ style: { fontWeight: 600, color: ticket.billable !== false ? "#16a34a" : "#dc2626", background: "none", border: "none", cursor: "pointer", fontSize: 11, textDecoration: "underline" },
1294
+ children: ticket.billable !== false ? "Oui" : "Non"
1067
1295
  }
1068
- },
1069
- onBlur: handleAddTag,
1070
- placeholder: "Tag...",
1071
- autoFocus: true
1072
- }
1073
- ) : /* @__PURE__ */ jsx("button", { className: s.tagAddBtn, onClick: () => setAddingTag(true), "aria-label": "Ajouter un tag", children: "+ Tag" })
1074
- ] })
1075
- ] }),
1076
- /* @__PURE__ */ jsxs("div", { className: s.sideSection, children: [
1077
- /* @__PURE__ */ jsx("div", { className: s.sideSectionTitle, children: "Facturation" }),
1078
- /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 8 }, children: [
1079
- /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between" }, children: [
1080
- /* @__PURE__ */ jsx("span", { style: { fontSize: 12 }, children: "Type" }),
1081
- /* @__PURE__ */ jsxs(
1082
- "select",
1296
+ )
1297
+ ] }),
1298
+ totalMin > 0 && /* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", padding: "2px 0" }, children: [
1299
+ /* @__PURE__ */ jsx("span", { children: "Montant estim\xE9" }),
1300
+ /* @__PURE__ */ jsxs("span", { style: { fontWeight: 700, color: "var(--theme-text)" }, children: [
1301
+ (totalMin / 60 * 60).toFixed(0),
1302
+ "\u20AC"
1303
+ ] })
1304
+ ] })
1305
+ ] }),
1306
+ timeEntries.length > 0 && /* @__PURE__ */ jsx("div", { style: { marginTop: 8, fontSize: 11 }, children: timeEntries.slice(0, 6).map((e) => /* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", padding: "3px 0", color: "var(--theme-elevation-500)" }, children: [
1307
+ /* @__PURE__ */ jsx("span", { children: new Date(e.date).toLocaleDateString("fr-FR", { day: "numeric", month: "short" }) }),
1308
+ /* @__PURE__ */ jsxs("span", { title: e.description, style: { fontWeight: 600, cursor: e.description ? "help" : "default" }, children: [
1309
+ e.duration,
1310
+ "min"
1311
+ ] })
1312
+ ] }, e.id)) })
1313
+ ] }),
1314
+ /* @__PURE__ */ jsxs("div", { className: s.sideSection, children: [
1315
+ /* @__PURE__ */ jsx("div", { className: s.sideSectionTitle, children: t("detail.tags") }),
1316
+ /* @__PURE__ */ jsxs("div", { className: s.tagsWrap, children: [
1317
+ tags.map((tag) => /* @__PURE__ */ jsxs("span", { className: s.tagChip, children: [
1318
+ tag,
1319
+ /* @__PURE__ */ jsx("button", { className: s.tagRemove, "aria-label": `Retirer le tag ${tag}`, onClick: () => handleRemoveTag(tag), children: "\xD7" })
1320
+ ] }, tag)),
1321
+ addingTag ? /* @__PURE__ */ jsx(
1322
+ "input",
1083
1323
  {
1084
- value: ticket?.billingType || "hourly",
1085
- onChange: async (e) => {
1086
- try {
1087
- await fetch(`/api/tickets/${ticketId}`, {
1088
- method: "PATCH",
1089
- credentials: "include",
1090
- headers: { "Content-Type": "application/json" },
1091
- body: JSON.stringify({ billingType: e.target.value })
1092
- });
1093
- fetchAll();
1094
- } catch {
1324
+ className: s.tagInput,
1325
+ value: newTagValue,
1326
+ onChange: (e) => setNewTagValue(e.target.value),
1327
+ onKeyDown: (e) => {
1328
+ if (e.key === "Enter") handleAddTag();
1329
+ if (e.key === "Escape") {
1330
+ setAddingTag(false);
1331
+ setNewTagValue("");
1095
1332
  }
1096
1333
  },
1097
- style: { fontSize: 12, padding: "4px 8px", borderRadius: 6, border: "1px solid #e5e7eb", background: "#fff" },
1098
- children: [
1099
- /* @__PURE__ */ jsx("option", { value: "hourly", children: "Au temps" }),
1100
- /* @__PURE__ */ jsx("option", { value: "flat", children: "Forfait" })
1101
- ]
1334
+ onBlur: handleAddTag,
1335
+ placeholder: "Tag...",
1336
+ autoFocus: true
1102
1337
  }
1103
- )
1104
- ] }),
1105
- ticket?.billingType === "flat" && /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between" }, children: [
1106
- /* @__PURE__ */ jsx("span", { style: { fontSize: 12 }, children: "Montant forfait" }),
1107
- /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 4 }, children: [
1108
- /* @__PURE__ */ jsx(
1109
- "input",
1338
+ ) : /* @__PURE__ */ jsx("button", { className: s.tagAddBtn, onClick: () => setAddingTag(true), "aria-label": "Ajouter un tag", children: "+ Tag" })
1339
+ ] })
1340
+ ] }),
1341
+ /* @__PURE__ */ jsxs("div", { className: s.sideSection, children: [
1342
+ /* @__PURE__ */ jsx("div", { className: s.sideSectionTitle, children: "Facturation" }),
1343
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 8 }, children: [
1344
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between" }, children: [
1345
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 12 }, children: "Type" }),
1346
+ /* @__PURE__ */ jsxs(
1347
+ "select",
1110
1348
  {
1111
- type: "number",
1112
- defaultValue: ticket?.flatRateAmount ?? "",
1113
- placeholder: "0",
1114
- onBlur: async (e) => {
1115
- const val = e.target.value ? Number(e.target.value) : null;
1349
+ value: ticket?.billingType || "hourly",
1350
+ onChange: async (e) => {
1116
1351
  try {
1117
1352
  await fetch(`/api/tickets/${ticketId}`, {
1118
1353
  method: "PATCH",
1119
1354
  credentials: "include",
1120
1355
  headers: { "Content-Type": "application/json" },
1121
- body: JSON.stringify({ flatRateAmount: val })
1356
+ body: JSON.stringify({ billingType: e.target.value })
1122
1357
  });
1123
1358
  fetchAll();
1124
1359
  } catch {
1125
1360
  }
1126
1361
  },
1127
- style: { fontSize: 12, padding: "4px 8px", borderRadius: 6, border: "1px solid #e5e7eb", width: 80, textAlign: "right" }
1362
+ style: { fontSize: 12, padding: "4px 8px", borderRadius: 6, border: "1px solid #e5e7eb", background: "#fff" },
1363
+ children: [
1364
+ /* @__PURE__ */ jsx("option", { value: "hourly", children: "Au temps" }),
1365
+ /* @__PURE__ */ jsx("option", { value: "flat", children: "Forfait" })
1366
+ ]
1128
1367
  }
1129
- ),
1130
- /* @__PURE__ */ jsx("span", { style: { fontSize: 12, color: "#9ca3af" }, children: "\u20AC" })
1131
- ] })
1132
- ] }),
1133
- /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between" }, children: [
1134
- /* @__PURE__ */ jsx("span", { style: { fontSize: 12 }, children: "Montant factur\xE9" }),
1135
- /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 4 }, children: [
1136
- /* @__PURE__ */ jsx(
1137
- "input",
1368
+ )
1369
+ ] }),
1370
+ ticket?.billingType === "flat" && /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between" }, children: [
1371
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 12 }, children: "Montant forfait" }),
1372
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 4 }, children: [
1373
+ /* @__PURE__ */ jsx(
1374
+ "input",
1375
+ {
1376
+ type: "number",
1377
+ defaultValue: ticket?.flatRateAmount ?? "",
1378
+ placeholder: "0",
1379
+ onBlur: async (e) => {
1380
+ const val = e.target.value ? Number(e.target.value) : null;
1381
+ try {
1382
+ await fetch(`/api/tickets/${ticketId}`, {
1383
+ method: "PATCH",
1384
+ credentials: "include",
1385
+ headers: { "Content-Type": "application/json" },
1386
+ body: JSON.stringify({ flatRateAmount: val })
1387
+ });
1388
+ fetchAll();
1389
+ } catch {
1390
+ }
1391
+ },
1392
+ style: { fontSize: 12, padding: "4px 8px", borderRadius: 6, border: "1px solid #e5e7eb", width: 80, textAlign: "right" }
1393
+ }
1394
+ ),
1395
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 12, color: "#9ca3af" }, children: "\u20AC" })
1396
+ ] })
1397
+ ] }),
1398
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between" }, children: [
1399
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 12 }, children: "Montant factur\xE9" }),
1400
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 4 }, children: [
1401
+ /* @__PURE__ */ jsx(
1402
+ "input",
1403
+ {
1404
+ type: "number",
1405
+ defaultValue: ticket?.billedAmount ?? "",
1406
+ placeholder: "0",
1407
+ onBlur: async (e) => {
1408
+ const val = e.target.value ? Number(e.target.value) : null;
1409
+ try {
1410
+ await fetch(`/api/tickets/${ticketId}`, {
1411
+ method: "PATCH",
1412
+ credentials: "include",
1413
+ headers: { "Content-Type": "application/json" },
1414
+ body: JSON.stringify({ billedAmount: val })
1415
+ });
1416
+ fetchAll();
1417
+ } catch {
1418
+ }
1419
+ },
1420
+ style: { fontSize: 12, padding: "4px 8px", borderRadius: 6, border: "1px solid #e5e7eb", width: 80, textAlign: "right" }
1421
+ }
1422
+ ),
1423
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 12, color: "#9ca3af" }, children: "\u20AC" })
1424
+ ] })
1425
+ ] }),
1426
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between" }, children: [
1427
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 12 }, children: "Paiement" }),
1428
+ /* @__PURE__ */ jsxs(
1429
+ "select",
1138
1430
  {
1139
- type: "number",
1140
- defaultValue: ticket?.billedAmount ?? "",
1141
- placeholder: "0",
1142
- onBlur: async (e) => {
1143
- const val = e.target.value ? Number(e.target.value) : null;
1431
+ value: ticket?.paymentStatus || "unpaid",
1432
+ onChange: async (e) => {
1144
1433
  try {
1145
1434
  await fetch(`/api/tickets/${ticketId}`, {
1146
1435
  method: "PATCH",
1147
1436
  credentials: "include",
1148
1437
  headers: { "Content-Type": "application/json" },
1149
- body: JSON.stringify({ billedAmount: val })
1438
+ body: JSON.stringify({ paymentStatus: e.target.value })
1150
1439
  });
1151
1440
  fetchAll();
1152
1441
  } catch {
1153
1442
  }
1154
1443
  },
1155
- style: { fontSize: 12, padding: "4px 8px", borderRadius: 6, border: "1px solid #e5e7eb", width: 80, textAlign: "right" }
1444
+ style: { fontSize: 12, padding: "4px 8px", borderRadius: 6, border: "1px solid #e5e7eb", background: "#fff" },
1445
+ children: [
1446
+ /* @__PURE__ */ jsx("option", { value: "unpaid", children: "Non pay\xE9" }),
1447
+ /* @__PURE__ */ jsx("option", { value: "partial", children: "Partiel" }),
1448
+ /* @__PURE__ */ jsx("option", { value: "paid", children: "Pay\xE9" })
1449
+ ]
1156
1450
  }
1157
- ),
1158
- /* @__PURE__ */ jsx("span", { style: { fontSize: 12, color: "#9ca3af" }, children: "\u20AC" })
1451
+ )
1159
1452
  ] })
1160
- ] }),
1161
- /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between" }, children: [
1162
- /* @__PURE__ */ jsx("span", { style: { fontSize: 12 }, children: "Paiement" }),
1163
- /* @__PURE__ */ jsxs(
1164
- "select",
1165
- {
1166
- value: ticket?.paymentStatus || "unpaid",
1167
- onChange: async (e) => {
1168
- try {
1169
- await fetch(`/api/tickets/${ticketId}`, {
1170
- method: "PATCH",
1171
- credentials: "include",
1172
- headers: { "Content-Type": "application/json" },
1173
- body: JSON.stringify({ paymentStatus: e.target.value })
1174
- });
1175
- fetchAll();
1176
- } catch {
1177
- }
1178
- },
1179
- style: { fontSize: 12, padding: "4px 8px", borderRadius: 6, border: "1px solid #e5e7eb", background: "#fff" },
1180
- children: [
1181
- /* @__PURE__ */ jsx("option", { value: "unpaid", children: "Non pay\xE9" }),
1182
- /* @__PURE__ */ jsx("option", { value: "partial", children: "Partiel" }),
1183
- /* @__PURE__ */ jsx("option", { value: "paid", children: "Pay\xE9" })
1184
- ]
1185
- }
1186
- )
1187
1453
  ] })
1188
- ] })
1189
- ] }),
1190
- features.timeTracking && /* @__PURE__ */ jsxs("div", { className: s.sideSection, children: [
1191
- /* @__PURE__ */ jsxs("div", { className: s.sideSectionTitle, children: [
1192
- t("detail.time"),
1193
- " ",
1194
- /* @__PURE__ */ jsx("span", { style: { fontWeight: 700, fontSize: 13, color: "#d97706" }, children: totalMin > 0 ? `${Math.floor(totalMin / 60)}h${String(totalMin % 60).padStart(2, "0")} ${t("detail.total")}` : "0min" })
1195
1454
  ] }),
1196
- /* @__PURE__ */ jsxs("div", { className: s.timer, children: [
1197
- /* @__PURE__ */ jsxs("span", { className: `${s.timerDisplay} ${timerRunning ? s.timerActive : ""}`, children: [
1198
- String(Math.floor(timerSeconds / 60)).padStart(2, "0"),
1199
- ":",
1200
- String(timerSeconds % 60).padStart(2, "0")
1455
+ summaryLoading && /* @__PURE__ */ jsx("div", { className: s.sideSection, children: /* @__PURE__ */ jsx("div", { style: { fontSize: 11, color: "var(--theme-elevation-400)", textAlign: "center", padding: 8 }, children: "\u2728 Chargement de la synth\xE8se\u2026" }) })
1456
+ ] }),
1457
+ sidebarTab === "client" && /* @__PURE__ */ jsxs("div", { role: "tabpanel", id: "sidebar-panel-client", "aria-labelledby": "sidebar-tab-client", children: [
1458
+ client && /* @__PURE__ */ jsxs("div", { className: s.sideSection, children: [
1459
+ /* @__PURE__ */ jsxs("div", { className: s.clientCard, children: [
1460
+ /* @__PURE__ */ jsx("div", { className: s.clientAvatar, children: initials }),
1461
+ /* @__PURE__ */ jsxs("div", { className: s.clientInfo, children: [
1462
+ /* @__PURE__ */ jsxs("div", { className: s.clientName, children: [
1463
+ client.firstName,
1464
+ " ",
1465
+ client.lastName
1466
+ ] }),
1467
+ /* @__PURE__ */ jsx("div", { className: s.clientCompany, children: client.company }),
1468
+ /* @__PURE__ */ jsx("a", { href: `mailto:${client.email}`, className: s.clientEmail, children: client.email }),
1469
+ client.phone && /* @__PURE__ */ jsx("a", { href: `tel:${client.phone}`, className: s.clientEmail, children: client.phone })
1470
+ ] })
1201
1471
  ] }),
1202
- !timerRunning ? /* @__PURE__ */ jsx("button", { className: s.timerBtn, onClick: () => setTimerRunning(true), style: { color: "#dc2626", borderColor: "#dc2626" }, "aria-label": "D\xE9marrer le timer", children: timerSeconds > 0 ? "\u25B6" : "\u25B6 Go" }) : /* @__PURE__ */ jsx("button", { className: s.timerBtn, onClick: () => setTimerRunning(false), "aria-label": "Mettre en pause le timer", children: "\u23F8" }),
1203
- timerSeconds >= 60 && !timerRunning && /* @__PURE__ */ jsxs("button", { className: s.timerBtn, onClick: () => {
1204
- handleTimerSave();
1205
- localStorage.removeItem(`timer-sec-${ticketId}`);
1206
- localStorage.removeItem(`timer-run-${ticketId}`);
1207
- }, style: { color: "#16a34a", borderColor: "#16a34a" }, "aria-label": "Sauvegarder le temps", children: [
1208
- "\u{1F4BE} ",
1209
- Math.round(timerSeconds / 60),
1210
- "m"
1472
+ /* @__PURE__ */ jsxs("div", { className: s.clientActions, children: [
1473
+ /* @__PURE__ */ jsx(Link, { href: `/admin/collections/support-clients/${client.id}`, className: s.smallBtn, children: t("client.clientSheet") }),
1474
+ /* @__PURE__ */ jsx("button", { className: s.smallBtn, onClick: () => window.open(`/api/admin/impersonate?clientId=${client.id}`, "_blank"), children: t("client.clientPortal") })
1211
1475
  ] })
1212
1476
  ] }),
1213
- /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 6, marginTop: 8, alignItems: "center" }, children: [
1214
- /* @__PURE__ */ jsx("input", { type: "number", min: "1", placeholder: "min", style: { width: 60, padding: "4px 8px", borderRadius: 6, border: "1px solid var(--theme-elevation-200)", fontSize: 12, color: "var(--theme-text)", background: "var(--theme-elevation-0)" }, id: "manual-time-input" }),
1215
- /* @__PURE__ */ jsx("button", { className: s.timerBtn, onClick: async () => {
1216
- const input = document.getElementById("manual-time-input");
1217
- const mins = Number(input?.value);
1218
- if (!mins || mins < 1 || !ticketId) return;
1219
- try {
1220
- await fetch("/api/time-entries", { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ ticket: Number(ticketId), duration: mins, date: (/* @__PURE__ */ new Date()).toISOString(), description: "Saisie manuelle" }) });
1221
- if (input) input.value = "";
1222
- fetchAll();
1223
- } catch {
1224
- }
1225
- }, style: { fontSize: 11 }, children: "+ Ajouter" })
1226
- ] }),
1227
- /* @__PURE__ */ jsxs("div", { style: { marginTop: 8, fontSize: 11, color: "var(--theme-elevation-500)" }, children: [
1228
- /* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", padding: "2px 0", alignItems: "center" }, children: [
1229
- /* @__PURE__ */ jsx("span", { children: "Facturable" }),
1230
- /* @__PURE__ */ jsx(
1231
- "button",
1232
- {
1233
- onClick: async () => {
1234
- const newVal = ticket.billable === false ? true : false;
1235
- try {
1236
- await fetch(`/api/tickets/${ticketId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ billable: newVal }) });
1237
- fetchAll();
1238
- } catch {
1239
- }
1240
- },
1241
- style: { fontWeight: 600, color: ticket.billable !== false ? "#16a34a" : "#dc2626", background: "none", border: "none", cursor: "pointer", fontSize: 11, textDecoration: "underline" },
1242
- children: ticket.billable !== false ? "Oui" : "Non"
1243
- }
1244
- )
1245
- ] }),
1246
- totalMin > 0 && /* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", padding: "2px 0" }, children: [
1247
- /* @__PURE__ */ jsx("span", { children: "Montant estim\xE9" }),
1248
- /* @__PURE__ */ jsxs("span", { style: { fontWeight: 700, color: "var(--theme-text)" }, children: [
1249
- (totalMin / 60 * 60).toFixed(0),
1250
- "\u20AC"
1477
+ /* @__PURE__ */ jsxs("div", { className: s.sideSection, children: [
1478
+ /* @__PURE__ */ jsx("div", { className: s.sideSectionTitle, children: t("detail.sidebar.previousTickets") }),
1479
+ previousTickets.length === 0 ? /* @__PURE__ */ jsx("div", { className: s.previousTicketsEmpty, children: "\u2014" }) : /* @__PURE__ */ jsx("ul", { className: s.previousTicketsList, children: previousTickets.map((pt) => /* @__PURE__ */ jsx("li", { className: s.previousTicketsItem, children: /* @__PURE__ */ jsxs(Link, { href: `/admin/collections/tickets/${pt.id}`, className: s.previousTicketsLink, children: [
1480
+ /* @__PURE__ */ jsx("span", { className: s.previousTicketsSubject, children: pt.subject || `#${pt.id}` }),
1481
+ /* @__PURE__ */ jsxs("span", { className: s.previousTicketsMeta, children: [
1482
+ pt.status ? /* @__PURE__ */ jsx("span", { className: s.previousTicketsStatus, children: pt.status }) : null,
1483
+ /* @__PURE__ */ jsx("span", { className: s.previousTicketsDate, children: new Date(pt.createdAt).toLocaleDateString("fr-FR", { day: "numeric", month: "short", year: "numeric" }) })
1251
1484
  ] })
1252
- ] })
1253
- ] }),
1254
- timeEntries.length > 0 && /* @__PURE__ */ jsx("div", { style: { marginTop: 8, fontSize: 11 }, children: timeEntries.slice(0, 6).map((e) => /* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", padding: "3px 0", color: "var(--theme-elevation-500)" }, children: [
1255
- /* @__PURE__ */ jsx("span", { children: new Date(e.date).toLocaleDateString("fr-FR", { day: "numeric", month: "short" }) }),
1256
- /* @__PURE__ */ jsxs("span", { title: e.description, style: { fontWeight: 600, cursor: e.description ? "help" : "default" }, children: [
1257
- e.duration,
1258
- "min"
1259
- ] })
1260
- ] }, e.id)) })
1485
+ ] }) }, pt.id)) })
1486
+ ] })
1261
1487
  ] }),
1262
- features.activityLog && /* @__PURE__ */ jsxs("div", { className: s.sideSection, children: [
1263
- /* @__PURE__ */ jsx("div", { className: s.sideSectionTitle, children: /* @__PURE__ */ jsxs("button", { className: s.collapseBtn, onClick: () => setShowActivity(!showActivity), "aria-label": showActivity ? "Masquer le journal" : "Afficher le journal", children: [
1264
- "Activit\xE9 ",
1265
- showActivity ? "\u25BE" : "\u25B8"
1266
- ] }) }),
1267
- showActivity && activityLog.slice(0, 8).map((a) => /* @__PURE__ */ jsxs("div", { className: s.activityItem, children: [
1488
+ sidebarTab === "activity" && /* @__PURE__ */ jsx("div", { role: "tabpanel", id: "sidebar-panel-activity", "aria-labelledby": "sidebar-tab-activity", children: features.activityLog ? /* @__PURE__ */ jsxs("div", { className: s.sideSection, children: [
1489
+ /* @__PURE__ */ jsx("div", { className: s.sideSectionTitle, children: t("detail.activity") }),
1490
+ activityLog.length === 0 ? /* @__PURE__ */ jsx("div", { className: s.previousTicketsEmpty, children: "\u2014" }) : activityLog.slice(0, 30).map((a) => /* @__PURE__ */ jsxs("div", { className: s.activityItem, children: [
1268
1491
  /* @__PURE__ */ jsx("div", { className: s.activityDot, style: { backgroundColor: a.actorType === "admin" ? "#2563eb" : a.actorType === "system" ? "#6b7280" : "#16a34a" } }),
1269
1492
  /* @__PURE__ */ jsxs("div", { className: s.activityContent, children: [
1270
- /* @__PURE__ */ jsx("div", { className: s.activityText, children: (a.detail || a.action).slice(0, 60) }),
1493
+ /* @__PURE__ */ jsx("div", { className: s.activityText, children: (a.detail || a.action).slice(0, 120) }),
1271
1494
  /* @__PURE__ */ jsx("div", { className: s.activityTime, children: timeAgo(a.createdAt) })
1272
1495
  ] })
1273
1496
  ] }, a.id))
1274
- ] }),
1275
- summaryLoading && /* @__PURE__ */ jsx("div", { className: s.sideSection, children: /* @__PURE__ */ jsx("div", { style: { fontSize: 11, color: "var(--theme-elevation-400)", textAlign: "center", padding: 8 }, children: "\u2728 Chargement de la synth\xE8se\u2026" }) })
1497
+ ] }) : /* @__PURE__ */ jsx("div", { className: s.sideSection, children: /* @__PURE__ */ jsx("div", { className: s.previousTicketsEmpty, children: "\u2014" }) }) })
1276
1498
  ] })
1277
1499
  ] }),
1278
1500
  undoToast && /* @__PURE__ */ jsxs("div", { className: s.undoToast, role: "alert", children: [