@consilioweb/payload-support 0.6.2 → 0.6.4

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 (94) hide show
  1. package/dist/components/TicketConversation/components/AISummaryPanel.cjs +77 -0
  2. package/dist/components/TicketConversation/components/AISummaryPanel.js +76 -0
  3. package/dist/components/TicketConversation/components/ActionPanels.cjs +121 -0
  4. package/dist/components/TicketConversation/components/ActionPanels.js +118 -0
  5. package/dist/components/TicketConversation/components/ActivityLog.cjs +24 -0
  6. package/dist/components/TicketConversation/components/ActivityLog.js +23 -0
  7. package/dist/components/TicketConversation/components/ClientBar.cjs +43 -0
  8. package/dist/components/TicketConversation/components/ClientBar.js +42 -0
  9. package/dist/components/TicketConversation/components/ClientHistory.cjs +138 -0
  10. package/dist/components/TicketConversation/components/ClientHistory.js +137 -0
  11. package/dist/components/TicketConversation/components/CodeBlock.cjs +154 -0
  12. package/dist/components/TicketConversation/components/CodeBlock.js +152 -0
  13. package/dist/components/TicketConversation/components/CodeBlockInserter.cjs +156 -0
  14. package/dist/components/TicketConversation/components/CodeBlockInserter.js +155 -0
  15. package/dist/components/TicketConversation/components/QuickActions.cjs +66 -0
  16. package/dist/components/TicketConversation/components/QuickActions.js +65 -0
  17. package/dist/components/TicketConversation/components/TicketHeader.cjs +93 -0
  18. package/dist/components/TicketConversation/components/TicketHeader.js +92 -0
  19. package/dist/components/TicketConversation/components/TimeTrackingPanel.cjs +134 -0
  20. package/dist/components/TicketConversation/components/TimeTrackingPanel.js +133 -0
  21. package/dist/components/TicketConversation/config.cjs +44 -0
  22. package/dist/components/TicketConversation/config.js +41 -0
  23. package/dist/components/TicketConversation/constants.cjs +100 -0
  24. package/dist/components/TicketConversation/constants.js +96 -0
  25. package/dist/components/TicketConversation/context.cjs +13 -0
  26. package/dist/components/TicketConversation/context.js +11 -0
  27. package/dist/components/TicketConversation/hooks/useAI.cjs +177 -0
  28. package/dist/components/TicketConversation/hooks/useAI.js +176 -0
  29. package/dist/components/TicketConversation/hooks/useMessageActions.cjs +136 -0
  30. package/dist/components/TicketConversation/hooks/useMessageActions.js +135 -0
  31. package/dist/components/TicketConversation/hooks/useReply.cjs +188 -0
  32. package/dist/components/TicketConversation/hooks/useReply.js +187 -0
  33. package/dist/components/TicketConversation/hooks/useTicketActions.cjs +231 -0
  34. package/dist/components/TicketConversation/hooks/useTicketActions.js +230 -0
  35. package/dist/components/TicketConversation/hooks/useTimeTracking.cjs +123 -0
  36. package/dist/components/TicketConversation/hooks/useTimeTracking.js +122 -0
  37. package/dist/components/TicketConversation/hooks/useTranslation.cjs +78 -0
  38. package/dist/components/TicketConversation/hooks/useTranslation.js +70 -0
  39. package/dist/components/TicketConversation/index.cjs +1128 -0
  40. package/dist/components/TicketConversation/index.js +1123 -0
  41. package/dist/components/TicketConversation/locales/en.json +878 -0
  42. package/dist/components/TicketConversation/locales/fr.json +878 -0
  43. package/dist/components/TicketConversation/types.cjs +2 -0
  44. package/dist/components/TicketConversation/types.js +2 -0
  45. package/dist/components/TicketConversation/utils.cjs +27 -0
  46. package/dist/components/TicketConversation/utils.js +25 -0
  47. package/dist/styles/BillingView.module.scss +311 -0
  48. package/dist/styles/ChatView.module.scss +438 -0
  49. package/dist/styles/CommandPalette.module.scss +160 -0
  50. package/dist/styles/CrmView.module.scss +554 -0
  51. package/dist/styles/EmailTracking.module.scss +238 -0
  52. package/dist/styles/ImportConversation.module.scss +267 -0
  53. package/dist/styles/Layout.module.scss +55 -0
  54. package/dist/styles/Logs.module.scss +164 -0
  55. package/dist/styles/NewTicket.module.scss +143 -0
  56. package/dist/styles/PendingEmails.module.scss +629 -0
  57. package/dist/styles/SupportDashboard.module.scss +649 -0
  58. package/dist/styles/TicketDetail.module.scss +1050 -0
  59. package/dist/styles/TicketInbox.module.scss +296 -0
  60. package/dist/styles/TicketingSettings.module.scss +358 -0
  61. package/dist/styles/TimeDashboard.module.scss +287 -0
  62. package/dist/styles/_tokens.scss +78 -0
  63. package/dist/{components/TicketConversation.css → styles/theme.css} +326 -13
  64. package/dist/views/BillingView/client.js +1 -1
  65. package/dist/views/BillingView/index.js +2 -2
  66. package/dist/views/ChatView/client.js +1 -1
  67. package/dist/views/ChatView/index.js +2 -2
  68. package/dist/views/CrmView/client.js +1 -1
  69. package/dist/views/CrmView/index.js +2 -2
  70. package/dist/views/EmailTrackingView/client.js +1 -1
  71. package/dist/views/EmailTrackingView/index.js +2 -2
  72. package/dist/views/ImportConversationView/client.js +1 -1
  73. package/dist/views/ImportConversationView/index.js +2 -2
  74. package/dist/views/LogsView/client.js +1 -1
  75. package/dist/views/LogsView/index.js +2 -2
  76. package/dist/views/NewTicketView/client.js +1 -1
  77. package/dist/views/NewTicketView/index.js +2 -2
  78. package/dist/views/PendingEmailsView/client.js +1 -1
  79. package/dist/views/PendingEmailsView/index.js +2 -2
  80. package/dist/views/SupportDashboardView/client.js +1 -1
  81. package/dist/views/SupportDashboardView/index.js +2 -2
  82. package/dist/views/TicketDetailView/client.js +2 -2
  83. package/dist/views/TicketDetailView/index.js +2 -2
  84. package/dist/views/TicketInboxView/client.js +1 -1
  85. package/dist/views/TicketInboxView/index.js +2 -2
  86. package/dist/views/TicketingSettingsView/client.js +3 -3
  87. package/dist/views/TicketingSettingsView/index.js +2 -2
  88. package/dist/views/TimeDashboardView/client.js +1 -1
  89. package/dist/views/TimeDashboardView/index.js +2 -2
  90. package/dist/views/shared/AdminViewHeader.js +1 -1
  91. package/dist/views/shared/index.js +5 -5
  92. package/dist/views.js +13 -13
  93. package/package.json +2 -2
  94. package/dist/components/TicketConversation.js +0 -3004
