@consilioweb/payload-support 0.9.10 → 0.9.12
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.
- package/dist/components/TicketConversation/hooks/useAI.cjs +24 -5
- package/dist/components/TicketConversation/hooks/useAI.js +24 -5
- package/dist/index.cjs +301 -13
- package/dist/index.js +301 -13
- package/dist/styles/BillingView.module.scss +132 -0
- package/dist/views/BillingView/client.cjs +287 -170
- package/dist/views/BillingView/client.js +287 -170
- package/dist/views/TicketDetailView/client.cjs +28 -6
- package/dist/views/TicketDetailView/client.js +28 -6
- package/package.json +1 -1
- package/src/collections/Tickets.ts +78 -0
- package/src/components/TicketConversation/hooks/useAI.ts +29 -5
- package/src/endpoints/ai.ts +13 -8
- package/src/endpoints/billing.ts +70 -9
- package/src/endpoints/index.ts +3 -0
- package/src/endpoints/ticket-synthesis.ts +67 -0
- package/src/styles/BillingView.module.scss +132 -0
- package/src/utils/generateTicketSynthesis.ts +178 -0
- package/src/views/BillingView/client.tsx +234 -102
- package/src/views/TicketDetailView/client.tsx +24 -6
|
@@ -50,6 +50,9 @@ const BillingClient = () => {
|
|
|
50
50
|
const [projects, setProjects] = useState([]);
|
|
51
51
|
const [projectsLoaded, setProjectsLoaded] = useState(false);
|
|
52
52
|
const [copied, setCopied] = useState(false);
|
|
53
|
+
const [hideEmpty, setHideEmpty] = useState(false);
|
|
54
|
+
const [expandedSummaries, setExpandedSummaries] = useState(/* @__PURE__ */ new Set());
|
|
55
|
+
const [regeneratingIds, setRegeneratingIds] = useState(/* @__PURE__ */ new Set());
|
|
53
56
|
const [billedTickets, setBilledTickets] = useState(() => {
|
|
54
57
|
if (typeof window === "undefined") return /* @__PURE__ */ new Set();
|
|
55
58
|
try {
|
|
@@ -68,16 +71,29 @@ const BillingClient = () => {
|
|
|
68
71
|
return next;
|
|
69
72
|
});
|
|
70
73
|
}, []);
|
|
71
|
-
const
|
|
74
|
+
const toggleSummary = useCallback((ticketId) => {
|
|
75
|
+
setExpandedSummaries((prev) => {
|
|
76
|
+
const next = new Set(prev);
|
|
77
|
+
if (next.has(ticketId)) next.delete(ticketId);
|
|
78
|
+
else next.add(ticketId);
|
|
79
|
+
return next;
|
|
80
|
+
});
|
|
81
|
+
}, []);
|
|
82
|
+
const visibleGroups = React.useMemo(() => {
|
|
83
|
+
if (!data) return [];
|
|
84
|
+
if (!hideEmpty) return data.groups;
|
|
85
|
+
return data.groups.map((g) => ({ ...g, tickets: g.tickets.filter((t2) => !t2.hasNoTimeEntries) })).filter((g) => g.tickets.length > 0);
|
|
86
|
+
}, [data, hideEmpty]);
|
|
87
|
+
const allTicketIds = visibleGroups.flatMap((g) => g.tickets.map((t2) => t2.id));
|
|
72
88
|
const allBilled = allTicketIds.length > 0 && allTicketIds.every((id) => billedTickets.has(id));
|
|
73
89
|
const toggleAll = useCallback(() => {
|
|
74
90
|
setBilledTickets((prev) => {
|
|
75
|
-
const ids =
|
|
91
|
+
const ids = visibleGroups.flatMap((g) => g.tickets.map((t2) => t2.id));
|
|
76
92
|
const next = ids.every((id) => prev.has(id)) ? /* @__PURE__ */ new Set() : new Set(ids);
|
|
77
93
|
localStorage.setItem("billing-checked-tickets", JSON.stringify([...next]));
|
|
78
94
|
return next;
|
|
79
95
|
});
|
|
80
|
-
}, [
|
|
96
|
+
}, [visibleGroups]);
|
|
81
97
|
const loadProjects = useCallback(async () => {
|
|
82
98
|
if (projectsLoaded) return;
|
|
83
99
|
try {
|
|
@@ -111,24 +127,65 @@ const BillingClient = () => {
|
|
|
111
127
|
setFrom(range.from);
|
|
112
128
|
setTo(range.to);
|
|
113
129
|
};
|
|
130
|
+
const requestSynthesis = useCallback(async (ticketId, force) => {
|
|
131
|
+
setRegeneratingIds((prev) => new Set(prev).add(ticketId));
|
|
132
|
+
try {
|
|
133
|
+
const params = new URLSearchParams({ ticketId: String(ticketId) });
|
|
134
|
+
if (force) params.set("force", "true");
|
|
135
|
+
const res = await fetch(`/api/support/ticket-synthesis?${params}`, { method: "POST" });
|
|
136
|
+
if (res.ok) {
|
|
137
|
+
const json = await res.json();
|
|
138
|
+
setData((prev) => {
|
|
139
|
+
if (!prev) return prev;
|
|
140
|
+
return {
|
|
141
|
+
...prev,
|
|
142
|
+
groups: prev.groups.map((g) => ({
|
|
143
|
+
...g,
|
|
144
|
+
tickets: g.tickets.map(
|
|
145
|
+
(t2) => t2.id === ticketId ? { ...t2, aiSummary: json.summary, aiSummaryGeneratedAt: json.generatedAt, aiSummaryStatus: "done" } : t2
|
|
146
|
+
)
|
|
147
|
+
}))
|
|
148
|
+
};
|
|
149
|
+
});
|
|
150
|
+
setExpandedSummaries((prev) => new Set(prev).add(ticketId));
|
|
151
|
+
}
|
|
152
|
+
} catch (err) {
|
|
153
|
+
console.error("[billing] Synthesis error:", err);
|
|
154
|
+
} finally {
|
|
155
|
+
setRegeneratingIds((prev) => {
|
|
156
|
+
const next = new Set(prev);
|
|
157
|
+
next.delete(ticketId);
|
|
158
|
+
return next;
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}, []);
|
|
114
162
|
const copyRecap = useCallback(() => {
|
|
115
163
|
if (!data) return;
|
|
116
164
|
const lines = [];
|
|
117
165
|
lines.push(`PRE-FACTURATION \u2014 Du ${from} au ${to}`);
|
|
118
166
|
lines.push(`Taux horaire : ${rate} EUR/h`);
|
|
119
167
|
lines.push("=".repeat(50));
|
|
120
|
-
for (const group of
|
|
168
|
+
for (const group of visibleGroups) {
|
|
121
169
|
lines.push("");
|
|
122
170
|
lines.push(`PROJET : ${group.project?.name || "Sans projet"}`);
|
|
123
171
|
if (group.client?.company) lines.push(`Client : ${group.client.company}`);
|
|
124
172
|
lines.push("-".repeat(40));
|
|
125
173
|
for (const ticket of group.tickets) {
|
|
126
|
-
|
|
174
|
+
const flag = ticket.hasNoTimeEntries ? " [AUCUN TEMPS SAISI]" : "";
|
|
175
|
+
lines.push(` ${ticket.ticketNumber} \u2014 ${ticket.subject}${flag}`);
|
|
127
176
|
for (const entry of ticket.entries) {
|
|
128
177
|
lines.push(` ${entry.date} | ${formatDuration(entry.duration)} | ${entry.description || "-"}`);
|
|
129
178
|
}
|
|
130
|
-
|
|
131
|
-
|
|
179
|
+
if (ticket.entries.length > 0) {
|
|
180
|
+
const ticketAmount = ticket.billedAmount || Number(formatAmount(ticket.totalMinutes, rate));
|
|
181
|
+
lines.push(` Sous-total : ${formatDuration(ticket.totalMinutes)} = ${ticketAmount.toFixed(2)} EUR${ticket.billedAmount ? " (forfait)" : ""}`);
|
|
182
|
+
}
|
|
183
|
+
if (ticket.aiSummary) {
|
|
184
|
+
lines.push(" Detail des actions :");
|
|
185
|
+
for (const detailLine of ticket.aiSummary.split("\n")) {
|
|
186
|
+
lines.push(` ${detailLine}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
132
189
|
}
|
|
133
190
|
const groupAmount = group.totalBilledAmount > 0 ? group.totalBilledAmount : Number(formatAmount(group.totalMinutes, rate));
|
|
134
191
|
lines.push(` Total projet : ${formatDuration(group.totalMinutes)} = ${groupAmount.toFixed(2)} EUR`);
|
|
@@ -139,8 +196,13 @@ const BillingClient = () => {
|
|
|
139
196
|
navigator.clipboard.writeText(lines.join("\n"));
|
|
140
197
|
setCopied(true);
|
|
141
198
|
setTimeout(() => setCopied(false), 2e3);
|
|
142
|
-
}, [data, from, to, rate]);
|
|
143
|
-
const
|
|
199
|
+
}, [data, visibleGroups, from, to, rate]);
|
|
200
|
+
const copyTicketSummary = useCallback((ticket) => {
|
|
201
|
+
const lines = [`${ticket.ticketNumber} \u2014 ${ticket.subject}`];
|
|
202
|
+
if (ticket.aiSummary) lines.push("", ticket.aiSummary);
|
|
203
|
+
navigator.clipboard.writeText(lines.join("\n"));
|
|
204
|
+
}, []);
|
|
205
|
+
const totalTickets = visibleGroups.reduce((sum, g) => sum + g.tickets.length, 0);
|
|
144
206
|
return /* @__PURE__ */ jsxs("div", { className: styles.page, children: [
|
|
145
207
|
/* @__PURE__ */ jsx("div", { className: styles.header, children: /* @__PURE__ */ jsxs("div", { children: [
|
|
146
208
|
/* @__PURE__ */ jsx("h1", { className: styles.title, children: t("billing.title") }),
|
|
@@ -164,18 +226,10 @@ const BillingClient = () => {
|
|
|
164
226
|
] }),
|
|
165
227
|
/* @__PURE__ */ jsxs("div", { className: styles.fieldGroup, children: [
|
|
166
228
|
/* @__PURE__ */ jsx("label", { className: styles.label, children: t("billing.filters.project") }),
|
|
167
|
-
/* @__PURE__ */ jsxs(
|
|
168
|
-
"
|
|
169
|
-
{
|
|
170
|
-
|
|
171
|
-
onChange: (e) => setProjectId(e.target.value),
|
|
172
|
-
className: styles.select,
|
|
173
|
-
children: [
|
|
174
|
-
/* @__PURE__ */ jsx("option", { value: "", children: t("ticket.allProjects") }),
|
|
175
|
-
projects.map((p) => /* @__PURE__ */ jsx("option", { value: p.id, children: p.name }, p.id))
|
|
176
|
-
]
|
|
177
|
-
}
|
|
178
|
-
)
|
|
229
|
+
/* @__PURE__ */ jsxs("select", { value: projectId, onChange: (e) => setProjectId(e.target.value), className: styles.select, children: [
|
|
230
|
+
/* @__PURE__ */ jsx("option", { value: "", children: t("ticket.allProjects") }),
|
|
231
|
+
projects.map((p) => /* @__PURE__ */ jsx("option", { value: p.id, children: p.name }, p.id))
|
|
232
|
+
] })
|
|
179
233
|
] }),
|
|
180
234
|
/* @__PURE__ */ jsxs("div", { className: styles.fieldGroup, children: [
|
|
181
235
|
/* @__PURE__ */ jsx("label", { className: styles.label, children: t("billing.filters.hourlyRate") }),
|
|
@@ -193,167 +247,230 @@ const BillingClient = () => {
|
|
|
193
247
|
/* @__PURE__ */ jsx("span", { className: styles.rateUnit, children: t("billing.filters.rateUnit") })
|
|
194
248
|
] })
|
|
195
249
|
] }),
|
|
196
|
-
/* @__PURE__ */ jsx(
|
|
197
|
-
"button",
|
|
198
|
-
{
|
|
199
|
-
className: styles.btnPrimary,
|
|
200
|
-
onClick: fetchBilling,
|
|
201
|
-
disabled: loading,
|
|
202
|
-
children: loading ? t("billing.filters.loading") : t("billing.filters.load")
|
|
203
|
-
}
|
|
204
|
-
)
|
|
250
|
+
/* @__PURE__ */ jsx("button", { className: styles.btnPrimary, onClick: fetchBilling, disabled: loading, children: loading ? t("billing.filters.loading") : t("billing.filters.load") })
|
|
205
251
|
] })
|
|
206
252
|
] }),
|
|
207
|
-
data && /* @__PURE__ */
|
|
208
|
-
data.
|
|
209
|
-
/* @__PURE__ */ jsxs("
|
|
210
|
-
/* @__PURE__ */
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
253
|
+
data && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
254
|
+
data.ticketsWithoutTime > 0 && /* @__PURE__ */ jsxs("div", { className: styles.warningBanner, children: [
|
|
255
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
256
|
+
/* @__PURE__ */ jsx("strong", { children: data.ticketsWithoutTime }),
|
|
257
|
+
" ticket",
|
|
258
|
+
data.ticketsWithoutTime > 1 ? "s" : "",
|
|
259
|
+
" actif",
|
|
260
|
+
data.ticketsWithoutTime > 1 ? "s" : "",
|
|
261
|
+
" sans temps saisi sur la periode."
|
|
262
|
+
] }),
|
|
263
|
+
/* @__PURE__ */ jsxs("label", { className: styles.toggleLabel, children: [
|
|
264
|
+
/* @__PURE__ */ jsx("input", { type: "checkbox", checked: hideEmpty, onChange: (e) => setHideEmpty(e.target.checked) }),
|
|
265
|
+
/* @__PURE__ */ jsx("span", { children: "Masquer ces tickets" })
|
|
266
|
+
] })
|
|
267
|
+
] }),
|
|
268
|
+
visibleGroups.length === 0 ? /* @__PURE__ */ jsx("div", { className: styles.empty, children: t("billing.empty") }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
269
|
+
visibleGroups.map((group, gi) => /* @__PURE__ */ jsxs("div", { className: styles.groupCard, children: [
|
|
270
|
+
/* @__PURE__ */ jsxs("div", { className: styles.groupHeader, children: [
|
|
271
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
272
|
+
/* @__PURE__ */ jsx("span", { className: styles.groupName, children: group.project?.name || "Sans projet" }),
|
|
273
|
+
group.client?.company && /* @__PURE__ */ jsxs("span", { className: styles.groupClient, children: [
|
|
274
|
+
"\u2014 ",
|
|
275
|
+
group.client.company
|
|
276
|
+
] })
|
|
277
|
+
] }),
|
|
278
|
+
/* @__PURE__ */ jsxs("div", { className: styles.groupTotals, children: [
|
|
279
|
+
/* @__PURE__ */ jsx("div", { className: styles.groupDuration, children: formatDuration(group.totalMinutes) }),
|
|
280
|
+
group.totalBilledAmount > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
281
|
+
/* @__PURE__ */ jsxs("div", { className: styles.groupAmountBilled, children: [
|
|
282
|
+
group.totalBilledAmount.toFixed(2),
|
|
283
|
+
" EUR facture"
|
|
284
|
+
] }),
|
|
285
|
+
/* @__PURE__ */ jsxs("div", { className: styles.groupAmountStrike, children: [
|
|
286
|
+
formatAmount(group.totalMinutes, rate),
|
|
287
|
+
" EUR (temps)"
|
|
288
|
+
] })
|
|
289
|
+
] }) : /* @__PURE__ */ jsxs("div", { className: styles.groupAmount, children: [
|
|
225
290
|
formatAmount(group.totalMinutes, rate),
|
|
226
|
-
" EUR
|
|
291
|
+
" EUR"
|
|
227
292
|
] })
|
|
228
|
-
] }) : /* @__PURE__ */ jsxs("div", { className: styles.groupAmount, children: [
|
|
229
|
-
formatAmount(group.totalMinutes, rate),
|
|
230
|
-
" EUR"
|
|
231
293
|
] })
|
|
232
|
-
] })
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
{
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
"a",
|
|
283
|
-
{
|
|
284
|
-
href: `/admin/support/ticket?id=${ticket.id}`,
|
|
285
|
-
className: styles.ticketLink,
|
|
286
|
-
children: ticket.ticketNumber
|
|
287
|
-
}
|
|
288
|
-
) }),
|
|
289
|
-
/* @__PURE__ */ jsx("td", { className: `${styles.tdLeft} ${isBilled ? styles.strikethrough : ""}`, rowSpan: ticket.entries.length, children: ticket.subject })
|
|
290
|
-
] }) : null,
|
|
291
|
-
/* @__PURE__ */ jsx("td", { className: styles.td, children: entry.date }),
|
|
292
|
-
/* @__PURE__ */ jsx("td", { className: styles.td, children: formatDuration(entry.duration) }),
|
|
293
|
-
/* @__PURE__ */ jsx("td", { className: `${styles.tdLeft} ${styles.secondary}`, children: entry.description || "-" }),
|
|
294
|
-
/* @__PURE__ */ jsxs("td", { className: `${styles.td} ${styles.bold}`, children: [
|
|
295
|
-
formatAmount(entry.duration, rate),
|
|
296
|
-
" EUR"
|
|
297
|
-
] }),
|
|
298
|
-
ei === 0 ? /* @__PURE__ */ jsx(
|
|
299
|
-
"td",
|
|
294
|
+
] }),
|
|
295
|
+
/* @__PURE__ */ jsxs("table", { className: styles.table, children: [
|
|
296
|
+
/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
297
|
+
/* @__PURE__ */ jsx("th", { className: styles.thCheckbox, children: /* @__PURE__ */ jsx(
|
|
298
|
+
"input",
|
|
299
|
+
{
|
|
300
|
+
type: "checkbox",
|
|
301
|
+
checked: group.tickets.every((t2) => billedTickets.has(t2.id)),
|
|
302
|
+
onChange: () => {
|
|
303
|
+
const ids = group.tickets.map((t2) => t2.id);
|
|
304
|
+
const allChecked = ids.every((id) => billedTickets.has(id));
|
|
305
|
+
setBilledTickets((prev) => {
|
|
306
|
+
const next = new Set(prev);
|
|
307
|
+
ids.forEach((id) => allChecked ? next.delete(id) : next.add(id));
|
|
308
|
+
localStorage.setItem("billing-checked-tickets", JSON.stringify([...next]));
|
|
309
|
+
return next;
|
|
310
|
+
});
|
|
311
|
+
},
|
|
312
|
+
className: styles.checkbox,
|
|
313
|
+
title: "Tout cocher/decocher"
|
|
314
|
+
}
|
|
315
|
+
) }),
|
|
316
|
+
/* @__PURE__ */ jsx("th", { className: styles.th, children: "N\xB0 Ticket" }),
|
|
317
|
+
/* @__PURE__ */ jsx("th", { className: styles.thLeft, children: "Sujet" }),
|
|
318
|
+
/* @__PURE__ */ jsx("th", { className: styles.th, children: "Date" }),
|
|
319
|
+
/* @__PURE__ */ jsx("th", { className: styles.th, children: "Duree" }),
|
|
320
|
+
/* @__PURE__ */ jsx("th", { className: styles.thLeft, children: "Description" }),
|
|
321
|
+
/* @__PURE__ */ jsx("th", { className: styles.th, children: "Montant" }),
|
|
322
|
+
/* @__PURE__ */ jsx("th", { className: styles.th, children: "Facture" })
|
|
323
|
+
] }) }),
|
|
324
|
+
/* @__PURE__ */ jsx("tbody", { children: group.tickets.map((ticket) => {
|
|
325
|
+
const isBilled = billedTickets.has(ticket.id);
|
|
326
|
+
const isExpanded = expandedSummaries.has(ticket.id);
|
|
327
|
+
const isRegenerating = regeneratingIds.has(ticket.id);
|
|
328
|
+
const rowSpan = Math.max(ticket.entries.length, 1);
|
|
329
|
+
const renderTicketHeaderCells = () => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
330
|
+
/* @__PURE__ */ jsx("td", { className: styles.td, rowSpan, style: { verticalAlign: "middle" }, children: /* @__PURE__ */ jsx(
|
|
331
|
+
"input",
|
|
332
|
+
{
|
|
333
|
+
type: "checkbox",
|
|
334
|
+
checked: isBilled,
|
|
335
|
+
onChange: () => toggleBilled(ticket.id),
|
|
336
|
+
className: styles.checkbox,
|
|
337
|
+
title: isBilled ? "Marquer comme non facture" : "Marquer comme facture"
|
|
338
|
+
}
|
|
339
|
+
) }),
|
|
340
|
+
/* @__PURE__ */ jsxs("td", { className: `${styles.td} ${styles.bold} ${isBilled ? styles.strikethrough : ""}`, rowSpan, children: [
|
|
341
|
+
/* @__PURE__ */ jsx("a", { href: `/admin/support/ticket?id=${ticket.id}`, className: styles.ticketLink, children: ticket.ticketNumber }),
|
|
342
|
+
/* @__PURE__ */ jsxs(
|
|
343
|
+
"button",
|
|
300
344
|
{
|
|
301
|
-
className:
|
|
302
|
-
|
|
303
|
-
|
|
345
|
+
className: styles.summaryBtn,
|
|
346
|
+
onClick: () => toggleSummary(ticket.id),
|
|
347
|
+
title: isExpanded ? "Masquer le detail" : "Afficher le detail IA",
|
|
348
|
+
children: [
|
|
349
|
+
isExpanded ? "\u25BC" : "\u25B6",
|
|
350
|
+
" IA"
|
|
351
|
+
]
|
|
304
352
|
}
|
|
305
|
-
)
|
|
306
|
-
]
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
353
|
+
)
|
|
354
|
+
] }),
|
|
355
|
+
/* @__PURE__ */ jsxs("td", { className: `${styles.tdLeft} ${isBilled ? styles.strikethrough : ""}`, rowSpan, children: [
|
|
356
|
+
ticket.subject,
|
|
357
|
+
ticket.hasNoTimeEntries && /* @__PURE__ */ jsx("span", { className: styles.noTimeBadge, title: "Aucun temps saisi sur la periode", children: "\u26A0 Aucun temps saisi" })
|
|
358
|
+
] })
|
|
359
|
+
] });
|
|
360
|
+
const rows = [];
|
|
361
|
+
if (ticket.entries.length === 0) {
|
|
362
|
+
rows.push(
|
|
363
|
+
/* @__PURE__ */ jsxs(
|
|
364
|
+
"tr",
|
|
365
|
+
{
|
|
366
|
+
className: `${isBilled ? styles.tableRowBilled : styles.tableRowNoTime}`,
|
|
367
|
+
children: [
|
|
368
|
+
renderTicketHeaderCells(),
|
|
369
|
+
/* @__PURE__ */ jsx("td", { className: `${styles.td} ${styles.secondary}`, colSpan: 5, children: /* @__PURE__ */ jsx("em", { children: "Pas de saisie de temps. Verifier si du temps a ete oublie." }) })
|
|
370
|
+
]
|
|
371
|
+
},
|
|
372
|
+
`${ticket.id}-empty`
|
|
373
|
+
)
|
|
374
|
+
);
|
|
375
|
+
} else {
|
|
376
|
+
ticket.entries.forEach((entry, ei) => {
|
|
377
|
+
rows.push(
|
|
378
|
+
/* @__PURE__ */ jsxs(
|
|
379
|
+
"tr",
|
|
380
|
+
{
|
|
381
|
+
className: `${isBilled ? styles.tableRowBilled : styles.tableRow} ${!isBilled && ei % 2 === 0 ? styles.tableRowEven : ""} ${!isBilled && ei % 2 !== 0 ? styles.tableRowOdd : ""}`,
|
|
382
|
+
children: [
|
|
383
|
+
ei === 0 ? renderTicketHeaderCells() : null,
|
|
384
|
+
/* @__PURE__ */ jsx("td", { className: styles.td, children: entry.date }),
|
|
385
|
+
/* @__PURE__ */ jsx("td", { className: styles.td, children: formatDuration(entry.duration) }),
|
|
386
|
+
/* @__PURE__ */ jsx("td", { className: `${styles.tdLeft} ${styles.secondary}`, children: entry.description || "-" }),
|
|
387
|
+
/* @__PURE__ */ jsxs("td", { className: `${styles.td} ${styles.bold}`, children: [
|
|
388
|
+
formatAmount(entry.duration, rate),
|
|
389
|
+
" EUR"
|
|
390
|
+
] }),
|
|
391
|
+
ei === 0 ? /* @__PURE__ */ jsx(
|
|
392
|
+
"td",
|
|
393
|
+
{
|
|
394
|
+
className: `${styles.td} ${ticket.billedAmount ? styles.billedAmount : styles.secondary}`,
|
|
395
|
+
rowSpan: ticket.entries.length,
|
|
396
|
+
children: ticket.billedAmount ? `${ticket.billedAmount.toFixed(2)} EUR` : "-"
|
|
397
|
+
}
|
|
398
|
+
) : null
|
|
399
|
+
]
|
|
400
|
+
},
|
|
401
|
+
`${ticket.id}-${ei}`
|
|
402
|
+
)
|
|
403
|
+
);
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
if (isExpanded) {
|
|
407
|
+
rows.push(
|
|
408
|
+
/* @__PURE__ */ jsx("tr", { className: styles.summaryRow, children: /* @__PURE__ */ jsxs("td", { colSpan: 8, className: styles.summaryCell, children: [
|
|
409
|
+
/* @__PURE__ */ jsxs("div", { className: styles.summaryHeader, children: [
|
|
410
|
+
/* @__PURE__ */ jsx("strong", { children: "Synthese IA des actions" }),
|
|
411
|
+
/* @__PURE__ */ jsxs("div", { className: styles.summaryActions, children: [
|
|
412
|
+
ticket.aiSummaryGeneratedAt && /* @__PURE__ */ jsxs("span", { className: styles.summaryMeta, children: [
|
|
413
|
+
"Genere le ",
|
|
414
|
+
new Date(ticket.aiSummaryGeneratedAt).toLocaleString("fr-FR")
|
|
415
|
+
] }),
|
|
416
|
+
ticket.aiSummary && /* @__PURE__ */ jsx(
|
|
417
|
+
"button",
|
|
418
|
+
{
|
|
419
|
+
className: styles.summaryAction,
|
|
420
|
+
onClick: () => copyTicketSummary(ticket),
|
|
421
|
+
children: "Copier"
|
|
422
|
+
}
|
|
423
|
+
),
|
|
424
|
+
/* @__PURE__ */ jsx(
|
|
425
|
+
"button",
|
|
426
|
+
{
|
|
427
|
+
className: styles.summaryAction,
|
|
428
|
+
onClick: () => requestSynthesis(ticket.id, !!ticket.aiSummary),
|
|
429
|
+
disabled: isRegenerating,
|
|
430
|
+
children: isRegenerating ? "Generation..." : ticket.aiSummary ? "Regenerer" : "Generer"
|
|
431
|
+
}
|
|
432
|
+
)
|
|
433
|
+
] })
|
|
434
|
+
] }),
|
|
435
|
+
ticket.aiSummaryStatus === "pending" && !ticket.aiSummary ? /* @__PURE__ */ jsx("div", { className: styles.summaryEmpty, children: 'Generation en cours en arriere-plan. Cliquer sur "Generer" pour forcer.' }) : ticket.aiSummary ? /* @__PURE__ */ jsx("pre", { className: styles.summaryText, children: ticket.aiSummary }) : /* @__PURE__ */ jsx("div", { className: styles.summaryEmpty, children: 'Pas de synthese disponible. La synthese est generee automatiquement quand le ticket passe en "resolu", ou manuellement via le bouton "Generer".' })
|
|
436
|
+
] }) }, `${ticket.id}-summary`)
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
return rows;
|
|
440
|
+
}) })
|
|
441
|
+
] })
|
|
442
|
+
] }, gi)),
|
|
443
|
+
/* @__PURE__ */ jsxs("div", { className: styles.grandTotal, children: [
|
|
444
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
445
|
+
/* @__PURE__ */ jsxs("div", { className: styles.totalMeta, children: [
|
|
446
|
+
totalTickets,
|
|
447
|
+
" ticket",
|
|
448
|
+
totalTickets > 1 ? "s" : "",
|
|
449
|
+
" affiche",
|
|
450
|
+
totalTickets > 1 ? "s" : "",
|
|
451
|
+
billedTickets.size > 0 && /* @__PURE__ */ jsxs("span", { className: styles.totalChecked, children: [
|
|
452
|
+
"(",
|
|
453
|
+
allTicketIds.filter((id) => billedTickets.has(id)).length,
|
|
454
|
+
" coche",
|
|
455
|
+
allTicketIds.filter((id) => billedTickets.has(id)).length > 1 ? "s" : "",
|
|
456
|
+
")"
|
|
457
|
+
] })
|
|
458
|
+
] }),
|
|
459
|
+
/* @__PURE__ */ jsxs("div", { className: styles.totalAmount, children: [
|
|
460
|
+
"Total : ",
|
|
461
|
+
formatDuration(data.grandTotalMinutes),
|
|
462
|
+
" =",
|
|
463
|
+
" ",
|
|
464
|
+
data.grandTotalBilledAmount > 0 ? `${data.grandTotalBilledAmount.toFixed(2)} EUR` : `${formatAmount(data.grandTotalMinutes, rate)} EUR`
|
|
327
465
|
] })
|
|
328
466
|
] }),
|
|
329
|
-
/* @__PURE__ */ jsxs("div", { className: styles.
|
|
330
|
-
"
|
|
331
|
-
|
|
332
|
-
" =",
|
|
333
|
-
" ",
|
|
334
|
-
data.grandTotalBilledAmount > 0 ? `${data.grandTotalBilledAmount.toFixed(2)} EUR` : `${formatAmount(data.grandTotalMinutes, rate)} EUR`
|
|
467
|
+
/* @__PURE__ */ jsxs("div", { className: styles.totalActions, children: [
|
|
468
|
+
/* @__PURE__ */ jsx("button", { className: allBilled ? styles.btnSecondary : styles.btnGreen, onClick: toggleAll, children: allBilled ? t("billing.totals.uncheckAll") : t("billing.totals.checkAll") }),
|
|
469
|
+
/* @__PURE__ */ jsx("button", { className: copied ? styles.btnSuccess : styles.btnAmber, onClick: copyRecap, children: copied ? t("billing.totals.copiedRecap") : t("billing.totals.copyRecap") })
|
|
335
470
|
] })
|
|
336
|
-
] }),
|
|
337
|
-
/* @__PURE__ */ jsxs("div", { className: styles.totalActions, children: [
|
|
338
|
-
/* @__PURE__ */ jsx(
|
|
339
|
-
"button",
|
|
340
|
-
{
|
|
341
|
-
className: allBilled ? styles.btnSecondary : styles.btnGreen,
|
|
342
|
-
onClick: toggleAll,
|
|
343
|
-
children: allBilled ? t("billing.totals.uncheckAll") : t("billing.totals.checkAll")
|
|
344
|
-
}
|
|
345
|
-
),
|
|
346
|
-
/* @__PURE__ */ jsx(
|
|
347
|
-
"button",
|
|
348
|
-
{
|
|
349
|
-
className: copied ? styles.btnSuccess : styles.btnAmber,
|
|
350
|
-
onClick: copyRecap,
|
|
351
|
-
children: copied ? t("billing.totals.copiedRecap") : t("billing.totals.copyRecap")
|
|
352
|
-
}
|
|
353
|
-
)
|
|
354
471
|
] })
|
|
355
472
|
] })
|
|
356
|
-
] })
|
|
473
|
+
] })
|
|
357
474
|
] });
|
|
358
475
|
};
|
|
359
476
|
|
|
@@ -141,7 +141,9 @@ const TicketDetailClient = () => {
|
|
|
141
141
|
const [sendAsClient, setSendAsClient] = React.useState(false);
|
|
142
142
|
const [editingMsgId, setEditingMsgId] = React.useState(null);
|
|
143
143
|
const [editingBody, setEditingBody] = React.useState("");
|
|
144
|
+
const [editingHtml, setEditingHtml] = React.useState("");
|
|
144
145
|
const [editSaving, setEditSaving] = React.useState(false);
|
|
146
|
+
const editEditorRef = React.useRef(null);
|
|
145
147
|
const [sending, setSending] = React.useState(false);
|
|
146
148
|
const [showMenu, setShowMenu] = React.useState(false);
|
|
147
149
|
const [clientTyping, setClientTyping] = React.useState(false);
|
|
@@ -483,10 +485,12 @@ ${uploadedLinks.join("\n")}` : replyBody.trim() || "[Contenu enrichi]";
|
|
|
483
485
|
const startEditMessage = (msg) => {
|
|
484
486
|
setEditingMsgId(msg.id);
|
|
485
487
|
setEditingBody(msg.body || "");
|
|
488
|
+
setEditingHtml(msg.bodyHtml || (msg.body || "").replace(/\n/g, "<br/>"));
|
|
486
489
|
};
|
|
487
490
|
const cancelEditMessage = () => {
|
|
488
491
|
setEditingMsgId(null);
|
|
489
492
|
setEditingBody("");
|
|
493
|
+
setEditingHtml("");
|
|
490
494
|
};
|
|
491
495
|
const saveEditMessage = async () => {
|
|
492
496
|
if (editingMsgId === null) return;
|
|
@@ -496,11 +500,12 @@ ${uploadedLinks.join("\n")}` : replyBody.trim() || "[Contenu enrichi]";
|
|
|
496
500
|
method: "PATCH",
|
|
497
501
|
headers: { "Content-Type": "application/json" },
|
|
498
502
|
credentials: "include",
|
|
499
|
-
body: JSON.stringify({ body: editingBody, bodyHtml: null, skipNotification: true })
|
|
503
|
+
body: JSON.stringify({ body: editingBody, bodyHtml: editingHtml || null, skipNotification: true })
|
|
500
504
|
});
|
|
501
505
|
if (res.ok) {
|
|
502
506
|
setEditingMsgId(null);
|
|
503
507
|
setEditingBody("");
|
|
508
|
+
setEditingHtml("");
|
|
504
509
|
fetchAll();
|
|
505
510
|
}
|
|
506
511
|
} catch {
|
|
@@ -719,12 +724,29 @@ ${uploadedLinks.join("\n")}` : replyBody.trim() || "[Contenu enrichi]";
|
|
|
719
724
|
] }),
|
|
720
725
|
editingMsgId === msg.id ? /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 6 }, children: [
|
|
721
726
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
722
|
-
|
|
727
|
+
index.RichTextEditor,
|
|
723
728
|
{
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
729
|
+
ref: editEditorRef,
|
|
730
|
+
initialValue: editingHtml,
|
|
731
|
+
onChange: (html, text) => {
|
|
732
|
+
setEditingHtml(html);
|
|
733
|
+
setEditingBody(text);
|
|
734
|
+
},
|
|
735
|
+
placeholder: "\xC9diter le message...",
|
|
736
|
+
minHeight: 120,
|
|
737
|
+
onFileUpload: async (file) => {
|
|
738
|
+
try {
|
|
739
|
+
const formData = new FormData();
|
|
740
|
+
formData.append("file", file);
|
|
741
|
+
formData.append("_payload", JSON.stringify({ alt: file.name }));
|
|
742
|
+
const ur = await fetch("/api/media", { method: "POST", credentials: "include", body: formData });
|
|
743
|
+
if (!ur.ok) return null;
|
|
744
|
+
const ud = await ur.json();
|
|
745
|
+
return ud.doc?.url || null;
|
|
746
|
+
} catch {
|
|
747
|
+
return null;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
728
750
|
}
|
|
729
751
|
),
|
|
730
752
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: 6, justifyContent: "flex-end" }, children: [
|