@consilioweb/payload-support 2.0.0 → 3.0.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 (222) hide show
  1. package/README.md +656 -355
  2. package/dist/components/RichTextEditor/index.d.ts +19 -0
  3. package/dist/components/TicketConversation/RewriteDropdown.d.ts +6 -0
  4. package/dist/components/TicketConversation/SkeletonText.d.ts +4 -0
  5. package/dist/components/TicketConversation/components/AISummaryPanel.d.ts +14 -0
  6. package/dist/components/TicketConversation/components/ActionPanels.d.ts +47 -0
  7. package/dist/components/TicketConversation/components/ActivityLog.d.ts +5 -0
  8. package/dist/components/TicketConversation/components/ClientBar.d.ts +5 -0
  9. package/dist/components/TicketConversation/components/ClientHistory.d.ts +24 -0
  10. package/dist/components/TicketConversation/components/CodeBlock.d.ts +26 -0
  11. package/dist/components/TicketConversation/components/CodeBlockInserter.d.ts +12 -0
  12. package/dist/components/TicketConversation/components/QuickActions.d.ts +32 -0
  13. package/dist/components/TicketConversation/components/TicketHeader.d.ts +19 -0
  14. package/dist/components/TicketConversation/components/TimeTrackingPanel.d.ts +24 -0
  15. package/dist/components/TicketConversation/config.d.ts +10 -0
  16. package/dist/components/TicketConversation/config.js +1 -40
  17. package/dist/components/TicketConversation/constants.d.ts +56 -0
  18. package/dist/components/TicketConversation/context.d.ts +53 -0
  19. package/dist/components/TicketConversation/hooks/useAI.d.ts +21 -0
  20. package/dist/components/TicketConversation/hooks/useDocumentIdFromUrl.d.ts +3 -0
  21. package/dist/components/TicketConversation/hooks/useFeatures.d.ts +23 -0
  22. package/dist/components/TicketConversation/hooks/useFeatures.js +61 -0
  23. package/dist/components/TicketConversation/hooks/useMessageActions.d.ts +20 -0
  24. package/dist/components/TicketConversation/hooks/useReply.d.ts +27 -0
  25. package/dist/components/TicketConversation/hooks/useTicketActions.d.ts +58 -0
  26. package/dist/components/TicketConversation/hooks/useTimeTracking.d.ts +18 -0
  27. package/dist/components/TicketConversation/hooks/useTranslation.d.ts +33 -0
  28. package/dist/components/TicketConversation/hooks/useTranslation.js +22 -3
  29. package/dist/components/TicketConversation/index.d.ts +4 -0
  30. package/dist/components/TicketConversation/index.js +2 -2
  31. package/dist/components/TicketConversation/types.d.ts +56 -0
  32. package/dist/components/TicketConversation/utils.d.ts +2 -0
  33. package/dist/index.cjs +392 -320
  34. package/dist/index.d.cts +100 -3
  35. package/dist/index.d.ts +100 -3
  36. package/dist/index.js +388 -321
  37. package/dist/utils/features.d.ts +123 -0
  38. package/dist/utils/features.js +147 -0
  39. package/dist/views/BillingView/client.d.ts +2 -0
  40. package/dist/views/BillingView/index.d.ts +4 -0
  41. package/dist/views/ChatView/client.d.ts +2 -0
  42. package/dist/views/ChatView/client.js +9 -9
  43. package/dist/views/ChatView/index.d.ts +4 -0
  44. package/dist/views/CrmView/client.d.ts +2 -0
  45. package/dist/views/CrmView/client.js +6 -9
  46. package/dist/views/CrmView/index.d.ts +4 -0
  47. package/dist/views/EmailTrackingView/client.d.ts +2 -0
  48. package/dist/views/EmailTrackingView/index.d.ts +4 -0
  49. package/dist/views/ImportConversationView/client.d.ts +2 -0
  50. package/dist/views/ImportConversationView/index.d.ts +4 -0
  51. package/dist/views/LogsView/client.d.ts +2 -0
  52. package/dist/views/LogsView/index.d.ts +4 -0
  53. package/dist/views/NewTicketView/client.d.ts +2 -0
  54. package/dist/views/NewTicketView/index.d.ts +4 -0
  55. package/dist/views/PendingEmailsView/client.d.ts +2 -0
  56. package/dist/views/PendingEmailsView/index.d.ts +4 -0
  57. package/dist/views/SupportDashboardView/client.d.ts +2 -0
  58. package/dist/views/SupportDashboardView/client.js +1 -1
  59. package/dist/views/SupportDashboardView/index.d.ts +4 -0
  60. package/dist/views/TicketDetailView/NextActionItem.d.ts +6 -0
  61. package/dist/views/TicketDetailView/RewriteDropdown.d.ts +7 -0
  62. package/dist/views/TicketDetailView/client.d.ts +2 -0
  63. package/dist/views/TicketDetailView/client.js +2 -2
  64. package/dist/views/TicketDetailView/constants.d.ts +13 -0
  65. package/dist/views/TicketDetailView/helpers.d.ts +2 -0
  66. package/dist/views/TicketDetailView/index.d.ts +4 -0
  67. package/dist/views/TicketDetailView/types.d.ts +40 -0
  68. package/dist/views/TicketInboxView/client.d.ts +2 -0
  69. package/dist/views/TicketInboxView/client.js +5 -5
  70. package/dist/views/TicketInboxView/index.d.ts +4 -0
  71. package/dist/views/TicketingSettingsView/client.d.ts +2 -0
  72. package/dist/views/TicketingSettingsView/client.js +6 -5
  73. package/dist/views/TicketingSettingsView/index.d.ts +4 -0
  74. package/dist/views/TimeDashboardView/client.d.ts +2 -0
  75. package/dist/views/TimeDashboardView/index.d.ts +4 -0
  76. package/dist/views/shared/AdminViewHeader.d.ts +13 -0
  77. package/dist/views/shared/ErrorBoundary.d.ts +17 -0
  78. package/dist/views/shared/Skeleton.d.ts +21 -0
  79. package/dist/views/shared/StatusPill.d.ts +8 -0
  80. package/dist/views/shared/adminTokens.d.ts +19 -0
  81. package/dist/views/shared/config.d.ts +8 -0
  82. package/dist/views/shared/config.js +1 -40
  83. package/dist/views/shared/dateLocale.d.ts +5 -0
  84. package/dist/views/shared/index.d.ts +12 -0
  85. package/dist/views/shared/index.js +2 -1
  86. package/dist/views/shared/locales/en.json +708 -0
  87. package/dist/views/shared/locales/fr.json +708 -0
  88. package/dist/views/shared/sla.d.ts +14 -0
  89. package/dist/views.d.ts +13 -13
  90. package/package.json +26 -41
  91. package/src/collections/SupportClients.ts +29 -0
  92. package/src/collections/TicketCollaborators.ts +6 -1
  93. package/src/collections/TicketMessages.ts +46 -24
  94. package/src/collections/Tickets.ts +117 -79
  95. package/src/collections/TimeEntries.ts +57 -32
  96. package/src/components/TicketConversation/config.ts +16 -80
  97. package/src/components/TicketConversation/hooks/useFeatures.ts +109 -0
  98. package/src/components/TicketConversation/hooks/useTranslation.ts +38 -4
  99. package/src/components/TicketConversation/index.tsx +2 -2
  100. package/src/endpoints/ai.ts +2 -1
  101. package/src/endpoints/client-intelligence.ts +2 -1
  102. package/src/endpoints/process-scheduled.ts +13 -8
  103. package/src/endpoints/round-robin-config.ts +30 -20
  104. package/src/endpoints/settings.ts +28 -77
  105. package/src/index.ts +6 -2
  106. package/src/portal/LiveChat.tsx +33 -22
  107. package/src/portal/auth/tickets/detail/TicketPolling.tsx +5 -1
  108. package/src/types/lucide-react.d.ts +10 -2
  109. package/src/utils/aiAgent.ts +2 -1
  110. package/src/utils/aiProvider.ts +21 -0
  111. package/src/utils/features.ts +297 -0
  112. package/src/utils/fireWebhooks.ts +6 -0
  113. package/src/utils/generateTicketSynthesis.ts +2 -1
  114. package/src/utils/index.ts +4 -2
  115. package/src/utils/readSettings.ts +82 -9
  116. package/src/utils/webhookDispatcher.ts +19 -7
  117. package/src/views/ChatView/client.tsx +13 -9
  118. package/src/views/CrmView/client.tsx +10 -11
  119. package/src/views/SupportDashboardView/client.tsx +1 -1
  120. package/src/views/TicketDetailView/client.tsx +2 -2
  121. package/src/views/TicketInboxView/client.tsx +10 -5
  122. package/src/views/TicketingSettingsView/client.tsx +9 -7
  123. package/src/views/shared/config.ts +14 -80
  124. package/src/views/shared/index.ts +3 -1
  125. package/src/views/shared/locales/en.json +2 -2
  126. package/src/views/shared/locales/fr.json +2 -2
  127. package/dist/components/RichTextEditor/index.cjs +0 -279
  128. package/dist/components/TicketConversation/RewriteDropdown.cjs +0 -98
  129. package/dist/components/TicketConversation/SkeletonText.cjs +0 -21
  130. package/dist/components/TicketConversation/components/AISummaryPanel.cjs +0 -85
  131. package/dist/components/TicketConversation/components/ActionPanels.cjs +0 -193
  132. package/dist/components/TicketConversation/components/ActivityLog.cjs +0 -24
  133. package/dist/components/TicketConversation/components/ClientBar.cjs +0 -43
  134. package/dist/components/TicketConversation/components/ClientHistory.cjs +0 -138
  135. package/dist/components/TicketConversation/components/CodeBlock.cjs +0 -171
  136. package/dist/components/TicketConversation/components/CodeBlockInserter.cjs +0 -156
  137. package/dist/components/TicketConversation/components/QuickActions.cjs +0 -87
  138. package/dist/components/TicketConversation/components/TicketHeader.cjs +0 -93
  139. package/dist/components/TicketConversation/components/TimeTrackingPanel.cjs +0 -134
  140. package/dist/components/TicketConversation/config.cjs +0 -44
  141. package/dist/components/TicketConversation/constants.cjs +0 -127
  142. package/dist/components/TicketConversation/context.cjs +0 -13
  143. package/dist/components/TicketConversation/hooks/useAI.cjs +0 -196
  144. package/dist/components/TicketConversation/hooks/useDocumentIdFromUrl.cjs +0 -20
  145. package/dist/components/TicketConversation/hooks/useMessageActions.cjs +0 -136
  146. package/dist/components/TicketConversation/hooks/useReply.cjs +0 -188
  147. package/dist/components/TicketConversation/hooks/useTicketActions.cjs +0 -290
  148. package/dist/components/TicketConversation/hooks/useTimeTracking.cjs +0 -123
  149. package/dist/components/TicketConversation/hooks/useTranslation.cjs +0 -78
  150. package/dist/components/TicketConversation/index.cjs +0 -1114
  151. package/dist/components/TicketConversation/types.cjs +0 -2
  152. package/dist/components/TicketConversation/utils.cjs +0 -27
  153. package/dist/views/BillingView/client.cjs +0 -469
  154. package/dist/views/BillingView/index.cjs +0 -34
  155. package/dist/views/ChatView/client.cjs +0 -301
  156. package/dist/views/ChatView/index.cjs +0 -34
  157. package/dist/views/CrmView/client.cjs +0 -458
  158. package/dist/views/CrmView/index.cjs +0 -34
  159. package/dist/views/EmailTrackingView/client.cjs +0 -171
  160. package/dist/views/EmailTrackingView/index.cjs +0 -34
  161. package/dist/views/ImportConversationView/client.cjs +0 -238
  162. package/dist/views/ImportConversationView/index.cjs +0 -34
  163. package/dist/views/LogsView/client.cjs +0 -148
  164. package/dist/views/LogsView/index.cjs +0 -32
  165. package/dist/views/NewTicketView/client.cjs +0 -213
  166. package/dist/views/NewTicketView/index.cjs +0 -32
  167. package/dist/views/PendingEmailsView/client.cjs +0 -471
  168. package/dist/views/PendingEmailsView/index.cjs +0 -34
  169. package/dist/views/SupportDashboardView/client.cjs +0 -437
  170. package/dist/views/SupportDashboardView/index.cjs +0 -34
  171. package/dist/views/TicketDetailView/NextActionItem.cjs +0 -40
  172. package/dist/views/TicketDetailView/RewriteDropdown.cjs +0 -80
  173. package/dist/views/TicketDetailView/client.cjs +0 -1412
  174. package/dist/views/TicketDetailView/constants.cjs +0 -20
  175. package/dist/views/TicketDetailView/helpers.cjs +0 -23
  176. package/dist/views/TicketDetailView/index.cjs +0 -34
  177. package/dist/views/TicketDetailView/types.cjs +0 -2
  178. package/dist/views/TicketInboxView/client.cjs +0 -359
  179. package/dist/views/TicketInboxView/index.cjs +0 -32
  180. package/dist/views/TicketingSettingsView/client.cjs +0 -740
  181. package/dist/views/TicketingSettingsView/index.cjs +0 -34
  182. package/dist/views/TimeDashboardView/client.cjs +0 -180
  183. package/dist/views/TimeDashboardView/index.cjs +0 -34
  184. package/dist/views/shared/AdminViewHeader.cjs +0 -68
  185. package/dist/views/shared/ErrorBoundary.cjs +0 -55
  186. package/dist/views/shared/Skeleton.cjs +0 -77
  187. package/dist/views/shared/StatusPill.cjs +0 -47
  188. package/dist/views/shared/adminTokens.cjs +0 -34
  189. package/dist/views/shared/config.cjs +0 -44
  190. package/dist/views/shared/dateLocale.cjs +0 -5
  191. package/dist/views/shared/index.cjs +0 -72
  192. package/dist/views/shared/sla.cjs +0 -37
  193. package/dist/views.cjs +0 -86
  194. package/dist/views.d.cts +0 -13
  195. package/src/__tests__/aiSummaryRendering.test.ts +0 -21
  196. package/src/__tests__/authResponses.test.ts +0 -69
  197. package/src/__tests__/capabilities.test.ts +0 -71
  198. package/src/__tests__/generateTrackingToken.test.ts +0 -67
  199. package/src/__tests__/integration/ai-agent.test.ts +0 -46
  200. package/src/__tests__/integration/auth.test.ts +0 -43
  201. package/src/__tests__/integration/automation-rules.test.ts +0 -68
  202. package/src/__tests__/integration/buildTestPayload.ts +0 -52
  203. package/src/__tests__/integration/channels.test.ts +0 -52
  204. package/src/__tests__/integration/core-behaviors.test.ts +0 -81
  205. package/src/__tests__/integration/dashboard-volume.test.ts +0 -31
  206. package/src/__tests__/integration/digest.test.ts +0 -48
  207. package/src/__tests__/integration/invoice-pdf.test.ts +0 -30
  208. package/src/__tests__/integration/isolation.test.ts +0 -81
  209. package/src/__tests__/integration/mentions-invoice.test.ts +0 -50
  210. package/src/__tests__/integration/nps.test.ts +0 -37
  211. package/src/__tests__/integration/per-team-sla.test.ts +0 -44
  212. package/src/__tests__/integration/push.test.ts +0 -75
  213. package/src/__tests__/integration/sanitization.test.ts +0 -35
  214. package/src/__tests__/integration/sla-pause.test.ts +0 -39
  215. package/src/__tests__/integration/smoke.test.ts +0 -26
  216. package/src/__tests__/integration/snooze.test.ts +0 -68
  217. package/src/__tests__/integration/teams.test.ts +0 -38
  218. package/src/__tests__/rateLimiter.test.ts +0 -83
  219. package/src/__tests__/sanitizeHtml.test.ts +0 -165
  220. package/src/__tests__/sla.test.ts +0 -258
  221. package/src/__tests__/trackOpenEndpoint.test.ts +0 -54
  222. package/src/__tests__/webhookSecurity.test.ts +0 -58