@@ -0,0 +1,230 @@
1
+ "use client";
2
+ import { useState, useRef } from 'react';
3
+
4
+ function useTicketActions(id, fetchAll) {
5
+ const [statusUpdating, setStatusUpdating] = useState(false);
6
+ const [showMerge, setShowMerge] = useState(false);
7
+ const [mergeTarget, setMergeTarget] = useState("");
8
+ const [mergeTargetInfo, setMergeTargetInfo] = useState(null);
9
+ const [merging, setMerging] = useState(false);
10
+ const [mergeError, setMergeError] = useState("");
11
+ const [showExtMsg, setShowExtMsg] = useState(false);
12
+ const [extMsgBody, setExtMsgBody] = useState("");
13
+ const [extMsgAuthor, setExtMsgAuthor] = useState("client");
14
+ const [extMsgDate, setExtMsgDate] = useState("");
15
+ const [extMsgFiles, setExtMsgFiles] = useState([]);
16
+ const [sendingExtMsg, setSendingExtMsg] = useState(false);
17
+ const extFileInputRef = useRef(null);
18
+ const [showSnooze, setShowSnooze] = useState(false);
19
+ const [snoozeUntil, setSnoozeUntil] = useState(null);
20
+ const [snoozeSaving, setSnoozeSaving] = useState(false);
21
+ const [showNextTicket, setShowNextTicket] = useState(false);
22
+ const [nextTicketId, setNextTicketId] = useState(null);
23
+ const [nextTicketInfo, setNextTicketInfo] = useState("");
24
+ const handleStatusChange = async (newStatus) => {
25
+ if (!id) return;
26
+ setStatusUpdating(true);
27
+ try {
28
+ await fetch(`/api/tickets/${id}`, {
29
+ method: "PATCH",
30
+ headers: { "Content-Type": "application/json" },
31
+ credentials: "include",
32
+ body: JSON.stringify({ status: newStatus })
33
+ });
34
+ window.location.reload();
35
+ } catch {
36
+ } finally {
37
+ setStatusUpdating(false);
38
+ }
39
+ };
40
+ const handleMergeLookup = async () => {
41
+ if (!mergeTarget.trim()) return;
42
+ setMergeTargetInfo(null);
43
+ setMergeError("");
44
+ try {
45
+ const searchVal = mergeTarget.trim().toUpperCase();
46
+ const res = await fetch(`/api/tickets?where[ticketNumber][equals]=${encodeURIComponent(searchVal)}&limit=1&depth=0`, { credentials: "include" });
47
+ if (res.ok) {
48
+ const d = await res.json();
49
+ if (d.docs?.length > 0) {
50
+ const t = d.docs[0];
51
+ if (String(t.id) === String(id)) {
52
+ setMergeError("Impossible de fusionner un ticket avec lui-m\xEAme");
53
+ } else {
54
+ setMergeTargetInfo({ id: t.id, ticketNumber: t.ticketNumber, subject: t.subject });
55
+ }
56
+ } else {
57
+ setMergeError("Ticket introuvable");
58
+ }
59
+ }
60
+ } catch {
61
+ setMergeError("Erreur de recherche");
62
+ }
63
+ };
64
+ const handleMerge = async () => {
65
+ if (!mergeTargetInfo || !id) return;
66
+ setMerging(true);
67
+ setMergeError("");
68
+ try {
69
+ const res = await fetch("/api/support/merge-tickets", {
70
+ method: "POST",
71
+ headers: { "Content-Type": "application/json" },
72
+ credentials: "include",
73
+ body: JSON.stringify({ sourceTicketId: id, targetTicketId: mergeTargetInfo.id })
74
+ });
75
+ if (res.ok) {
76
+ window.location.href = `/admin/collections/tickets/${mergeTargetInfo.id}`;
77
+ } else {
78
+ const d = await res.json().catch(() => ({}));
79
+ setMergeError(d.error || "Erreur de fusion");
80
+ }
81
+ } catch {
82
+ setMergeError("Erreur r\xE9seau");
83
+ } finally {
84
+ setMerging(false);
85
+ }
86
+ };
87
+ const handleExtFileChange = (e) => {
88
+ if (e.target.files) {
89
+ const maxSize = 1 * 1024 * 1024;
90
+ const newFiles = Array.from(e.target.files);
91
+ const tooLarge = newFiles.filter((f) => f.size > maxSize);
92
+ if (tooLarge.length > 0) {
93
+ alert(`Fichier(s) trop volumineux (max 1 Mo) : ${tooLarge.map((f) => f.name).join(", ")}`);
94
+ }
95
+ setExtMsgFiles((prev) => [...prev, ...newFiles.filter((f) => f.size <= maxSize)]);
96
+ }
97
+ if (extFileInputRef.current) extFileInputRef.current.value = "";
98
+ };
99
+ const handleSendExtMsg = async () => {
100
+ if (!extMsgBody.trim() || !id) return;
101
+ setSendingExtMsg(true);
102
+ try {
103
+ const uploadedIds = [];
104
+ for (const file of extMsgFiles) {
105
+ if (file.size > 1 * 1024 * 1024) continue;
106
+ const formData = new FormData();
107
+ formData.append("file", file);
108
+ formData.append("_payload", JSON.stringify({ alt: file.name }));
109
+ const uploadRes = await fetch("/api/media", { method: "POST", credentials: "include", body: formData });
110
+ if (uploadRes.ok) {
111
+ const d = await uploadRes.json();
112
+ if (d.doc?.id) uploadedIds.push(d.doc.id);
113
+ }
114
+ }
115
+ const messageData = {
116
+ ticket: id,
117
+ body: extMsgBody.trim(),
118
+ authorType: extMsgAuthor,
119
+ isInternal: false,
120
+ skipNotification: true
121
+ };
122
+ if (extMsgDate) {
123
+ messageData.createdAt = new Date(extMsgDate).toISOString();
124
+ }
125
+ if (uploadedIds.length > 0) {
126
+ messageData.attachments = uploadedIds.map((mid) => ({ file: mid }));
127
+ }
128
+ const res = await fetch("/api/ticket-messages", {
129
+ method: "POST",
130
+ headers: { "Content-Type": "application/json" },
131
+ credentials: "include",
132
+ body: JSON.stringify(messageData)
133
+ });
134
+ if (res.ok) {
135
+ setExtMsgBody("");
136
+ setExtMsgDate("");
137
+ setExtMsgFiles([]);
138
+ setShowExtMsg(false);
139
+ fetchAll();
140
+ }
141
+ } catch {
142
+ } finally {
143
+ setSendingExtMsg(false);
144
+ }
145
+ };
146
+ const handleSnooze = async (days, customDate) => {
147
+ if (!id) return;
148
+ setSnoozeSaving(true);
149
+ try {
150
+ let newSnooze = null;
151
+ if (days !== null) {
152
+ const d = /* @__PURE__ */ new Date();
153
+ d.setDate(d.getDate() + days);
154
+ newSnooze = d.toISOString();
155
+ } else if (customDate) {
156
+ newSnooze = new Date(customDate).toISOString();
157
+ }
158
+ await fetch(`/api/tickets/${id}`, {
159
+ method: "PATCH",
160
+ headers: { "Content-Type": "application/json" },
161
+ credentials: "include",
162
+ body: JSON.stringify({ snoozeUntil: newSnooze })
163
+ });
164
+ setSnoozeUntil(newSnooze);
165
+ setShowSnooze(false);
166
+ fetchAll();
167
+ } catch {
168
+ } finally {
169
+ setSnoozeSaving(false);
170
+ }
171
+ };
172
+ const handleNextTicket = async () => {
173
+ try {
174
+ const res = await fetch("/api/tickets?where[status][equals]=open&sort=updatedAt&limit=1&depth=0", { credentials: "include" });
175
+ if (res.ok) {
176
+ const d = await res.json();
177
+ if (d.docs?.length > 0 && String(d.docs[0].id) !== String(id)) {
178
+ setNextTicketId(d.docs[0].id);
179
+ setNextTicketInfo(`${d.docs[0].ticketNumber} \u2014 ${d.docs[0].subject}`);
180
+ } else {
181
+ setNextTicketId(null);
182
+ setNextTicketInfo("Aucun autre ticket ouvert");
183
+ }
184
+ setShowNextTicket(true);
185
+ }
186
+ } catch {
187
+ }
188
+ };
189
+ return {
190
+ statusUpdating,
191
+ handleStatusChange,
192
+ showMerge,
193
+ setShowMerge,
194
+ mergeTarget,
195
+ setMergeTarget,
196
+ mergeTargetInfo,
197
+ setMergeTargetInfo,
198
+ mergeError,
199
+ setMergeError,
200
+ merging,
201
+ handleMergeLookup,
202
+ handleMerge,
203
+ showExtMsg,
204
+ setShowExtMsg,
205
+ extMsgBody,
206
+ setExtMsgBody,
207
+ extMsgAuthor,
208
+ setExtMsgAuthor,
209
+ extMsgDate,
210
+ setExtMsgDate,
211
+ extMsgFiles,
212
+ setExtMsgFiles,
213
+ sendingExtMsg,
214
+ handleExtFileChange,
215
+ handleSendExtMsg,
216
+ showSnooze,
217
+ setShowSnooze,
218
+ snoozeUntil,
219
+ setSnoozeUntil,
220
+ snoozeSaving,
221
+ handleSnooze,
222
+ showNextTicket,
223
+ setShowNextTicket,
224
+ nextTicketId,
225
+ nextTicketInfo,
226
+ handleNextTicket
227
+ };
228
+ }
229
+
230
+ export { useTicketActions };
@@ -0,0 +1,123 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+
5
+ function useTimeTracking(id, fetchAll) {
6
+ const [duration, setDuration] = react.useState("");
7
+ const [timeDescription, setTimeDescription] = react.useState("");
8
+ const [addingTime, setAddingTime] = react.useState(false);
9
+ const [timeSuccess, setTimeSuccess] = react.useState("");
10
+ const [timerRunning, setTimerRunning] = react.useState(false);
11
+ const [timerSeconds, setTimerSeconds] = react.useState(0);
12
+ const [timerDescription, setTimerDescription] = react.useState("");
13
+ const timerIntervalRef = react.useRef(null);
14
+ const timerKeepAliveRef = react.useRef(null);
15
+ const handleAddTime = async () => {
16
+ if (!duration || !id) return;
17
+ setAddingTime(true);
18
+ setTimeSuccess("");
19
+ try {
20
+ const res = await fetch("/api/time-entries", {
21
+ method: "POST",
22
+ headers: { "Content-Type": "application/json" },
23
+ credentials: "include",
24
+ body: JSON.stringify({
25
+ ticket: id,
26
+ duration: parseInt(duration, 10),
27
+ date: (/* @__PURE__ */ new Date()).toISOString(),
28
+ description: timeDescription.trim() || void 0
29
+ })
30
+ });
31
+ if (res.ok) {
32
+ setDuration("");
33
+ setTimeDescription("");
34
+ setTimeSuccess(`${duration} min ajout\xE9es`);
35
+ setTimeout(() => setTimeSuccess(""), 3e3);
36
+ fetchAll();
37
+ }
38
+ } catch {
39
+ } finally {
40
+ setAddingTime(false);
41
+ }
42
+ };
43
+ const handleTimerStart = (reset = false) => {
44
+ setTimerRunning(true);
45
+ if (reset) setTimerSeconds(0);
46
+ timerIntervalRef.current = setInterval(() => {
47
+ setTimerSeconds((prev) => prev + 1);
48
+ }, 1e3);
49
+ timerKeepAliveRef.current = setInterval(() => {
50
+ fetch("/api/users/me", { credentials: "include" }).catch(() => {
51
+ });
52
+ }, 5 * 60 * 1e3);
53
+ };
54
+ const handleTimerStop = () => {
55
+ setTimerRunning(false);
56
+ if (timerIntervalRef.current) {
57
+ clearInterval(timerIntervalRef.current);
58
+ timerIntervalRef.current = null;
59
+ }
60
+ if (timerKeepAliveRef.current) {
61
+ clearInterval(timerKeepAliveRef.current);
62
+ timerKeepAliveRef.current = null;
63
+ }
64
+ };
65
+ const handleTimerSave = async () => {
66
+ if (!id || timerSeconds < 60) return;
67
+ const minutes = Math.round(timerSeconds / 60);
68
+ setAddingTime(true);
69
+ try {
70
+ const res = await fetch("/api/time-entries", {
71
+ method: "POST",
72
+ headers: { "Content-Type": "application/json" },
73
+ credentials: "include",
74
+ body: JSON.stringify({
75
+ ticket: id,
76
+ duration: minutes,
77
+ date: (/* @__PURE__ */ new Date()).toISOString(),
78
+ description: timerDescription.trim() || "Timer"
79
+ })
80
+ });
81
+ if (res.ok) {
82
+ setTimeSuccess(`${minutes} min ajout\xE9es (timer)`);
83
+ setTimeout(() => setTimeSuccess(""), 3e3);
84
+ setTimerSeconds(0);
85
+ setTimerDescription("");
86
+ fetchAll();
87
+ }
88
+ } catch {
89
+ } finally {
90
+ setAddingTime(false);
91
+ }
92
+ };
93
+ const handleTimerDiscard = () => {
94
+ setTimerSeconds(0);
95
+ setTimerDescription("");
96
+ };
97
+ react.useEffect(() => {
98
+ return () => {
99
+ if (timerIntervalRef.current) clearInterval(timerIntervalRef.current);
100
+ if (timerKeepAliveRef.current) clearInterval(timerKeepAliveRef.current);
101
+ };
102
+ }, []);
103
+ return {
104
+ duration,
105
+ setDuration,
106
+ timeDescription,
107
+ setTimeDescription,
108
+ addingTime,
109
+ timeSuccess,
110
+ timerRunning,
111
+ timerSeconds,
112
+ setTimerSeconds,
113
+ timerDescription,
114
+ setTimerDescription,
115
+ handleAddTime,
116
+ handleTimerStart,
117
+ handleTimerStop,
118
+ handleTimerSave,
119
+ handleTimerDiscard
120
+ };
121
+ }
122
+
123
+ exports.useTimeTracking = useTimeTracking;
@@ -0,0 +1,122 @@
1
+ "use client";
2
+ import { useState, useRef, useEffect } from 'react';
3
+
4
+ function useTimeTracking(id, fetchAll) {
5
+ const [duration, setDuration] = useState("");
6
+ const [timeDescription, setTimeDescription] = useState("");
7
+ const [addingTime, setAddingTime] = useState(false);
8
+ const [timeSuccess, setTimeSuccess] = useState("");
9
+ const [timerRunning, setTimerRunning] = useState(false);
10
+ const [timerSeconds, setTimerSeconds] = useState(0);
11
+ const [timerDescription, setTimerDescription] = useState("");
12
+ const timerIntervalRef = useRef(null);
13
+ const timerKeepAliveRef = useRef(null);
14
+ const handleAddTime = async () => {
15
+ if (!duration || !id) return;
16
+ setAddingTime(true);
17
+ setTimeSuccess("");
18
+ try {
19
+ const res = await fetch("/api/time-entries", {
20
+ method: "POST",
21
+ headers: { "Content-Type": "application/json" },
22
+ credentials: "include",
23
+ body: JSON.stringify({
24
+ ticket: id,
25
+ duration: parseInt(duration, 10),
26
+ date: (/* @__PURE__ */ new Date()).toISOString(),
27
+ description: timeDescription.trim() || void 0
28
+ })
29
+ });
30
+ if (res.ok) {
31
+ setDuration("");
32
+ setTimeDescription("");
33
+ setTimeSuccess(`${duration} min ajout\xE9es`);
34
+ setTimeout(() => setTimeSuccess(""), 3e3);
35
+ fetchAll();
36
+ }
37
+ } catch {
38
+ } finally {
39
+ setAddingTime(false);
40
+ }
41
+ };
42
+ const handleTimerStart = (reset = false) => {
43
+ setTimerRunning(true);
44
+ if (reset) setTimerSeconds(0);
45
+ timerIntervalRef.current = setInterval(() => {
46
+ setTimerSeconds((prev) => prev + 1);
47
+ }, 1e3);
48
+ timerKeepAliveRef.current = setInterval(() => {
49
+ fetch("/api/users/me", { credentials: "include" }).catch(() => {
50
+ });
51
+ }, 5 * 60 * 1e3);
52
+ };
53
+ const handleTimerStop = () => {
54
+ setTimerRunning(false);
55
+ if (timerIntervalRef.current) {
56
+ clearInterval(timerIntervalRef.current);
57
+ timerIntervalRef.current = null;
58
+ }
59
+ if (timerKeepAliveRef.current) {
60
+ clearInterval(timerKeepAliveRef.current);
61
+ timerKeepAliveRef.current = null;
62
+ }
63
+ };
64
+ const handleTimerSave = async () => {
65
+ if (!id || timerSeconds < 60) return;
66
+ const minutes = Math.round(timerSeconds / 60);
67
+ setAddingTime(true);
68
+ try {
69
+ const res = await fetch("/api/time-entries", {
70
+ method: "POST",
71
+ headers: { "Content-Type": "application/json" },
72
+ credentials: "include",
73
+ body: JSON.stringify({
74
+ ticket: id,
75
+ duration: minutes,
76
+ date: (/* @__PURE__ */ new Date()).toISOString(),
77
+ description: timerDescription.trim() || "Timer"
78
+ })
79
+ });
80
+ if (res.ok) {
81
+ setTimeSuccess(`${minutes} min ajout\xE9es (timer)`);
82
+ setTimeout(() => setTimeSuccess(""), 3e3);
83
+ setTimerSeconds(0);
84
+ setTimerDescription("");
85
+ fetchAll();
86
+ }
87
+ } catch {
88
+ } finally {
89
+ setAddingTime(false);
90
+ }
91
+ };
92
+ const handleTimerDiscard = () => {
93
+ setTimerSeconds(0);
94
+ setTimerDescription("");
95
+ };
96
+ useEffect(() => {
97
+ return () => {
98
+ if (timerIntervalRef.current) clearInterval(timerIntervalRef.current);
99
+ if (timerKeepAliveRef.current) clearInterval(timerKeepAliveRef.current);
100
+ };
101
+ }, []);
102
+ return {
103
+ duration,
104
+ setDuration,
105
+ timeDescription,
106
+ setTimeDescription,
107
+ addingTime,
108
+ timeSuccess,
109
+ timerRunning,
110
+ timerSeconds,
111
+ setTimerSeconds,
112
+ timerDescription,
113
+ setTimerDescription,
114
+ handleAddTime,
115
+ handleTimerStart,
116
+ handleTimerStop,
117
+ handleTimerSave,
118
+ handleTimerDiscard
119
+ };
120
+ }
121
+
122
+ export { useTimeTracking };
@@ -0,0 +1,78 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var frTranslations = require('../locales/fr.json');
5
+ var enTranslations = require('../locales/en.json');
6
+
7
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
8
+
9
+ var frTranslations__default = /*#__PURE__*/_interopDefault(frTranslations);
10
+ var enTranslations__default = /*#__PURE__*/_interopDefault(enTranslations);
11
+
12
+ const LOCALE_STORAGE_KEY = "support_locale";
13
+ const DEFAULT_LOCALE = "fr";
14
+ const translations = {
15
+ fr: frTranslations__default.default,
16
+ en: enTranslations__default.default
17
+ };
18
+ function getNestedValue(obj, path) {
19
+ const keys = path.split(".");
20
+ let current = obj;
21
+ for (const key of keys) {
22
+ if (current == null || typeof current !== "object") {
23
+ return path;
24
+ }
25
+ current = current[key];
26
+ }
27
+ if (typeof current === "string") {
28
+ return current;
29
+ }
30
+ return path;
31
+ }
32
+ function interpolate(template, vars) {
33
+ if (!vars) return template;
34
+ return template.replace(/\{\{(\w+(?:\.\w+)*)\}\}/g, (_, key) => {
35
+ return String(vars[key] ?? `{{${key}}}`);
36
+ });
37
+ }
38
+ function useTranslation() {
39
+ const [locale, setLocaleState] = react.useState(() => {
40
+ if (typeof window === "undefined") return DEFAULT_LOCALE;
41
+ try {
42
+ const stored = localStorage.getItem(LOCALE_STORAGE_KEY);
43
+ if (stored === "fr" || stored === "en") return stored;
44
+ } catch {
45
+ }
46
+ return DEFAULT_LOCALE;
47
+ });
48
+ react.useEffect(() => {
49
+ fetch("/api/support/user-prefs", { credentials: "include" }).then((r) => r.ok ? r.json() : null).then((data) => {
50
+ if (data?.locale && (data.locale === "fr" || data.locale === "en")) {
51
+ setLocaleState(data.locale);
52
+ }
53
+ }).catch(() => {
54
+ });
55
+ }, []);
56
+ react.useEffect(() => {
57
+ try {
58
+ localStorage.setItem(LOCALE_STORAGE_KEY, locale);
59
+ } catch {
60
+ }
61
+ }, [locale]);
62
+ const setLocale = react.useCallback((newLocale) => {
63
+ setLocaleState(newLocale);
64
+ }, []);
65
+ const t = react.useCallback(
66
+ (key, vars) => {
67
+ const dict = translations[locale] || translations[DEFAULT_LOCALE];
68
+ const value = getNestedValue(dict, key);
69
+ return interpolate(value, vars);
70
+ },
71
+ [locale]
72
+ );
73
+ return { t, locale, setLocale };
74
+ }
75
+
76
+ exports.DEFAULT_LOCALE = DEFAULT_LOCALE;
77
+ exports.LOCALE_STORAGE_KEY = LOCALE_STORAGE_KEY;
78
+ exports.useTranslation = useTranslation;
@@ -0,0 +1,70 @@
1
+ "use client";
2
+ import { useState, useEffect, useCallback } from 'react';
3
+ import frTranslations from '../locales/fr.json';
4
+ import enTranslations from '../locales/en.json';
5
+
6
+ const LOCALE_STORAGE_KEY = "support_locale";
7
+ const DEFAULT_LOCALE = "fr";
8
+ const translations = {
9
+ fr: frTranslations,
10
+ en: enTranslations
11
+ };
12
+ function getNestedValue(obj, path) {
13
+ const keys = path.split(".");
14
+ let current = obj;
15
+ for (const key of keys) {
16
+ if (current == null || typeof current !== "object") {
17
+ return path;
18
+ }
19
+ current = current[key];
20
+ }
21
+ if (typeof current === "string") {
22
+ return current;
23
+ }
24
+ return path;
25
+ }
26
+ function interpolate(template, vars) {
27
+ if (!vars) return template;
28
+ return template.replace(/\{\{(\w+(?:\.\w+)*)\}\}/g, (_, key) => {
29
+ return String(vars[key] ?? `{{${key}}}`);
30
+ });
31
+ }
32
+ function useTranslation() {
33
+ const [locale, setLocaleState] = useState(() => {
34
+ if (typeof window === "undefined") return DEFAULT_LOCALE;
35
+ try {
36
+ const stored = localStorage.getItem(LOCALE_STORAGE_KEY);
37
+ if (stored === "fr" || stored === "en") return stored;
38
+ } catch {
39
+ }
40
+ return DEFAULT_LOCALE;
41
+ });
42
+ useEffect(() => {
43
+ fetch("/api/support/user-prefs", { credentials: "include" }).then((r) => r.ok ? r.json() : null).then((data) => {
44
+ if (data?.locale && (data.locale === "fr" || data.locale === "en")) {
45
+ setLocaleState(data.locale);
46
+ }
47
+ }).catch(() => {
48
+ });
49
+ }, []);
50
+ useEffect(() => {
51
+ try {
52
+ localStorage.setItem(LOCALE_STORAGE_KEY, locale);
53
+ } catch {
54
+ }
55
+ }, [locale]);
56
+ const setLocale = useCallback((newLocale) => {
57
+ setLocaleState(newLocale);
58
+ }, []);
59
+ const t = useCallback(
60
+ (key, vars) => {
61
+ const dict = translations[locale] || translations[DEFAULT_LOCALE];
62
+ const value = getNestedValue(dict, key);
63
+ return interpolate(value, vars);
64
+ },
65
+ [locale]
66
+ );
67
+ return { t, locale, setLocale };
68
+ }
69
+
70
+ export { DEFAULT_LOCALE, LOCALE_STORAGE_KEY, useTranslation };