@@ -1,1412 +0,0 @@
1
- 'use strict';
2
-
3
- var jsxRuntime = require('react/jsx-runtime');
4
- var React = require('react');
5
- var navigation = require('next/navigation');
6
- var Link = require('next/link');
7
- var index = require('../../components/RichTextEditor/index');
8
- var CodeBlock = require('../../components/TicketConversation/components/CodeBlock');
9
- var CodeBlockInserter = require('../../components/TicketConversation/components/CodeBlockInserter');
10
- var config = require('../shared/config');
11
- var sla = require('../shared/sla');
12
- var useTranslation = require('../../components/TicketConversation/hooks/useTranslation');
13
- var dateLocale = require('../shared/dateLocale');
14
- var s = require('../../styles/TicketDetail.module.scss');
15
- var constants = require('./constants');
16
- var helpers = require('./helpers');
17
- var RewriteDropdown = require('./RewriteDropdown');
18
- var NextActionItem = require('./NextActionItem');
19
-
20
- function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
21
-
22
- var React__default = /*#__PURE__*/_interopDefault(React);
23
- var Link__default = /*#__PURE__*/_interopDefault(Link);
24
- var s__default = /*#__PURE__*/_interopDefault(s);
25
-
26
- const TicketDetailClient = () => {
27
- const { t } = useTranslation.useTranslation();
28
- const searchParams = navigation.useSearchParams();
29
- const ticketId = searchParams.get("id");
30
- const features = config.getFeatures();
31
- const editorRef = React.useRef(null);
32
- const threadEndRef = React.useRef(null);
33
- const dropdownRef = React.useRef(null);
34
- const fileInputRef = React.useRef(null);
35
- const [ticket, setTicket] = React.useState(null);
36
- const [messages, setMessages] = React.useState([]);
37
- const [client, setClient] = React.useState(null);
38
- const [timeEntries, setTimeEntries] = React.useState([]);
39
- const [activityLog, setActivityLog] = React.useState([]);
40
- const [cannedResponses, setCannedResponses] = React.useState([]);
41
- const [loading, setLoading] = React.useState(true);
42
- const [replyBody, setReplyBody] = React.useState("");
43
- const [replyHtml, setReplyHtml] = React.useState("");
44
- const [isInternal, setIsInternal] = React.useState(false);
45
- const [notifyClient, setNotifyClient] = React.useState(true);
46
- const [sendAsClient, setSendAsClient] = React.useState(false);
47
- const [fromAlias, setFromAlias] = React.useState("");
48
- const [editingMsgId, setEditingMsgId] = React.useState(null);
49
- const [editingBody, setEditingBody] = React.useState("");
50
- const [editingHtml, setEditingHtml] = React.useState("");
51
- const [editSaving, setEditSaving] = React.useState(false);
52
- const editEditorRef = React.useRef(null);
53
- const [sending, setSending] = React.useState(false);
54
- const [showMenu, setShowMenu] = React.useState(false);
55
- const [clientTyping, setClientTyping] = React.useState(false);
56
- const [aiReplying, setAiReplying] = React.useState(false);
57
- const [aiSuggestion, setAiSuggestion] = React.useState(null);
58
- const [aiRewriting, setAiRewriting] = React.useState(false);
59
- const [sentiment, setSentiment] = React.useState(null);
60
- const [statusUpdating, setStatusUpdating] = React.useState(false);
61
- const [sidebarTab, setSidebarTab] = React.useState(() => {
62
- if (typeof window === "undefined") return "overview";
63
- const v = localStorage.getItem("support_sidebar_tab");
64
- return v === "client" || v === "activity" ? v : "overview";
65
- });
66
- React.useEffect(() => {
67
- if (typeof window === "undefined") return;
68
- localStorage.setItem("support_sidebar_tab", sidebarTab);
69
- }, [sidebarTab]);
70
- const [previousTickets, setPreviousTickets] = React.useState([]);
71
- const [clientSummary, setClientSummary] = React.useState(null);
72
- const [summaryLoading, setSummaryLoading] = React.useState(false);
73
- const [timerRunning, setTimerRunning] = React.useState(() => {
74
- if (typeof window === "undefined" || !ticketId) return false;
75
- return localStorage.getItem(`timer-run-${ticketId}`) === "1";
76
- });
77
- const [timerSeconds, setTimerSeconds] = React.useState(() => {
78
- if (typeof window === "undefined" || !ticketId) return 0;
79
- const s2 = Number(localStorage.getItem(`timer-sec-${ticketId}`) || 0);
80
- const t2 = Number(localStorage.getItem(`timer-ts-${ticketId}`) || 0);
81
- const running = localStorage.getItem(`timer-run-${ticketId}`) === "1";
82
- return running && t2 ? s2 + Math.floor((Date.now() - t2) / 1e3) : s2;
83
- });
84
- const timerRef = React.useRef(null);
85
- const [otherViewers, setOtherViewers] = React.useState([]);
86
- const [macros, setMacros] = React.useState([]);
87
- const [applyingMacro, setApplyingMacro] = React.useState(false);
88
- const [undoToast, setUndoToast] = React.useState(null);
89
- const [splitModal, setSplitModal] = React.useState(null);
90
- const [splitSubject, setSplitSubject] = React.useState("");
91
- const [pendingFiles, setPendingFiles] = React.useState([]);
92
- const [composerDragOver, setComposerDragOver] = React.useState(false);
93
- const [tags, setTags] = React.useState([]);
94
- const [addingTag, setAddingTag] = React.useState(false);
95
- const [newTagValue, setNewTagValue] = React.useState("");
96
- const fetchAll = React.useCallback(async () => {
97
- if (!ticketId) return;
98
- try {
99
- const [mr, tr, ter, ar, cr] = await Promise.all([
100
- fetch(`/api/ticket-messages?where[ticket][equals]=${ticketId}&sort=createdAt&limit=200&depth=1`, { credentials: "include" }),
101
- fetch(`/api/tickets/${ticketId}?depth=1`, { credentials: "include" }),
102
- fetch(`/api/time-entries?where[ticket][equals]=${ticketId}&sort=-date&limit=50&depth=0`, { credentials: "include" }),
103
- fetch(`/api/ticket-activity-log?where[ticket][equals]=${ticketId}&sort=-createdAt&limit=30&depth=0`, { credentials: "include" }),
104
- fetch("/api/canned-responses?sort=sortOrder&limit=50&depth=0", { credentials: "include" })
105
- ]);
106
- if (mr.ok) {
107
- const d = await mr.json();
108
- setMessages(d.docs || []);
109
- }
110
- if (tr.ok) {
111
- const d = await tr.json();
112
- setTicket(d);
113
- if (d.client && typeof d.client === "object") setClient(d.client);
114
- if (Array.isArray(d.tags)) setTags(d.tags.map((t2) => typeof t2 === "object" ? t2.tag || "" : t2).filter(Boolean));
115
- }
116
- if (ter.ok) {
117
- const d = await ter.json();
118
- setTimeEntries(d.docs || []);
119
- }
120
- if (ar.ok) {
121
- const d = await ar.json();
122
- setActivityLog(d.docs || []);
123
- }
124
- if (cr.ok) {
125
- const d = await cr.json();
126
- setCannedResponses(d.docs || []);
127
- }
128
- } catch {
129
- }
130
- setLoading(false);
131
- }, [ticketId]);
132
- const failCountRef = React.useRef({ messages: 0, typing: 0, presence: 0 });
133
- const MAX_FAILS = 3;
134
- React.useEffect(() => {
135
- fetchAll();
136
- }, [fetchAll]);
137
- React.useEffect(() => {
138
- if (!ticket) return;
139
- const clientId = typeof ticket.client === "object" ? ticket.client?.id : ticket.client;
140
- if (!clientId) return;
141
- setSummaryLoading(true);
142
- fetch(`/api/support/client-intelligence?clientId=${clientId}`, { credentials: "include" }).then((r) => r.ok ? r.json() : null).then((d) => {
143
- if (d) setClientSummary(d);
144
- }).catch(() => {
145
- }).finally(() => setSummaryLoading(false));
146
- }, [ticket?.id]);
147
- React.useEffect(() => {
148
- if (sidebarTab !== "client" || !ticket || !ticketId) return;
149
- const clientId = typeof ticket.client === "object" ? ticket.client?.id : ticket.client;
150
- if (!clientId) return;
151
- let aborted = false;
152
- fetch(
153
- `/api/tickets?where[client][equals]=${clientId}&where[id][not_equals]=${ticketId}&limit=5&sort=-createdAt&depth=0`,
154
- { credentials: "include" }
155
- ).then((r) => r.ok ? r.json() : null).then((d) => {
156
- if (!aborted && d?.docs) setPreviousTickets(d.docs);
157
- }).catch(() => {
158
- });
159
- return () => {
160
- aborted = true;
161
- };
162
- }, [sidebarTab, ticket?.id, ticketId]);
163
- React.useEffect(() => {
164
- if (!ticketId || loading) return;
165
- const iv = setInterval(async () => {
166
- if (failCountRef.current.messages >= MAX_FAILS) return;
167
- try {
168
- const [mr, tr] = await Promise.all([
169
- fetch(`/api/ticket-messages?where[ticket][equals]=${ticketId}&sort=createdAt&limit=200&depth=1`, { credentials: "include" }),
170
- fetch(`/api/tickets/${ticketId}?depth=0`, { credentials: "include" })
171
- ]);
172
- if (mr.ok && tr.ok) {
173
- failCountRef.current.messages = 0;
174
- const d = await mr.json();
175
- setMessages(d.docs || []);
176
- const td = await tr.json();
177
- setTicket((p) => p ? { ...p, ...td } : td);
178
- } else {
179
- failCountRef.current.messages++;
180
- }
181
- } catch {
182
- failCountRef.current.messages++;
183
- }
184
- }, 1e4);
185
- return () => clearInterval(iv);
186
- }, [ticketId, loading]);
187
- React.useEffect(() => {
188
- if (!ticketId) return;
189
- fetch(`/api/tickets/${ticketId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ lastAdminReadAt: (/* @__PURE__ */ new Date()).toISOString() }) }).catch(() => {
190
- });
191
- }, [ticketId, messages.length]);
192
- React.useEffect(() => {
193
- if (!ticketId) return;
194
- const iv = setInterval(async () => {
195
- if (failCountRef.current.typing >= MAX_FAILS) return;
196
- try {
197
- const r = await fetch(`/api/support/typing?ticketId=${ticketId}`, { credentials: "include" });
198
- if (r.ok) {
199
- failCountRef.current.typing = 0;
200
- const d = await r.json();
201
- setClientTyping(d.typing);
202
- } else {
203
- failCountRef.current.typing++;
204
- }
205
- } catch {
206
- failCountRef.current.typing++;
207
- }
208
- }, 3e3);
209
- return () => clearInterval(iv);
210
- }, [ticketId]);
211
- React.useEffect(() => {
212
- if (!features.ai || messages.length === 0) return;
213
- const clientMsgs = messages.filter((m) => m.authorType === "client" || m.authorType === "email").slice(-3);
214
- if (clientMsgs.length === 0) return;
215
- const contextText = clientMsgs.map((m) => m.body).join("\n---\n").slice(0, 1e3);
216
- fetch("/api/support/ai", { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ action: "sentiment", text: contextText }) }).then((r) => r.json()).then((d) => {
217
- const raw = (d.sentiment || "").toLowerCase().replace(/[^a-zéèêàùûîôëüöç]/g, "");
218
- const map = {
219
- "frustr\xE9": { emoji: "\u{1F624}", label: "Frustr\xE9", color: "#dc2626" },
220
- "frustre": { emoji: "\u{1F624}", label: "Frustr\xE9", color: "#dc2626" },
221
- "m\xE9content": { emoji: "\u{1F620}", label: "M\xE9content", color: "#ea580c" },
222
- "mecontent": { emoji: "\u{1F620}", label: "M\xE9content", color: "#ea580c" },
223
- "urgent": { emoji: "\u{1F525}", label: "Urgent", color: "#dc2626" },
224
- "neutre": { emoji: "\u{1F610}", label: "Neutre", color: "#6b7280" },
225
- "satisfait": { emoji: "\u{1F60A}", label: "Satisfait", color: "#16a34a" }
226
- };
227
- const m = Object.keys(map).find((k) => raw.includes(k));
228
- setSentiment(m ? map[m] : { emoji: "\u{1F610}", label: "Neutre", color: "#6b7280" });
229
- }).catch(() => {
230
- });
231
- }, [messages.length, features.ai]);
232
- React.useEffect(() => {
233
- threadEndRef.current?.scrollIntoView({ behavior: "smooth" });
234
- }, [messages.length]);
235
- React.useEffect(() => {
236
- if (timerRunning) {
237
- localStorage.setItem(`timer-run-${ticketId}`, "1");
238
- localStorage.setItem(`timer-ts-${ticketId}`, String(Date.now()));
239
- localStorage.setItem(`timer-sec-${ticketId}`, String(timerSeconds));
240
- timerRef.current = setInterval(() => {
241
- setTimerSeconds((p) => {
242
- const next = p + 1;
243
- localStorage.setItem(`timer-sec-${ticketId}`, String(next));
244
- localStorage.setItem(`timer-ts-${ticketId}`, String(Date.now()));
245
- return next;
246
- });
247
- }, 1e3);
248
- } else {
249
- localStorage.setItem(`timer-run-${ticketId}`, "0");
250
- localStorage.setItem(`timer-sec-${ticketId}`, String(timerSeconds));
251
- if (timerRef.current) clearInterval(timerRef.current);
252
- }
253
- return () => {
254
- if (timerRef.current) clearInterval(timerRef.current);
255
- };
256
- }, [timerRunning, ticketId]);
257
- React.useEffect(() => {
258
- if (!ticketId) return;
259
- const join = () => fetch("/api/support/presence", {
260
- method: "POST",
261
- headers: { "Content-Type": "application/json" },
262
- credentials: "include",
263
- body: JSON.stringify({ ticketId: Number(ticketId), action: "join" })
264
- }).catch(() => {
265
- });
266
- join();
267
- const heartbeat = setInterval(join, 2e4);
268
- return () => {
269
- clearInterval(heartbeat);
270
- fetch("/api/support/presence", {
271
- method: "POST",
272
- headers: { "Content-Type": "application/json" },
273
- credentials: "include",
274
- body: JSON.stringify({ ticketId: Number(ticketId), action: "leave" })
275
- }).catch(() => {
276
- });
277
- };
278
- }, [ticketId]);
279
- React.useEffect(() => {
280
- if (!ticketId) return;
281
- const poll = setInterval(async () => {
282
- if (failCountRef.current.presence >= MAX_FAILS) return;
283
- try {
284
- const r = await fetch(`/api/support/presence?ticketId=${ticketId}`, { credentials: "include" });
285
- if (r.ok) {
286
- failCountRef.current.presence = 0;
287
- const d = await r.json();
288
- setOtherViewers(d.viewers || []);
289
- } else {
290
- failCountRef.current.presence++;
291
- }
292
- } catch {
293
- failCountRef.current.presence++;
294
- }
295
- }, 5e3);
296
- return () => clearInterval(poll);
297
- }, [ticketId]);
298
- React.useEffect(() => {
299
- fetch("/api/macros?where[isActive][equals]=true&depth=0&limit=50", { credentials: "include" }).then((r) => r.ok ? r.json() : null).then((d) => {
300
- if (d?.docs) setMacros(d.docs.map((m) => ({ id: m.id, name: m.name })));
301
- }).catch(() => {
302
- });
303
- }, []);
304
- React.useEffect(() => {
305
- if (!showMenu) return;
306
- const handler = (e) => {
307
- if (dropdownRef.current && !dropdownRef.current.contains(e.target)) {
308
- setShowMenu(false);
309
- }
310
- };
311
- document.addEventListener("mousedown", handler);
312
- return () => document.removeEventListener("mousedown", handler);
313
- }, [showMenu]);
314
- React.useEffect(() => {
315
- const handler = (e) => {
316
- const mod = e.metaKey || e.ctrlKey;
317
- if (mod && e.key === "Enter") {
318
- e.preventDefault();
319
- const sendBtn = document.querySelector('[data-action="send-reply"]');
320
- if (sendBtn && !sendBtn.disabled) sendBtn.click();
321
- }
322
- if (mod && e.shiftKey && e.key.toLowerCase() === "n") {
323
- e.preventDefault();
324
- setIsInternal((prev) => !prev);
325
- }
326
- if (e.key === "Escape") {
327
- setShowMenu(false);
328
- setSplitModal(null);
329
- }
330
- };
331
- document.addEventListener("keydown", handler);
332
- return () => document.removeEventListener("keydown", handler);
333
- }, []);
334
- const handleSend = async () => {
335
- if (!replyBody.trim() && !replyHtml.trim() || !ticketId) return;
336
- setSending(true);
337
- try {
338
- const uploadedLinks = [];
339
- for (const file of pendingFiles) {
340
- const formData = new FormData();
341
- formData.append("file", file);
342
- formData.append("_payload", JSON.stringify({ alt: file.name }));
343
- try {
344
- const ur = await fetch("/api/media", { method: "POST", credentials: "include", body: formData });
345
- if (ur.ok) {
346
- const ud = await ur.json();
347
- if (ud.doc?.url) uploadedLinks.push(`[${ud.doc.filename || file.name}](${ud.doc.url})`);
348
- }
349
- } catch {
350
- }
351
- }
352
- const finalBody = uploadedLinks.length > 0 ? `${replyBody.trim()}
353
-
354
- ${uploadedLinks.join("\n")}` : replyBody.trim() || "[Contenu enrichi]";
355
- const finalHtml = uploadedLinks.length > 0 ? `${replyHtml || replyBody.trim()}<br/><br/>${uploadedLinks.map((l) => l.replace(/\[(.+?)\]\((.+?)\)/g, '<a href="$2">$1</a>')).join("<br/>")}` : replyHtml || void 0;
356
- const res = await fetch("/api/ticket-messages", {
357
- method: "POST",
358
- headers: { "Content-Type": "application/json" },
359
- credentials: "include",
360
- body: JSON.stringify({
361
- ticket: Number(ticketId),
362
- body: finalBody,
363
- ...finalHtml ? { bodyHtml: finalHtml } : {},
364
- authorType: sendAsClient ? "client" : "admin",
365
- ...sendAsClient && client ? { authorClient: client.id } : {},
366
- ...fromAlias && !sendAsClient ? { fromAlias } : {},
367
- isInternal: sendAsClient ? false : isInternal,
368
- skipNotification: sendAsClient || isInternal || !notifyClient
369
- })
370
- });
371
- if (res.ok) {
372
- setReplyBody("");
373
- setReplyHtml("");
374
- setIsInternal(false);
375
- setSendAsClient(false);
376
- setFromAlias("");
377
- setPendingFiles([]);
378
- editorRef.current?.clear();
379
- fetchAll();
380
- }
381
- } catch {
382
- } finally {
383
- setSending(false);
384
- }
385
- };
386
- const handleStatusChange = async (v) => {
387
- if (!ticketId) return;
388
- setStatusUpdating(true);
389
- try {
390
- await fetch(`/api/tickets/${ticketId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ status: v }) });
391
- fetchAll();
392
- } catch {
393
- } finally {
394
- setStatusUpdating(false);
395
- }
396
- };
397
- const handleFieldPatch = async (field, value) => {
398
- if (!ticketId) return;
399
- try {
400
- await fetch(`/api/tickets/${ticketId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ [field]: value }) });
401
- fetchAll();
402
- } catch {
403
- }
404
- };
405
- const handleDeleteMessage = (msgId) => {
406
- if (undoToast) clearTimeout(undoToast.timer);
407
- const timer = setTimeout(() => {
408
- fetch(`/api/ticket-messages/${msgId}`, { method: "DELETE", credentials: "include" }).then(() => fetchAll());
409
- setUndoToast(null);
410
- }, 5e3);
411
- setUndoToast({ msgId, timer });
412
- };
413
- const handleUndoDelete = () => {
414
- if (undoToast) {
415
- clearTimeout(undoToast.timer);
416
- setUndoToast(null);
417
- }
418
- };
419
- const startEditMessage = (msg) => {
420
- setEditingMsgId(msg.id);
421
- setEditingBody(msg.body || "");
422
- setEditingHtml(msg.bodyHtml || (msg.body || "").replace(/\n/g, "<br/>"));
423
- };
424
- const cancelEditMessage = () => {
425
- setEditingMsgId(null);
426
- setEditingBody("");
427
- setEditingHtml("");
428
- };
429
- const saveEditMessage = async () => {
430
- if (editingMsgId === null) return;
431
- setEditSaving(true);
432
- try {
433
- const res = await fetch(`/api/ticket-messages/${editingMsgId}`, {
434
- method: "PATCH",
435
- headers: { "Content-Type": "application/json" },
436
- credentials: "include",
437
- body: JSON.stringify({ body: editingBody, bodyHtml: editingHtml || null, skipNotification: true })
438
- });
439
- if (res.ok) {
440
- setEditingMsgId(null);
441
- setEditingBody("");
442
- setEditingHtml("");
443
- fetchAll();
444
- }
445
- } catch {
446
- } finally {
447
- setEditSaving(false);
448
- }
449
- };
450
- const handleSplitConfirm = async () => {
451
- if (!splitModal || !splitSubject.trim()) return;
452
- try {
453
- const r = await fetch("/api/support/split-ticket", { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ messageId: splitModal.messageId, subject: splitSubject }) });
454
- const d = await r.json();
455
- if (d.ticketNumber) {
456
- fetchAll();
457
- }
458
- } catch {
459
- }
460
- setSplitModal(null);
461
- setSplitSubject("");
462
- };
463
- const handleAiSuggest = async () => {
464
- if (messages.length === 0) return;
465
- setAiReplying(true);
466
- try {
467
- const r = await fetch("/api/support/ai", {
468
- method: "POST",
469
- headers: { "Content-Type": "application/json" },
470
- credentials: "include",
471
- body: JSON.stringify({
472
- action: "suggest_reply",
473
- messages: messages.slice(-10).map((m) => ({ authorType: m.authorType, body: m.body })),
474
- clientName: `${client?.firstName || ""} ${client?.lastName || ""}`.trim(),
475
- clientCompany: client?.company
476
- })
477
- });
478
- if (r.ok) {
479
- const d = await r.json();
480
- if (d.reply) setAiSuggestion(d.reply);
481
- }
482
- } catch {
483
- } finally {
484
- setAiReplying(false);
485
- }
486
- };
487
- const handleAiSuggestionInsert = () => {
488
- if (!aiSuggestion) return;
489
- setReplyBody(aiSuggestion);
490
- setReplyHtml(aiSuggestion.replace(/\n/g, "<br/>"));
491
- editorRef.current?.setContent(aiSuggestion.replace(/\n/g, "<br/>"));
492
- setAiSuggestion(null);
493
- };
494
- const handleAiRewrite = async (style = "auto") => {
495
- if (!replyBody.trim()) return;
496
- setAiRewriting(true);
497
- try {
498
- const r = await fetch("/api/support/ai", { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ action: "rewrite", text: replyBody, style }) });
499
- if (r.ok) {
500
- const d = await r.json();
501
- if (d.rewritten) {
502
- setReplyBody(d.rewritten);
503
- setReplyHtml(d.rewritten.replace(/\n/g, "<br/>"));
504
- editorRef.current?.setContent(d.rewritten.replace(/\n/g, "<br/>"));
505
- }
506
- }
507
- } catch {
508
- } finally {
509
- setAiRewriting(false);
510
- }
511
- };
512
- const handleTimerSave = async () => {
513
- if (!ticketId || timerSeconds < 60) return;
514
- try {
515
- await fetch("/api/time-entries", { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ ticket: Number(ticketId), duration: Math.round(timerSeconds / 60), date: (/* @__PURE__ */ new Date()).toISOString(), description: "Timer" }) });
516
- setTimerSeconds(0);
517
- setTimerRunning(false);
518
- fetchAll();
519
- } catch {
520
- }
521
- };
522
- const handleApplyMacro = async (macroId) => {
523
- if (!ticketId || applyingMacro) return;
524
- setApplyingMacro(true);
525
- try {
526
- const r = await fetch("/api/support/apply-macro", {
527
- method: "POST",
528
- headers: { "Content-Type": "application/json" },
529
- credentials: "include",
530
- body: JSON.stringify({ macroId, ticketId: Number(ticketId) })
531
- });
532
- if (r.ok) {
533
- fetchAll();
534
- }
535
- } catch {
536
- } finally {
537
- setApplyingMacro(false);
538
- }
539
- };
540
- const handleFileSelect = (files) => {
541
- if (!files) return;
542
- setPendingFiles((prev) => [...prev, ...Array.from(files)]);
543
- };
544
- const handleFileDrop = (e) => {
545
- e.preventDefault();
546
- setComposerDragOver(false);
547
- handleFileSelect(e.dataTransfer.files);
548
- };
549
- const handleRemoveTag = async (tag) => {
550
- const newTags = tags.filter((t2) => t2 !== tag);
551
- setTags(newTags);
552
- if (ticketId) {
553
- await fetch(`/api/tickets/${ticketId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ tags: newTags.map((t2) => ({ tag: t2 })) }) }).catch(() => {
554
- });
555
- }
556
- };
557
- const handleAddTag = async () => {
558
- if (!newTagValue.trim()) {
559
- setAddingTag(false);
560
- return;
561
- }
562
- const tag = newTagValue.trim();
563
- if (tags.includes(tag)) {
564
- setAddingTag(false);
565
- setNewTagValue("");
566
- return;
567
- }
568
- const newTags = [...tags, tag];
569
- setTags(newTags);
570
- setAddingTag(false);
571
- setNewTagValue("");
572
- if (ticketId) {
573
- await fetch(`/api/tickets/${ticketId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ tags: newTags.map((t2) => ({ tag: t2 })) }) }).catch(() => {
574
- });
575
- }
576
- };
577
- if (!ticketId) return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: 60, textAlign: "center", color: "#94a3b8", fontSize: 14 }, children: t("detail.selectTicket") });
578
- if (loading) return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: 60, textAlign: "center", color: "#94a3b8", fontSize: 14 }, children: t("common.loading") });
579
- if (!ticket) return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: 60, textAlign: "center", color: "#94a3b8", fontSize: 14 }, children: t("detail.notFound") });
580
- const st = constants.STATUS_STYLE[ticket.status || "open"] || constants.STATUS_STYLE.open;
581
- const totalMin = timeEntries.reduce((a, e) => a + (e.duration || 0), 0);
582
- const initials = client ? `${(client.firstName?.[0] || "").toUpperCase()}${(client.lastName?.[0] || "").toUpperCase()}` : "?";
583
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.page, children: [
584
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.topBar, children: [
585
- /* @__PURE__ */ jsxRuntime.jsx(Link__default.default, { href: "/admin/support/inbox", className: s__default.default.backLink, "aria-label": t("detail.backToInbox"), children: "\u2190" }),
586
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.ticketMeta, children: [
587
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: s__default.default.ticketNumber, children: [
588
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: ticket.ticketNumber }),
589
- ticket.category ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
590
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: s__default.default.ticketNumberSep, children: "\xB7" }),
591
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: s__default.default.ticketNumberCat, children: ticket.category })
592
- ] }) : null,
593
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: s__default.default.ticketNumberSep, children: "\xB7" }),
594
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: s__default.default.ticketNumberSource, "aria-label": t("detail.source"), children: [
595
- /* @__PURE__ */ jsxRuntime.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: [
596
- /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "3", y: "5", width: "18", height: "14", rx: "2" }),
597
- /* @__PURE__ */ jsxRuntime.jsx("path", { d: "m3 7 9 6 9-6" })
598
- ] }),
599
- ticket.source === "email" ? "Email" : ticket.source || t("ticket.source.portal")
600
- ] })
601
- ] }),
602
- /* @__PURE__ */ jsxRuntime.jsx("h1", { className: s__default.default.ticketSubject, children: ticket.subject })
603
- ] }),
604
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.topBarRight, children: [
605
- /* @__PURE__ */ jsxRuntime.jsxs("select", { className: s__default.default.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: [
606
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "open", children: t("detail.statusOpen") }),
607
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "waiting_client", children: t("detail.statusWaiting") }),
608
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "resolved", children: t("detail.statusResolved") })
609
- ] }),
610
- sentiment && features.ai && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: s__default.default.sentimentBadge, style: { background: `${sentiment.color}12`, color: sentiment.color }, children: [
611
- sentiment.emoji,
612
- " ",
613
- sentiment.label
614
- ] }),
615
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.dropdown, ref: dropdownRef, children: [
616
- /* @__PURE__ */ jsxRuntime.jsx("button", { className: s__default.default.moreBtn, onClick: () => setShowMenu(!showMenu), "aria-label": "Plus d'options", children: "\xB7\xB7\xB7" }),
617
- showMenu && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.dropdownMenu, children: [
618
- /* @__PURE__ */ jsxRuntime.jsx("button", { className: s__default.default.dropdownItem, onClick: () => {
619
- navigator.clipboard.writeText(window.location.href);
620
- setShowMenu(false);
621
- }, children: t("detail.copyLink") }),
622
- /* @__PURE__ */ jsxRuntime.jsx(Link__default.default, { href: `/admin/collections/tickets/${ticketId}`, className: s__default.default.dropdownItem, onClick: () => setShowMenu(false), children: t("detail.payloadView") }),
623
- /* @__PURE__ */ jsxRuntime.jsx("a", { href: `/support/tickets/${ticketId}`, target: "_blank", rel: "noopener noreferrer", className: s__default.default.dropdownItem, onClick: () => setShowMenu(false), children: t("detail.clientView") })
624
- ] })
625
- ] })
626
- ] })
627
- ] }),
628
- (() => {
629
- const slaInfo = sla.computeSlaState({
630
- slaFirstResponseDue: ticket.slaFirstResponseDue,
631
- slaFirstResponseBreached: ticket.slaFirstResponseBreached,
632
- slaResolutionDue: ticket.slaResolutionDue,
633
- slaResolutionBreached: ticket.slaResolutionBreached,
634
- firstResponseAt: ticket.firstResponseAt
635
- });
636
- if (slaInfo.state !== "breach") return null;
637
- const duration = sla.formatSlaRemaining(slaInfo.remainingMs).replace(/^−/, "");
638
- const target = slaInfo.due ? new Date(slaInfo.due).toLocaleString(dateLocale.DATE_LOCALE, { day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit" }) : "";
639
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.slaBanner, role: "alert", children: [
640
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: s__default.default.slaBannerIcon, "aria-hidden": true, children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
641
- /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 9v4M12 17h.01" }),
642
- /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M10.3 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" })
643
- ] }) }),
644
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.slaBannerBody, children: [
645
- /* @__PURE__ */ jsxRuntime.jsx("strong", { children: t("inbox.slaBanner.breached") }),
646
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: s__default.default.slaBannerSep, children: "\u2014" }),
647
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: t("inbox.slaBanner.responseAwaitedFor", { duration }) }),
648
- target && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: s__default.default.slaBannerTarget, children: [
649
- "\xB7 ",
650
- t("inbox.slaBanner.target", { target })
651
- ] })
652
- ] }),
653
- /* @__PURE__ */ jsxRuntime.jsxs(
654
- "button",
655
- {
656
- type: "button",
657
- className: s__default.default.slaBannerCta,
658
- onClick: () => handleStatusChange("escalated"),
659
- "aria-label": t("inbox.slaBanner.escalate"),
660
- children: [
661
- "\u2191 ",
662
- t("inbox.slaBanner.escalate")
663
- ]
664
- }
665
- )
666
- ] });
667
- })(),
668
- otherViewers.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: {
669
- display: "flex",
670
- alignItems: "center",
671
- gap: 8,
672
- padding: "8px 14px",
673
- marginBottom: 12,
674
- borderRadius: 8,
675
- background: "#fef3c7",
676
- border: "1px solid #fde68a",
677
- fontSize: 13,
678
- fontWeight: 500,
679
- color: "#92400e"
680
- }, children: [
681
- /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", style: { flexShrink: 0 }, children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M9 1L3 9h4l-1 6 6-8H8l1-6z", fill: "#92400e" }) }),
682
- otherViewers.length === 1 ? t("detail.viewingAlso", { names: otherViewers.map((v) => v.name).join(", ") }) : t("detail.viewingAlsoPlural", { names: otherViewers.map((v) => v.name).join(", ") })
683
- ] }),
684
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.layout, children: [
685
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.conversationCol, children: [
686
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.thread, children: [
687
- messages.map((msg, idx) => {
688
- const prev = idx > 0 ? messages[idx - 1] : null;
689
- const showDate = msg.createdAt && (!prev?.createdAt || new Date(msg.createdAt).toDateString() !== new Date(prev.createdAt).toDateString());
690
- const isAdmin = msg.authorType === "admin";
691
- const isPendingDelete = undoToast?.msgId === msg.id;
692
- return /* @__PURE__ */ jsxRuntime.jsxs(React__default.default.Fragment, { children: [
693
- showDate && /* @__PURE__ */ jsxRuntime.jsx("div", { className: s__default.default.dateSeparator, children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: s__default.default.dateSeparatorText, children: helpers.dateLabel(msg.createdAt) }) }),
694
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `${s__default.default.message} ${isAdmin ? s__default.default.messageAdmin : s__default.default.messageClient} ${msg.isInternal ? s__default.default.messageInternal : ""}`, style: isPendingDelete ? { opacity: 0.3, pointerEvents: "none" } : void 0, children: [
695
- /* @__PURE__ */ jsxRuntime.jsx(
696
- "div",
697
- {
698
- className: s__default.default.avatar,
699
- style: {
700
- backgroundColor: isAdmin ? "var(--theme-info-500, #2a6bd8)" : msg.authorType === "email" ? "var(--theme-warning-500, #c97a0a)" : "var(--theme-accent-500, #6b46e8)"
701
- },
702
- children: isAdmin ? "CW" : initials
703
- }
704
- ),
705
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.messageContent, children: [
706
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.messageHeader, children: [
707
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: s__default.default.messageAuthor, children: msg.fromAlias || (isAdmin ? "Support" : msg.authorType === "email" ? "Email" : client?.firstName || "Client") }),
708
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: s__default.default.messageTime, children: helpers.timeAgo(msg.createdAt) }),
709
- msg.isInternal && /* @__PURE__ */ jsxRuntime.jsx("span", { className: s__default.default.badge, style: { background: "#fef3c7", color: "#92400e" }, children: t("detail.badgeInternal") }),
710
- msg.isSolution && /* @__PURE__ */ jsxRuntime.jsx("span", { className: s__default.default.badge, style: { background: "#dcfce7", color: "#166534" }, children: t("detail.badgeSolution") }),
711
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: s__default.default.messageMeta, children: isAdmin && !msg.isInternal && (() => {
712
- const ext = msg;
713
- if (ext.emailOpenedAt) {
714
- const d = new Date(ext.emailOpenedAt).toLocaleString(dateLocale.DATE_LOCALE, { day: "numeric", month: "short", hour: "2-digit", minute: "2-digit" });
715
- return /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { color: "#16a34a", cursor: "help" }, title: `Ouvert le ${d}`, children: [
716
- "\u2713\u2713 Lu ",
717
- d
718
- ] });
719
- }
720
- if (ext.emailSentAt) {
721
- const d = new Date(ext.emailSentAt).toLocaleString(dateLocale.DATE_LOCALE, { day: "numeric", month: "short", hour: "2-digit", minute: "2-digit" });
722
- return /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { color: "#2563eb", cursor: "help" }, title: `Envoy\xE9 le ${d}`, children: [
723
- "\u2713 Envoy\xE9 ",
724
- d
725
- ] });
726
- }
727
- return /* @__PURE__ */ jsxRuntime.jsx("span", { style: { color: "#94a3b8" }, children: "\u2713" });
728
- })() })
729
- ] }),
730
- editingMsgId === msg.id ? /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 6 }, children: [
731
- /* @__PURE__ */ jsxRuntime.jsx(
732
- index.RichTextEditor,
733
- {
734
- ref: editEditorRef,
735
- initialValue: editingHtml,
736
- onChange: (html, text) => {
737
- setEditingHtml(html);
738
- setEditingBody(text);
739
- },
740
- placeholder: t("detail.editPlaceholder"),
741
- minHeight: 120,
742
- onFileUpload: async (file) => {
743
- try {
744
- const formData = new FormData();
745
- formData.append("file", file);
746
- formData.append("_payload", JSON.stringify({ alt: file.name }));
747
- const ur = await fetch("/api/media", { method: "POST", credentials: "include", body: formData });
748
- if (!ur.ok) return null;
749
- const ud = await ur.json();
750
- return ud.doc?.url || null;
751
- } catch {
752
- return null;
753
- }
754
- }
755
- }
756
- ),
757
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: 6, justifyContent: "flex-end" }, children: [
758
- /* @__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: t("common.cancel") }),
759
- /* @__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 ? t("detail.editSaving") : t("detail.editSave") })
760
- ] })
761
- ] }) : 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 }),
762
- Array.isArray(msg.attachments) && msg.attachments.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: s__default.default.attachments, children: msg.attachments.map((att, i) => {
763
- const file = typeof att.file === "object" ? att.file : null;
764
- if (!file) return null;
765
- return (file.mimeType || "").startsWith("image/") ? /* @__PURE__ */ jsxRuntime.jsx("a", { href: file.url || "#", target: "_blank", rel: "noopener noreferrer", children: /* @__PURE__ */ jsxRuntime.jsx("img", { src: file.url || "", alt: "", className: s__default.default.attachmentImg }) }, i) : /* @__PURE__ */ jsxRuntime.jsxs("a", { href: file.url || "#", target: "_blank", rel: "noopener noreferrer", className: s__default.default.attachmentFile, children: [
766
- "PJ ",
767
- file.filename || "Fichier"
768
- ] }, i);
769
- }) })
770
- ] }),
771
- editingMsgId !== msg.id && !msg.deletedAt && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.messageActions, children: [
772
- /* @__PURE__ */ jsxRuntime.jsx("button", { className: s__default.default.actionIcon, title: t("detail.editMessage"), "aria-label": t("detail.editMessageLabel"), onClick: () => startEditMessage(msg), style: { fontSize: 11, width: "auto", padding: "4px 8px" }, children: t("detail.editMessage") }),
773
- /* @__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") }),
774
- features.splitTicket && !msg.isInternal && /* @__PURE__ */ jsxRuntime.jsx("button", { className: s__default.default.actionIcon, title: t("actions.extractMessage"), "aria-label": t("actions.extractToNewTicket"), onClick: () => {
775
- setSplitModal({ messageId: msg.id, preview: msg.body.slice(0, 200) });
776
- setSplitSubject(`Split: ${ticket.subject}`);
777
- }, style: { fontSize: 11, width: "auto", padding: "4px 8px" }, children: t("actions.extractMessage") }),
778
- isAdmin && !msg.isInternal && /* @__PURE__ */ jsxRuntime.jsx("button", { className: s__default.default.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" }, children: t("actions.resendEmail") })
779
- ] })
780
- ] })
781
- ] }, msg.id);
782
- }),
783
- /* @__PURE__ */ jsxRuntime.jsx("div", { ref: threadEndRef })
784
- ] }),
785
- clientTyping && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.typing, children: [
786
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: s__default.default.typingDots, children: [
787
- /* @__PURE__ */ jsxRuntime.jsx("span", {}),
788
- /* @__PURE__ */ jsxRuntime.jsx("span", {}),
789
- /* @__PURE__ */ jsxRuntime.jsx("span", {})
790
- ] }),
791
- t("detail.typing", { name: client?.firstName || "Client" })
792
- ] }),
793
- aiSuggestion && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.aiSuggestionPreview, role: "region", "aria-label": t("composer.aiSuggestion.label"), children: [
794
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.aiSuggestionPreviewHeader, children: [
795
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: s__default.default.aiSuggestionIcon, "aria-hidden": true, children: "\u2728" }),
796
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: s__default.default.aiSuggestionLabel, children: t("composer.aiSuggestion.label") }),
797
- /* @__PURE__ */ jsxRuntime.jsx(
798
- "button",
799
- {
800
- type: "button",
801
- className: s__default.default.aiSuggestionDismiss,
802
- onClick: () => setAiSuggestion(null),
803
- "aria-label": t("composer.aiSuggestion.ignore"),
804
- children: "\xD7"
805
- }
806
- )
807
- ] }),
808
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: s__default.default.aiSuggestionBody, children: aiSuggestion }),
809
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.aiSuggestionActions, children: [
810
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: s__default.default.aiSuggestionIgnore, onClick: () => setAiSuggestion(null), children: t("composer.aiSuggestion.ignore") }),
811
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: s__default.default.aiSuggestionInsert, onClick: handleAiSuggestionInsert, children: t("composer.aiSuggestion.insert") })
812
- ] })
813
- ] }),
814
- /* @__PURE__ */ jsxRuntime.jsxs(
815
- "div",
816
- {
817
- className: `${s__default.default.composer} ${isInternal ? s__default.default.composerInternal : ""} ${composerDragOver ? s__default.default.composerDragOver : ""}`,
818
- onDragOver: (e) => {
819
- e.preventDefault();
820
- setComposerDragOver(true);
821
- },
822
- onDragLeave: () => setComposerDragOver(false),
823
- onDrop: handleFileDrop,
824
- children: [
825
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.composerToolbar, children: [
826
- features.ai && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
827
- /* @__PURE__ */ jsxRuntime.jsx(
828
- "button",
829
- {
830
- className: `${s__default.default.toolbarBtn} ${s__default.default.toolbarBtnAi}`,
831
- "data-tooltip": t("detail.iaSuggestion"),
832
- "aria-label": t("detail.iaSuggestion"),
833
- onClick: handleAiSuggest,
834
- disabled: aiReplying || messages.length === 0,
835
- children: aiReplying ? `\u23F3 ${t("composer.aiSuggestion.loading")}` : `\u2728 ${t("detail.iaSuggestion")}`
836
- }
837
- ),
838
- /* @__PURE__ */ jsxRuntime.jsx(RewriteDropdown.RewriteDropdown, { disabled: aiRewriting || !replyBody.trim(), loading: aiRewriting, onSelect: (style) => handleAiRewrite(style), toolbarBtnClass: s__default.default.toolbarBtn })
839
- ] }),
840
- /* @__PURE__ */ jsxRuntime.jsx(
841
- CodeBlockInserter.CodeBlockInserter,
842
- {
843
- className: s__default.default.toolbarBtn,
844
- onInsert: (block) => {
845
- const nb = replyBody ? replyBody + block : block;
846
- setReplyBody(nb);
847
- setReplyHtml(nb.replace(/\n/g, "<br/>"));
848
- editorRef.current?.setContent(nb.replace(/\n/g, "<br/>"));
849
- }
850
- }
851
- ),
852
- /* @__PURE__ */ jsxRuntime.jsxs("button", { className: s__default.default.toolbarBtn, "data-tooltip": t("detail.file"), "aria-label": t("detail.file"), onClick: () => fileInputRef.current?.click(), style: { fontSize: 11, fontWeight: 700, padding: "4px 10px", width: "auto" }, children: [
853
- "\u{1F4CE} ",
854
- t("detail.file")
855
- ] }),
856
- /* @__PURE__ */ jsxRuntime.jsx("input", { ref: fileInputRef, type: "file", multiple: true, className: s__default.default.hiddenFileInput, onChange: (e) => handleFileSelect(e.target.files) }),
857
- macros.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(
858
- "select",
859
- {
860
- className: s__default.default.cannedSelect,
861
- onChange: (e) => {
862
- const id = Number(e.target.value);
863
- if (id) handleApplyMacro(id);
864
- e.target.value = "";
865
- },
866
- disabled: applyingMacro,
867
- style: { marginLeft: 0 },
868
- "aria-label": t("detail.macros"),
869
- children: [
870
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: applyingMacro ? t("detail.applyingMacro") : t("detail.macros") }),
871
- macros.map((m) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: m.id, children: m.name }, m.id))
872
- ]
873
- }
874
- ),
875
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: s__default.default.toolbarDivider }),
876
- features.canned && cannedResponses.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("select", { className: s__default.default.cannedSelect, "aria-label": t("detail.cannedResponses"), onChange: (e) => {
877
- const cr = cannedResponses.find((c) => String(c.id) === e.target.value);
878
- if (cr) {
879
- let b = cr.body;
880
- if (client) {
881
- b = b.replace(/\{\{client\.firstName\}\}/g, client.firstName).replace(/\{\{client\.company\}\}/g, client.company);
882
- }
883
- setReplyBody(b);
884
- setReplyHtml(b.replace(/\n/g, "<br/>"));
885
- editorRef.current?.setContent(b.replace(/\n/g, "<br/>"));
886
- }
887
- e.target.value = "";
888
- }, children: [
889
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: t("detail.cannedResponses") }),
890
- cannedResponses.map((cr) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: String(cr.id), children: cr.title }, cr.id))
891
- ] })
892
- ] }),
893
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.composerBox, children: [
894
- /* @__PURE__ */ jsxRuntime.jsx(
895
- index.RichTextEditor,
896
- {
897
- ref: editorRef,
898
- onChange: (html, text) => {
899
- setReplyHtml(html);
900
- setReplyBody(text);
901
- },
902
- placeholder: isInternal ? t("composer.placeholderInternal") : t("composer.placeholderReplyTo", { name: client?.firstName || "client" }),
903
- minHeight: 100,
904
- borderColor: "transparent",
905
- onFileUpload: async (file) => {
906
- try {
907
- const formData = new FormData();
908
- formData.append("file", file);
909
- formData.append("_payload", JSON.stringify({ alt: file.name }));
910
- const ur = await fetch("/api/media", { method: "POST", credentials: "include", body: formData });
911
- if (!ur.ok) return null;
912
- const ud = await ur.json();
913
- return ud.doc?.url || null;
914
- } catch {
915
- return null;
916
- }
917
- }
918
- }
919
- ),
920
- pendingFiles.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: s__default.default.uploadPreview, children: pendingFiles.map((f, i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.uploadPreviewItem, children: [
921
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: t("detail.uploadItem", { name: f.name }) }),
922
- /* @__PURE__ */ jsxRuntime.jsx("button", { className: s__default.default.uploadRemoveBtn, "aria-label": t("detail.removeUpload", { name: f.name }), onClick: () => setPendingFiles((prev) => prev.filter((_, j) => j !== i)), children: "\xD7" })
923
- ] }, i)) })
924
- ] }),
925
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.composerFooter, children: [
926
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.composerOptions, children: [
927
- /* @__PURE__ */ jsxRuntime.jsxs(
928
- "select",
929
- {
930
- value: sendAsClient ? "client" : "admin",
931
- onChange: (e) => {
932
- const asClient = e.target.value === "client";
933
- setSendAsClient(asClient);
934
- if (asClient) {
935
- setIsInternal(false);
936
- setNotifyClient(false);
937
- }
938
- },
939
- 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)" },
940
- children: [
941
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "admin", children: t("detail.sendAsSupport") }),
942
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "client", children: t("detail.sendAsClient") })
943
- ]
944
- }
945
- ),
946
- !sendAsClient && /* @__PURE__ */ jsxRuntime.jsxs(
947
- "select",
948
- {
949
- className: s__default.default.composerAliasSelect,
950
- value: fromAlias,
951
- onChange: (e) => setFromAlias(e.target.value),
952
- "aria-label": t("composer.aliases.label"),
953
- title: t("composer.aliases.tooltip"),
954
- children: [
955
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: t("composer.aliases.self") }),
956
- constants.ALIASES.map((a) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: a.value, children: a.label }, a.value))
957
- ]
958
- }
959
- ),
960
- !sendAsClient && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
961
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
962
- /* @__PURE__ */ jsxRuntime.jsx("input", { type: "checkbox", checked: isInternal, onChange: (e) => setIsInternal(e.target.checked) }),
963
- " ",
964
- t("detail.internalNote")
965
- ] }),
966
- /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
967
- /* @__PURE__ */ jsxRuntime.jsx("input", { type: "checkbox", checked: notifyClient, onChange: (e) => setNotifyClient(e.target.checked), disabled: isInternal }),
968
- " ",
969
- t("detail.notify")
970
- ] })
971
- ] })
972
- ] }),
973
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: s__default.default.sendShortcut, "aria-hidden": true, children: [
974
- /* @__PURE__ */ jsxRuntime.jsx("kbd", { children: "\u2318" }),
975
- /* @__PURE__ */ jsxRuntime.jsx("kbd", { children: "\u21B5" })
976
- ] }),
977
- /* @__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 ? t("detail.addClientMessage") : isInternal ? t("detail.sendNote") : t("detail.sendReply") })
978
- ] })
979
- ]
980
- }
981
- )
982
- ] }),
983
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.sidebar, children: [
984
- /* @__PURE__ */ jsxRuntime.jsxs("nav", { role: "tablist", "aria-label": t("detail.sidebar.tabs.overview"), className: s__default.default.sidebarTabs, children: [
985
- /* @__PURE__ */ jsxRuntime.jsx(
986
- "button",
987
- {
988
- type: "button",
989
- role: "tab",
990
- id: "sidebar-tab-overview",
991
- "aria-selected": sidebarTab === "overview",
992
- "aria-controls": "sidebar-panel-overview",
993
- tabIndex: sidebarTab === "overview" ? 0 : -1,
994
- className: `${s__default.default.sidebarTab} ${sidebarTab === "overview" ? s__default.default.sidebarTabActive : ""}`,
995
- onClick: () => setSidebarTab("overview"),
996
- children: t("detail.sidebar.tabs.overview")
997
- }
998
- ),
999
- /* @__PURE__ */ jsxRuntime.jsx(
1000
- "button",
1001
- {
1002
- type: "button",
1003
- role: "tab",
1004
- id: "sidebar-tab-client",
1005
- "aria-selected": sidebarTab === "client",
1006
- "aria-controls": "sidebar-panel-client",
1007
- tabIndex: sidebarTab === "client" ? 0 : -1,
1008
- className: `${s__default.default.sidebarTab} ${sidebarTab === "client" ? s__default.default.sidebarTabActive : ""}`,
1009
- onClick: () => setSidebarTab("client"),
1010
- children: t("detail.sidebar.tabs.client")
1011
- }
1012
- ),
1013
- /* @__PURE__ */ jsxRuntime.jsx(
1014
- "button",
1015
- {
1016
- type: "button",
1017
- role: "tab",
1018
- id: "sidebar-tab-activity",
1019
- "aria-selected": sidebarTab === "activity",
1020
- "aria-controls": "sidebar-panel-activity",
1021
- tabIndex: sidebarTab === "activity" ? 0 : -1,
1022
- className: `${s__default.default.sidebarTab} ${sidebarTab === "activity" ? s__default.default.sidebarTabActive : ""}`,
1023
- onClick: () => setSidebarTab("activity"),
1024
- children: t("detail.sidebar.tabs.activity")
1025
- }
1026
- )
1027
- ] }),
1028
- sidebarTab === "overview" && /* @__PURE__ */ jsxRuntime.jsxs("div", { role: "tabpanel", id: "sidebar-panel-overview", "aria-labelledby": "sidebar-tab-overview", children: [
1029
- client && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.sideSection, children: [
1030
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.clientCard, children: [
1031
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: s__default.default.clientAvatar, children: initials }),
1032
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.clientInfo, children: [
1033
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.clientName, children: [
1034
- client.firstName,
1035
- " ",
1036
- client.lastName
1037
- ] }),
1038
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: s__default.default.clientCompany, children: client.company }),
1039
- /* @__PURE__ */ jsxRuntime.jsx("a", { href: `mailto:${client.email}`, className: s__default.default.clientEmail, children: client.email })
1040
- ] })
1041
- ] }),
1042
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.clientActions, children: [
1043
- /* @__PURE__ */ jsxRuntime.jsx(Link__default.default, { href: `/admin/collections/support-clients/${client.id}`, className: s__default.default.smallBtn, children: t("client.clientSheet") }),
1044
- /* @__PURE__ */ jsxRuntime.jsx("button", { className: s__default.default.smallBtn, onClick: () => window.open(`/api/admin/impersonate?clientId=${client.id}`, "_blank"), children: t("client.clientPortal") })
1045
- ] })
1046
- ] }),
1047
- clientSummary && clientSummary.summary && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `${s__default.default.sideSection} ${s__default.default.aiCard}`, children: [
1048
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.aiCardTitle, children: [
1049
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: s__default.default.aiCardIcon, "aria-hidden": true, children: "\u2728" }),
1050
- t("detail.aiCard.title")
1051
- ] }),
1052
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: s__default.default.aiCardLead, children: clientSummary.summary }),
1053
- clientSummary.recurringTopics && clientSummary.recurringTopics.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: s__default.default.aiCardChips, children: clientSummary.recurringTopics.slice(0, 5).map((tp, i) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: s__default.default.aiChip, children: tp.topic }, `tp-${i}`)) }),
1054
- clientSummary.keyFacts && clientSummary.keyFacts.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("ul", { className: s__default.default.aiCardFacts, children: clientSummary.keyFacts.slice(0, 4).map((f, i) => /* @__PURE__ */ jsxRuntime.jsxs("li", { className: s__default.default.aiCardFact, children: [
1055
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: s__default.default.aiCardDot, "aria-hidden": true }),
1056
- f
1057
- ] }, `kf-${i}`)) }),
1058
- clientSummary?.nextActions && clientSummary.nextActions.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1059
- /* @__PURE__ */ jsxRuntime.jsx("h5", { className: s__default.default.aiCardNextTitle, children: t("detail.aiCard.nextActions.title") }),
1060
- /* @__PURE__ */ jsxRuntime.jsx("ul", { className: s__default.default.aiCardNextActions, children: clientSummary.nextActions.map((action, i) => /* @__PURE__ */ jsxRuntime.jsx(
1061
- NextActionItem.NextActionItem,
1062
- {
1063
- action,
1064
- index: i,
1065
- onToggle: async (done) => {
1066
- const updatedActions = (clientSummary.nextActions || []).map(
1067
- (a, j) => j === i ? { ...a, done } : a
1068
- );
1069
- if (clientSummary.id != null) {
1070
- try {
1071
- await fetch(`/api/client-summaries/${clientSummary.id}`, {
1072
- method: "PATCH",
1073
- credentials: "include",
1074
- headers: { "Content-Type": "application/json" },
1075
- body: JSON.stringify({ nextActions: updatedActions })
1076
- });
1077
- } catch {
1078
- }
1079
- }
1080
- setClientSummary({ ...clientSummary, nextActions: updatedActions });
1081
- }
1082
- },
1083
- action.id || `na-${i}`
1084
- )) })
1085
- ] })
1086
- ] }),
1087
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.sideSection, children: [
1088
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: s__default.default.sideSectionTitle, children: t("detail.details") }),
1089
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.sideField, children: [
1090
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: s__default.default.sideLabel, children: t("detail.priority") }),
1091
- /* @__PURE__ */ jsxRuntime.jsxs("select", { className: s__default.default.sideSelect, value: ticket.priority || "normal", onChange: (e) => handleFieldPatch("priority", e.target.value), "aria-label": t("detail.priority"), children: [
1092
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "low", children: t("ticket.priority.low") }),
1093
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "normal", children: t("ticket.priority.normal") }),
1094
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "high", children: t("ticket.priority.high") }),
1095
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "urgent", children: t("ticket.priority.urgent") })
1096
- ] })
1097
- ] }),
1098
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.sideField, children: [
1099
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: s__default.default.sideLabel, children: t("detail.category") }),
1100
- /* @__PURE__ */ jsxRuntime.jsxs("select", { className: s__default.default.sideSelect, value: ticket.category || "", onChange: (e) => handleFieldPatch("category", e.target.value), "aria-label": t("detail.category"), children: [
1101
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: "\u2014" }),
1102
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "bug", children: t("ticket.category.bug") }),
1103
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "content", children: t("ticket.category.content") }),
1104
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "feature", children: t("ticket.category.feature") }),
1105
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "question", children: t("ticket.category.question") }),
1106
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "hosting", children: t("ticket.category.hosting") })
1107
- ] })
1108
- ] }),
1109
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.sideField, children: [
1110
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: s__default.default.sideLabel, children: t("detail.source") }),
1111
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: s__default.default.sideValue, children: ticket.source || t("ticket.source.portal") })
1112
- ] }),
1113
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.sideField, children: [
1114
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: s__default.default.sideLabel, children: t("detail.assigned") }),
1115
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: s__default.default.sideValue, children: typeof ticket.assignedTo === "object" && ticket.assignedTo ? ticket.assignedTo.firstName || "Admin" : "\u2014" })
1116
- ] })
1117
- ] }),
1118
- features.timeTracking && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.sideSection, children: [
1119
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.sideSectionTitle, children: [
1120
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: t("detail.time") }),
1121
- /* @__PURE__ */ jsxRuntime.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" })
1122
- ] }),
1123
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.timer, children: [
1124
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: `${s__default.default.timerDisplay} ${timerRunning ? s__default.default.timerActive : ""}`, children: [
1125
- String(Math.floor(timerSeconds / 60)).padStart(2, "0"),
1126
- ":",
1127
- String(timerSeconds % 60).padStart(2, "0")
1128
- ] }),
1129
- !timerRunning ? /* @__PURE__ */ jsxRuntime.jsx("button", { className: s__default.default.timerBtn, onClick: () => setTimerRunning(true), "aria-label": t("detail.timerStart"), children: timerSeconds > 0 ? "\u25B6" : "\u25B6 Go" }) : /* @__PURE__ */ jsxRuntime.jsx("button", { className: `${s__default.default.timerBtn} ${s__default.default.timerBtnRunning}`, onClick: () => setTimerRunning(false), "aria-label": t("detail.timerPause"), children: "\u23F8 Pause" }),
1130
- timerSeconds >= 60 && !timerRunning && /* @__PURE__ */ jsxRuntime.jsxs("button", { className: `${s__default.default.timerBtn} ${s__default.default.timerBtnGhost}`, onClick: () => {
1131
- handleTimerSave();
1132
- localStorage.removeItem(`timer-sec-${ticketId}`);
1133
- localStorage.removeItem(`timer-run-${ticketId}`);
1134
- }, "aria-label": t("detail.timerSave"), children: [
1135
- "\u{1F4BE} ",
1136
- Math.round(timerSeconds / 60),
1137
- "m"
1138
- ] })
1139
- ] }),
1140
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: s__default.default.timerBar, "aria-hidden": true, children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: s__default.default.timerBarFill, style: { width: `${Math.min(100, (totalMin + Math.floor(timerSeconds / 60)) / 120 * 100)}%` } }) }),
1141
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: 6, marginTop: 8, alignItems: "center" }, children: [
1142
- /* @__PURE__ */ jsxRuntime.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" }),
1143
- /* @__PURE__ */ jsxRuntime.jsx("button", { className: `${s__default.default.timerBtn} ${s__default.default.timerBtnGhost}`, onClick: async () => {
1144
- const input = document.getElementById("manual-time-input");
1145
- const mins = Number(input?.value);
1146
- if (!mins || mins < 1 || !ticketId) return;
1147
- try {
1148
- 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" }) });
1149
- if (input) input.value = "";
1150
- fetchAll();
1151
- } catch {
1152
- }
1153
- }, style: { fontSize: 11 }, children: t("detail.addTime") })
1154
- ] }),
1155
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginTop: 8, fontSize: 11, color: "var(--theme-elevation-500)" }, children: [
1156
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", justifyContent: "space-between", padding: "2px 0", alignItems: "center" }, children: [
1157
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: t("detail.billing.billable") }),
1158
- /* @__PURE__ */ jsxRuntime.jsx(
1159
- "button",
1160
- {
1161
- onClick: async () => {
1162
- const newVal = ticket.billable === false ? true : false;
1163
- try {
1164
- await fetch(`/api/tickets/${ticketId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ billable: newVal }) });
1165
- fetchAll();
1166
- } catch {
1167
- }
1168
- },
1169
- style: { fontWeight: 600, color: ticket.billable !== false ? "#16a34a" : "#dc2626", background: "none", border: "none", cursor: "pointer", fontSize: 11, textDecoration: "underline" },
1170
- children: ticket.billable !== false ? t("detail.billing.yes") : t("detail.billing.no")
1171
- }
1172
- )
1173
- ] }),
1174
- totalMin > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", justifyContent: "space-between", padding: "2px 0" }, children: [
1175
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: t("detail.billing.estimatedAmount") }),
1176
- /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { fontWeight: 700, color: "var(--theme-text)" }, children: [
1177
- (totalMin / 60 * 60).toFixed(0),
1178
- "\u20AC"
1179
- ] })
1180
- ] })
1181
- ] }),
1182
- timeEntries.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { marginTop: 8, fontSize: 11 }, children: timeEntries.slice(0, 6).map((e) => /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", justifyContent: "space-between", padding: "3px 0", color: "var(--theme-elevation-500)" }, children: [
1183
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: new Date(e.date).toLocaleDateString(dateLocale.DATE_LOCALE, { day: "numeric", month: "short" }) }),
1184
- /* @__PURE__ */ jsxRuntime.jsxs("span", { title: e.description, style: { fontWeight: 600, cursor: e.description ? "help" : "default" }, children: [
1185
- e.duration,
1186
- "min"
1187
- ] })
1188
- ] }, e.id)) })
1189
- ] }),
1190
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.sideSection, children: [
1191
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: s__default.default.sideSectionTitle, children: t("detail.tags") }),
1192
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.tagsWrap, children: [
1193
- tags.map((tag) => /* @__PURE__ */ jsxRuntime.jsxs("span", { className: s__default.default.tagChip, children: [
1194
- tag,
1195
- /* @__PURE__ */ jsxRuntime.jsx("button", { className: s__default.default.tagRemove, "aria-label": t("detail.removeTag", { tag }), onClick: () => handleRemoveTag(tag), children: "\xD7" })
1196
- ] }, tag)),
1197
- addingTag ? /* @__PURE__ */ jsxRuntime.jsx(
1198
- "input",
1199
- {
1200
- className: s__default.default.tagInput,
1201
- value: newTagValue,
1202
- onChange: (e) => setNewTagValue(e.target.value),
1203
- onKeyDown: (e) => {
1204
- if (e.key === "Enter") handleAddTag();
1205
- if (e.key === "Escape") {
1206
- setAddingTag(false);
1207
- setNewTagValue("");
1208
- }
1209
- },
1210
- onBlur: handleAddTag,
1211
- placeholder: t("detail.tagPlaceholder"),
1212
- autoFocus: true
1213
- }
1214
- ) : /* @__PURE__ */ jsxRuntime.jsx("button", { className: s__default.default.tagAddBtn, onClick: () => setAddingTag(true), "aria-label": t("detail.addTag"), children: t("detail.addTagBtn") })
1215
- ] })
1216
- ] }),
1217
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.sideSection, children: [
1218
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: s__default.default.sideSectionTitle, children: t("detail.billing.title") }),
1219
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 8 }, children: [
1220
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between" }, children: [
1221
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: 12 }, children: t("detail.billing.type") }),
1222
- /* @__PURE__ */ jsxRuntime.jsxs(
1223
- "select",
1224
- {
1225
- value: ticket?.billingType || "hourly",
1226
- onChange: async (e) => {
1227
- try {
1228
- await fetch(`/api/tickets/${ticketId}`, {
1229
- method: "PATCH",
1230
- credentials: "include",
1231
- headers: { "Content-Type": "application/json" },
1232
- body: JSON.stringify({ billingType: e.target.value })
1233
- });
1234
- fetchAll();
1235
- } catch {
1236
- }
1237
- },
1238
- style: { fontSize: 12, padding: "4px 8px", borderRadius: 6, border: "1px solid #e5e7eb", background: "#fff" },
1239
- children: [
1240
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "hourly", children: t("detail.billing.hourly") }),
1241
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "flat", children: t("detail.billing.flat") })
1242
- ]
1243
- }
1244
- )
1245
- ] }),
1246
- ticket?.billingType === "flat" && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between" }, children: [
1247
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: 12 }, children: t("detail.billing.flatAmount") }),
1248
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", alignItems: "center", gap: 4 }, children: [
1249
- /* @__PURE__ */ jsxRuntime.jsx(
1250
- "input",
1251
- {
1252
- type: "number",
1253
- defaultValue: ticket?.flatRateAmount ?? "",
1254
- placeholder: "0",
1255
- onBlur: async (e) => {
1256
- const val = e.target.value ? Number(e.target.value) : null;
1257
- try {
1258
- await fetch(`/api/tickets/${ticketId}`, {
1259
- method: "PATCH",
1260
- credentials: "include",
1261
- headers: { "Content-Type": "application/json" },
1262
- body: JSON.stringify({ flatRateAmount: val })
1263
- });
1264
- fetchAll();
1265
- } catch {
1266
- }
1267
- },
1268
- style: { fontSize: 12, padding: "4px 8px", borderRadius: 6, border: "1px solid #e5e7eb", width: 80, textAlign: "right" }
1269
- }
1270
- ),
1271
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: 12, color: "#9ca3af" }, children: "\u20AC" })
1272
- ] })
1273
- ] }),
1274
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between" }, children: [
1275
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: 12 }, children: t("detail.billing.billedAmount") }),
1276
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", alignItems: "center", gap: 4 }, children: [
1277
- /* @__PURE__ */ jsxRuntime.jsx(
1278
- "input",
1279
- {
1280
- type: "number",
1281
- defaultValue: ticket?.billedAmount ?? "",
1282
- placeholder: "0",
1283
- onBlur: async (e) => {
1284
- const val = e.target.value ? Number(e.target.value) : null;
1285
- try {
1286
- await fetch(`/api/tickets/${ticketId}`, {
1287
- method: "PATCH",
1288
- credentials: "include",
1289
- headers: { "Content-Type": "application/json" },
1290
- body: JSON.stringify({ billedAmount: val })
1291
- });
1292
- fetchAll();
1293
- } catch {
1294
- }
1295
- },
1296
- style: { fontSize: 12, padding: "4px 8px", borderRadius: 6, border: "1px solid #e5e7eb", width: 80, textAlign: "right" }
1297
- }
1298
- ),
1299
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: 12, color: "#9ca3af" }, children: "\u20AC" })
1300
- ] })
1301
- ] }),
1302
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between" }, children: [
1303
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: 12 }, children: t("detail.billing.payment") }),
1304
- /* @__PURE__ */ jsxRuntime.jsxs(
1305
- "select",
1306
- {
1307
- value: ticket?.paymentStatus || "unpaid",
1308
- onChange: async (e) => {
1309
- try {
1310
- await fetch(`/api/tickets/${ticketId}`, {
1311
- method: "PATCH",
1312
- credentials: "include",
1313
- headers: { "Content-Type": "application/json" },
1314
- body: JSON.stringify({ paymentStatus: e.target.value })
1315
- });
1316
- fetchAll();
1317
- } catch {
1318
- }
1319
- },
1320
- style: { fontSize: 12, padding: "4px 8px", borderRadius: 6, border: "1px solid #e5e7eb", background: "#fff" },
1321
- children: [
1322
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "unpaid", children: t("detail.billing.unpaid") }),
1323
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "partial", children: t("detail.billing.partial") }),
1324
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "paid", children: t("detail.billing.paid") })
1325
- ]
1326
- }
1327
- )
1328
- ] })
1329
- ] })
1330
- ] }),
1331
- summaryLoading && /* @__PURE__ */ jsxRuntime.jsx("div", { className: s__default.default.sideSection, children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 11, color: "var(--theme-elevation-400)", textAlign: "center", padding: 8 }, children: t("detail.loadingSummary") }) })
1332
- ] }),
1333
- sidebarTab === "client" && /* @__PURE__ */ jsxRuntime.jsxs("div", { role: "tabpanel", id: "sidebar-panel-client", "aria-labelledby": "sidebar-tab-client", children: [
1334
- client && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.sideSection, children: [
1335
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.clientCard, children: [
1336
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: s__default.default.clientAvatar, children: initials }),
1337
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.clientInfo, children: [
1338
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.clientName, children: [
1339
- client.firstName,
1340
- " ",
1341
- client.lastName
1342
- ] }),
1343
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: s__default.default.clientCompany, children: client.company }),
1344
- /* @__PURE__ */ jsxRuntime.jsx("a", { href: `mailto:${client.email}`, className: s__default.default.clientEmail, children: client.email }),
1345
- client.phone && /* @__PURE__ */ jsxRuntime.jsx("a", { href: `tel:${client.phone}`, className: s__default.default.clientEmail, children: client.phone })
1346
- ] })
1347
- ] }),
1348
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.clientActions, children: [
1349
- /* @__PURE__ */ jsxRuntime.jsx(Link__default.default, { href: `/admin/collections/support-clients/${client.id}`, className: s__default.default.smallBtn, children: t("client.clientSheet") }),
1350
- /* @__PURE__ */ jsxRuntime.jsx("button", { className: s__default.default.smallBtn, onClick: () => window.open(`/api/admin/impersonate?clientId=${client.id}`, "_blank"), children: t("client.clientPortal") })
1351
- ] })
1352
- ] }),
1353
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.sideSection, children: [
1354
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: s__default.default.sideSectionTitle, children: t("detail.sidebar.previousTickets") }),
1355
- previousTickets.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: s__default.default.previousTicketsEmpty, children: "\u2014" }) : /* @__PURE__ */ jsxRuntime.jsx("ul", { className: s__default.default.previousTicketsList, children: previousTickets.map((pt) => /* @__PURE__ */ jsxRuntime.jsx("li", { className: s__default.default.previousTicketsItem, children: /* @__PURE__ */ jsxRuntime.jsxs(Link__default.default, { href: `/admin/collections/tickets/${pt.id}`, className: s__default.default.previousTicketsLink, children: [
1356
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: s__default.default.previousTicketsSubject, children: pt.subject || `#${pt.id}` }),
1357
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: s__default.default.previousTicketsMeta, children: [
1358
- pt.status ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: s__default.default.previousTicketsStatus, children: pt.status }) : null,
1359
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: s__default.default.previousTicketsDate, children: new Date(pt.createdAt).toLocaleDateString(dateLocale.DATE_LOCALE, { day: "numeric", month: "short", year: "numeric" }) })
1360
- ] })
1361
- ] }) }, pt.id)) })
1362
- ] })
1363
- ] }),
1364
- sidebarTab === "activity" && /* @__PURE__ */ jsxRuntime.jsx("div", { role: "tabpanel", id: "sidebar-panel-activity", "aria-labelledby": "sidebar-tab-activity", children: features.activityLog ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.sideSection, children: [
1365
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: s__default.default.sideSectionTitle, children: t("detail.activity") }),
1366
- activityLog.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: s__default.default.previousTicketsEmpty, children: "\u2014" }) : activityLog.slice(0, 30).map((a) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.activityItem, children: [
1367
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: s__default.default.activityDot, style: { backgroundColor: a.actorType === "admin" ? "#2563eb" : a.actorType === "system" ? "#6b7280" : "#16a34a" } }),
1368
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.activityContent, children: [
1369
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: s__default.default.activityText, children: (a.detail || a.action).slice(0, 120) }),
1370
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: s__default.default.activityTime, children: helpers.timeAgo(a.createdAt) })
1371
- ] })
1372
- ] }, a.id))
1373
- ] }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: s__default.default.sideSection, children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: s__default.default.previousTicketsEmpty, children: "\u2014" }) }) })
1374
- ] })
1375
- ] }),
1376
- undoToast && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.undoToast, role: "alert", children: [
1377
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: t("detail.messageDeletedToast") }),
1378
- /* @__PURE__ */ jsxRuntime.jsx("button", { className: s__default.default.undoBtn, onClick: handleUndoDelete, children: t("common.cancel") })
1379
- ] }),
1380
- splitModal && /* @__PURE__ */ jsxRuntime.jsx("div", { className: s__default.default.splitOverlay, onClick: (e) => {
1381
- if (e.target === e.currentTarget) {
1382
- setSplitModal(null);
1383
- setSplitSubject("");
1384
- }
1385
- }, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.splitModal, role: "dialog", "aria-label": t("actions.extractToNewTicket"), children: [
1386
- /* @__PURE__ */ jsxRuntime.jsx("h3", { className: s__default.default.splitTitle, children: t("actions.extractToNewTicket") }),
1387
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: s__default.default.splitPreview, children: splitModal.preview }),
1388
- /* @__PURE__ */ jsxRuntime.jsx(
1389
- "input",
1390
- {
1391
- className: s__default.default.splitInput,
1392
- value: splitSubject,
1393
- onChange: (e) => setSplitSubject(e.target.value),
1394
- onKeyDown: (e) => {
1395
- if (e.key === "Enter") handleSplitConfirm();
1396
- },
1397
- placeholder: t("detail.split.subjectPlaceholder"),
1398
- autoFocus: true
1399
- }
1400
- ),
1401
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.splitActions, children: [
1402
- /* @__PURE__ */ jsxRuntime.jsx("button", { className: s__default.default.splitCancelBtn, onClick: () => {
1403
- setSplitModal(null);
1404
- setSplitSubject("");
1405
- }, children: t("common.cancel") }),
1406
- /* @__PURE__ */ jsxRuntime.jsx("button", { className: s__default.default.splitConfirmBtn, onClick: handleSplitConfirm, disabled: !splitSubject.trim(), children: t("detail.split.createBtn") })
1407
- ] })
1408
- ] }) })
1409
- ] });
1410
- };
1411
-
1412
- exports.TicketDetailClient = TicketDetailClient;