@consilioweb/payload-support 0.9.12 → 0.10.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.
- package/dist/components/TicketConversation/locales/en.json +8 -0
- package/dist/components/TicketConversation/locales/fr.json +9 -1
- package/dist/index.cjs +1 -0
- package/dist/index.d.cts +18 -1
- package/dist/index.d.ts +18 -1
- package/dist/index.js +1 -1
- package/dist/styles/TicketDetail.module.scss +270 -20
- package/dist/styles/TicketInbox.module.scss +112 -50
- package/dist/views/TicketDetailView/client.cjs +80 -13
- package/dist/views/TicketDetailView/client.js +80 -13
- package/dist/views/TicketInboxView/client.cjs +116 -58
- package/dist/views/TicketInboxView/client.js +116 -58
- package/dist/views/shared/StatusPill.cjs +36 -0
- package/dist/views/shared/StatusPill.js +30 -0
- package/dist/views/shared/StatusPill.module.scss +66 -0
- package/dist/views/shared/index.cjs +14 -0
- package/dist/views/shared/index.js +2 -0
- package/dist/views/shared/sla.cjs +37 -0
- package/dist/views/shared/sla.js +34 -0
- package/package.json +1 -1
- package/src/components/TicketConversation/locales/en.json +8 -0
- package/src/components/TicketConversation/locales/fr.json +9 -1
- package/src/index.ts +2 -0
- package/src/styles/TicketDetail.module.scss +270 -20
- package/src/styles/TicketInbox.module.scss +112 -50
- package/src/views/TicketDetailView/client.tsx +88 -27
- package/src/views/TicketInboxView/client.tsx +106 -50
- package/src/views/shared/StatusPill.module.scss +66 -0
- package/src/views/shared/StatusPill.tsx +44 -0
- package/src/views/shared/index.ts +4 -0
- package/src/views/shared/sla.ts +56 -0
|
@@ -5,6 +5,7 @@ var react = require('react');
|
|
|
5
5
|
var navigation = require('next/navigation');
|
|
6
6
|
var Link = require('next/link');
|
|
7
7
|
var useTranslation = require('../../components/TicketConversation/hooks/useTranslation');
|
|
8
|
+
var shared = require('../shared');
|
|
8
9
|
var s = require('../../styles/TicketInbox.module.scss');
|
|
9
10
|
|
|
10
11
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
@@ -12,28 +13,18 @@ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
|
12
13
|
var Link__default = /*#__PURE__*/_interopDefault(Link);
|
|
13
14
|
var s__default = /*#__PURE__*/_interopDefault(s);
|
|
14
15
|
|
|
15
|
-
const STATUS_DOTS = {
|
|
16
|
-
open: "#22c55e",
|
|
17
|
-
waiting_client: "#eab308",
|
|
18
|
-
resolved: "#94a3b8"
|
|
19
|
-
};
|
|
20
|
-
const STATUS_LABEL_KEYS = {
|
|
21
|
-
open: "inbox.tabs.open",
|
|
22
|
-
waiting_client: "inbox.tabs.waiting",
|
|
23
|
-
resolved: "inbox.tabs.resolved"
|
|
24
|
-
};
|
|
25
16
|
const PRIORITY_COLORS = {
|
|
26
|
-
urgent: "
|
|
27
|
-
high: "
|
|
28
|
-
normal: "
|
|
29
|
-
low: "
|
|
17
|
+
urgent: "var(--theme-error-500)",
|
|
18
|
+
high: "var(--theme-warning-500)",
|
|
19
|
+
normal: "var(--theme-info-500, #2a6bd8)",
|
|
20
|
+
low: "var(--theme-elevation-400)"
|
|
30
21
|
};
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
22
|
+
const STATUS_VARIANT = {
|
|
23
|
+
open: "open",
|
|
24
|
+
waiting_client: "pending",
|
|
25
|
+
pending: "pending",
|
|
26
|
+
resolved: "resolved",
|
|
27
|
+
closed: "closed"
|
|
37
28
|
};
|
|
38
29
|
function relativeTime(dateStr) {
|
|
39
30
|
const diff = Date.now() - new Date(dateStr).getTime();
|
|
@@ -53,12 +44,12 @@ const TicketInboxClient = () => {
|
|
|
53
44
|
const [loading, setLoading] = react.useState(true);
|
|
54
45
|
const [tab, setTab] = react.useState(() => {
|
|
55
46
|
const urlTab = searchParams.get("tab");
|
|
56
|
-
if (urlTab && ["all", "open", "waiting_client", "resolved"].includes(urlTab)) return urlTab;
|
|
47
|
+
if (urlTab && ["all", "open", "waiting_client", "resolved", "sla_breach"].includes(urlTab)) return urlTab;
|
|
57
48
|
return "all";
|
|
58
49
|
});
|
|
59
50
|
react.useEffect(() => {
|
|
60
51
|
const urlTab = searchParams.get("tab");
|
|
61
|
-
if (urlTab && ["all", "open", "waiting_client", "resolved"].includes(urlTab)) {
|
|
52
|
+
if (urlTab && ["all", "open", "waiting_client", "resolved", "sla_breach"].includes(urlTab)) {
|
|
62
53
|
setTab(urlTab);
|
|
63
54
|
} else if (!urlTab) {
|
|
64
55
|
setTab("all");
|
|
@@ -66,7 +57,7 @@ const TicketInboxClient = () => {
|
|
|
66
57
|
}, [searchParams]);
|
|
67
58
|
const [search, setSearch] = react.useState("");
|
|
68
59
|
const [sort, setSort] = react.useState("-updatedAt");
|
|
69
|
-
const [counts, setCounts] = react.useState({ all: 0, open: 0, waiting: 0, resolved: 0 });
|
|
60
|
+
const [counts, setCounts] = react.useState({ all: 0, open: 0, waiting: 0, resolved: 0, breach: 0 });
|
|
70
61
|
const [selectedIdx, setSelectedIdx] = react.useState(-1);
|
|
71
62
|
const [checkedIds, setCheckedIds] = react.useState(/* @__PURE__ */ new Set());
|
|
72
63
|
const [bulkAction, setBulkAction] = react.useState("");
|
|
@@ -99,8 +90,32 @@ const TicketInboxClient = () => {
|
|
|
99
90
|
setBulkProcessing(false);
|
|
100
91
|
};
|
|
101
92
|
const fetchTickets = react.useCallback(async () => {
|
|
102
|
-
const params = [
|
|
103
|
-
|
|
93
|
+
const params = [
|
|
94
|
+
`limit=30`,
|
|
95
|
+
`sort=${sort}`,
|
|
96
|
+
`depth=1`,
|
|
97
|
+
`select[id]=true`,
|
|
98
|
+
`select[ticketNumber]=true`,
|
|
99
|
+
`select[subject]=true`,
|
|
100
|
+
`select[status]=true`,
|
|
101
|
+
`select[priority]=true`,
|
|
102
|
+
`select[category]=true`,
|
|
103
|
+
`select[client]=true`,
|
|
104
|
+
`select[updatedAt]=true`,
|
|
105
|
+
`select[lastClientMessageAt]=true`,
|
|
106
|
+
`select[lastAdminReadAt]=true`,
|
|
107
|
+
`select[slaFirstResponseDue]=true`,
|
|
108
|
+
`select[slaFirstResponseBreached]=true`,
|
|
109
|
+
`select[slaResolutionDue]=true`,
|
|
110
|
+
`select[slaResolutionBreached]=true`,
|
|
111
|
+
`select[firstResponseAt]=true`
|
|
112
|
+
];
|
|
113
|
+
if (tab === "sla_breach") {
|
|
114
|
+
params.push(`where[or][0][slaFirstResponseBreached][equals]=true`);
|
|
115
|
+
params.push(`where[or][1][slaResolutionBreached][equals]=true`);
|
|
116
|
+
} else if (tab !== "all") {
|
|
117
|
+
params.push(`where[status][equals]=${tab}`);
|
|
118
|
+
}
|
|
104
119
|
if (search.trim()) {
|
|
105
120
|
params.push(`where[or][0][subject][contains]=${encodeURIComponent(search)}`);
|
|
106
121
|
params.push(`where[or][1][ticketNumber][contains]=${encodeURIComponent(search)}`);
|
|
@@ -126,14 +141,21 @@ const TicketInboxClient = () => {
|
|
|
126
141
|
react.useEffect(() => {
|
|
127
142
|
const fetchCounts = async () => {
|
|
128
143
|
try {
|
|
129
|
-
const [all, openRes, waiting, resolved] = await Promise.all([
|
|
144
|
+
const [all, openRes, waiting, resolved, breach] = await Promise.all([
|
|
130
145
|
fetch("/api/tickets?limit=0&depth=0", { credentials: "include" }),
|
|
131
146
|
fetch("/api/tickets?limit=0&depth=0&where[status][equals]=open", { credentials: "include" }),
|
|
132
147
|
fetch("/api/tickets?limit=0&depth=0&where[status][equals]=waiting_client", { credentials: "include" }),
|
|
133
|
-
fetch("/api/tickets?limit=0&depth=0&where[status][equals]=resolved", { credentials: "include" })
|
|
148
|
+
fetch("/api/tickets?limit=0&depth=0&where[status][equals]=resolved", { credentials: "include" }),
|
|
149
|
+
fetch("/api/tickets?limit=0&depth=0&where[or][0][slaFirstResponseBreached][equals]=true&where[or][1][slaResolutionBreached][equals]=true", { credentials: "include" })
|
|
134
150
|
]);
|
|
135
|
-
const [a, o, w, r] = await Promise.all([all.json(), openRes.json(), waiting.json(), resolved.json()]);
|
|
136
|
-
setCounts({
|
|
151
|
+
const [a, o, w, r, b] = await Promise.all([all.json(), openRes.json(), waiting.json(), resolved.json(), breach.json()]);
|
|
152
|
+
setCounts({
|
|
153
|
+
all: a.totalDocs || 0,
|
|
154
|
+
open: o.totalDocs || 0,
|
|
155
|
+
waiting: w.totalDocs || 0,
|
|
156
|
+
resolved: r.totalDocs || 0,
|
|
157
|
+
breach: b.totalDocs || 0
|
|
158
|
+
});
|
|
137
159
|
} catch {
|
|
138
160
|
}
|
|
139
161
|
};
|
|
@@ -165,11 +187,22 @@ const TicketInboxClient = () => {
|
|
|
165
187
|
{ key: "all", label: t("inbox.tabs.all"), count: counts.all },
|
|
166
188
|
{ key: "open", label: t("inbox.tabs.open"), count: counts.open },
|
|
167
189
|
{ key: "waiting_client", label: t("inbox.tabs.waiting"), count: counts.waiting },
|
|
190
|
+
{ key: "sla_breach", label: t("inbox.tabs.slaBreach"), count: counts.breach, alert: true },
|
|
168
191
|
{ key: "resolved", label: t("inbox.tabs.resolved"), count: counts.resolved }
|
|
169
192
|
];
|
|
170
193
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.page, children: [
|
|
171
194
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.header, children: [
|
|
172
|
-
/* @__PURE__ */ jsxRuntime.
|
|
195
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
196
|
+
/* @__PURE__ */ jsxRuntime.jsx("h1", { className: s__default.default.title, children: t("inbox.title") }),
|
|
197
|
+
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: s__default.default.subtitle, children: [
|
|
198
|
+
tickets.length > 0 && `${tickets.length} sur ${counts.all} \xB7 `,
|
|
199
|
+
counts.breach > 0 ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: s__default.default.subtitleBreach, children: [
|
|
200
|
+
counts.breach,
|
|
201
|
+
" SLA d\xE9pass\xE9",
|
|
202
|
+
counts.breach > 1 ? "s" : ""
|
|
203
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Aucun SLA en alerte" })
|
|
204
|
+
] })
|
|
205
|
+
] }),
|
|
173
206
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.headerRight, children: [
|
|
174
207
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: s__default.default.searchWrap, children: [
|
|
175
208
|
/* @__PURE__ */ jsxRuntime.jsxs("svg", { className: s__default.default.searchIcon, width: "14", height: "14", viewBox: "0 0 16 16", fill: "none", children: [
|
|
@@ -191,25 +224,26 @@ const TicketInboxClient = () => {
|
|
|
191
224
|
/* @__PURE__ */ jsxRuntime.jsx(Link__default.default, { href: "/admin/support/new-ticket", className: s__default.default.newTicketBtn, children: t("inbox.newTicketBtn") })
|
|
192
225
|
] })
|
|
193
226
|
] }),
|
|
194
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: s__default.default.tabs, children: tabs.map((tk) =>
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
227
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: s__default.default.tabs, role: "tablist", children: tabs.map((tk) => {
|
|
228
|
+
const isAlert = tk.alert && tk.count > 0;
|
|
229
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
230
|
+
"button",
|
|
231
|
+
{
|
|
232
|
+
role: "tab",
|
|
233
|
+
"aria-selected": tab === tk.key,
|
|
234
|
+
className: `${s__default.default.tab} ${tab === tk.key ? s__default.default.tabActive : ""} ${isAlert ? s__default.default.tabAlert : ""}`,
|
|
235
|
+
onClick: () => {
|
|
236
|
+
setTab(tk.key);
|
|
237
|
+
setSelectedIdx(-1);
|
|
238
|
+
},
|
|
239
|
+
children: [
|
|
240
|
+
tk.label,
|
|
241
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: `${s__default.default.tabCount} ${isAlert ? s__default.default.tabCountAlert : ""}`, children: tk.count })
|
|
242
|
+
]
|
|
201
243
|
},
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
"(",
|
|
206
|
-
tk.count,
|
|
207
|
-
")"
|
|
208
|
-
] })
|
|
209
|
-
]
|
|
210
|
-
},
|
|
211
|
-
tk.key
|
|
212
|
-
)) }),
|
|
244
|
+
tk.key
|
|
245
|
+
);
|
|
246
|
+
}) }),
|
|
213
247
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: s__default.default.sortRow, children: checkedIds.size > 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: 8, alignItems: "center", flex: 1 }, children: [
|
|
214
248
|
/* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: 13, fontWeight: 600, color: "var(--theme-text)" }, children: checkedIds.size > 1 ? t("inbox.selectedPlural", { count: String(checkedIds.size) }) : t("inbox.selected", { count: String(checkedIds.size) }) }),
|
|
215
249
|
/* @__PURE__ */ jsxRuntime.jsx("button", { className: s__default.default.sortSelect, onClick: () => handleBulkAction("close"), disabled: bulkProcessing, children: t("inbox.closeAction") }),
|
|
@@ -239,11 +273,19 @@ const TicketInboxClient = () => {
|
|
|
239
273
|
const displayClient = clientName ? `${clientName}${clientCompany ? `, ${clientCompany}` : ""}` : "\u2014";
|
|
240
274
|
const isUnread = tk.lastClientMessageAt && (!tk.lastAdminReadAt || new Date(tk.lastClientMessageAt) > new Date(tk.lastAdminReadAt));
|
|
241
275
|
const priorityColor = PRIORITY_COLORS[tk.priority] || "transparent";
|
|
276
|
+
const sla = shared.computeSlaState(tk);
|
|
277
|
+
const isBreach = sla.state === "breach";
|
|
278
|
+
const statusVariant = STATUS_VARIANT[tk.status] || "open";
|
|
242
279
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
243
280
|
"a",
|
|
244
281
|
{
|
|
245
282
|
href: `/admin/support/ticket?id=${tk.id}`,
|
|
246
|
-
className:
|
|
283
|
+
className: [
|
|
284
|
+
s__default.default.row,
|
|
285
|
+
idx === selectedIdx ? s__default.default.rowSelected : "",
|
|
286
|
+
isUnread ? s__default.default.rowUnread : "",
|
|
287
|
+
isBreach ? s__default.default.rowBreach : ""
|
|
288
|
+
].filter(Boolean).join(" "),
|
|
247
289
|
onClick: (e) => {
|
|
248
290
|
e.preventDefault();
|
|
249
291
|
window.location.href = `/admin/support/ticket?id=${tk.id}`;
|
|
@@ -256,20 +298,36 @@ const TicketInboxClient = () => {
|
|
|
256
298
|
checked: checkedIds.has(tk.id),
|
|
257
299
|
onClick: (e) => e.stopPropagation(),
|
|
258
300
|
onChange: () => toggleCheck(tk.id),
|
|
259
|
-
|
|
301
|
+
className: s__default.default.rowCheck,
|
|
302
|
+
"aria-label": `S\xE9lectionner ${tk.ticketNumber}`
|
|
303
|
+
}
|
|
304
|
+
),
|
|
305
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
306
|
+
"span",
|
|
307
|
+
{
|
|
308
|
+
className: s__default.default.priorityDot,
|
|
309
|
+
style: { background: priorityColor },
|
|
310
|
+
"aria-label": `Priorit\xE9 ${tk.priority}`,
|
|
311
|
+
title: `Priorit\xE9 ${tk.priority}`
|
|
260
312
|
}
|
|
261
313
|
),
|
|
262
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
263
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: s__default.default.statusText, children: t(STATUS_LABEL_KEYS[tk.status] || "ticket.status.open") }),
|
|
314
|
+
/* @__PURE__ */ jsxRuntime.jsx(shared.StatusPill, { status: isBreach ? "sla-breach" : statusVariant }),
|
|
264
315
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: s__default.default.ticketNum, children: tk.ticketNumber }),
|
|
265
316
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: s__default.default.subject, children: tk.subject }),
|
|
266
317
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: s__default.default.client, children: displayClient }),
|
|
267
|
-
|
|
268
|
-
"
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
318
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
319
|
+
"span",
|
|
320
|
+
{
|
|
321
|
+
className: [
|
|
322
|
+
s__default.default.slaCell,
|
|
323
|
+
sla.state === "breach" ? s__default.default.slaBreach : "",
|
|
324
|
+
sla.state === "warn" ? s__default.default.slaWarn : "",
|
|
325
|
+
sla.state === "ok" ? s__default.default.slaOk : ""
|
|
326
|
+
].filter(Boolean).join(" "),
|
|
327
|
+
title: sla.due ? new Date(sla.due).toLocaleString("fr-FR") : "",
|
|
328
|
+
children: shared.formatSlaRemaining(sla.remainingMs)
|
|
329
|
+
}
|
|
330
|
+
),
|
|
273
331
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: s__default.default.timeAgo, children: relativeTime(tk.updatedAt) }),
|
|
274
332
|
isUnread ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: s__default.default.unreadDot }) : /* @__PURE__ */ jsxRuntime.jsx("span", {})
|
|
275
333
|
]
|
|
@@ -4,30 +4,21 @@ import { useState, useEffect, useCallback } from 'react';
|
|
|
4
4
|
import { useSearchParams } from 'next/navigation';
|
|
5
5
|
import Link from 'next/link';
|
|
6
6
|
import { useTranslation } from '../../components/TicketConversation/hooks/useTranslation.js';
|
|
7
|
+
import { computeSlaState, StatusPill, formatSlaRemaining } from '../shared.js';
|
|
7
8
|
import s from '../../styles/TicketInbox.module.scss';
|
|
8
9
|
|
|
9
|
-
const STATUS_DOTS = {
|
|
10
|
-
open: "#22c55e",
|
|
11
|
-
waiting_client: "#eab308",
|
|
12
|
-
resolved: "#94a3b8"
|
|
13
|
-
};
|
|
14
|
-
const STATUS_LABEL_KEYS = {
|
|
15
|
-
open: "inbox.tabs.open",
|
|
16
|
-
waiting_client: "inbox.tabs.waiting",
|
|
17
|
-
resolved: "inbox.tabs.resolved"
|
|
18
|
-
};
|
|
19
10
|
const PRIORITY_COLORS = {
|
|
20
|
-
urgent: "
|
|
21
|
-
high: "
|
|
22
|
-
normal: "
|
|
23
|
-
low: "
|
|
11
|
+
urgent: "var(--theme-error-500)",
|
|
12
|
+
high: "var(--theme-warning-500)",
|
|
13
|
+
normal: "var(--theme-info-500, #2a6bd8)",
|
|
14
|
+
low: "var(--theme-elevation-400)"
|
|
24
15
|
};
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
16
|
+
const STATUS_VARIANT = {
|
|
17
|
+
open: "open",
|
|
18
|
+
waiting_client: "pending",
|
|
19
|
+
pending: "pending",
|
|
20
|
+
resolved: "resolved",
|
|
21
|
+
closed: "closed"
|
|
31
22
|
};
|
|
32
23
|
function relativeTime(dateStr) {
|
|
33
24
|
const diff = Date.now() - new Date(dateStr).getTime();
|
|
@@ -47,12 +38,12 @@ const TicketInboxClient = () => {
|
|
|
47
38
|
const [loading, setLoading] = useState(true);
|
|
48
39
|
const [tab, setTab] = useState(() => {
|
|
49
40
|
const urlTab = searchParams.get("tab");
|
|
50
|
-
if (urlTab && ["all", "open", "waiting_client", "resolved"].includes(urlTab)) return urlTab;
|
|
41
|
+
if (urlTab && ["all", "open", "waiting_client", "resolved", "sla_breach"].includes(urlTab)) return urlTab;
|
|
51
42
|
return "all";
|
|
52
43
|
});
|
|
53
44
|
useEffect(() => {
|
|
54
45
|
const urlTab = searchParams.get("tab");
|
|
55
|
-
if (urlTab && ["all", "open", "waiting_client", "resolved"].includes(urlTab)) {
|
|
46
|
+
if (urlTab && ["all", "open", "waiting_client", "resolved", "sla_breach"].includes(urlTab)) {
|
|
56
47
|
setTab(urlTab);
|
|
57
48
|
} else if (!urlTab) {
|
|
58
49
|
setTab("all");
|
|
@@ -60,7 +51,7 @@ const TicketInboxClient = () => {
|
|
|
60
51
|
}, [searchParams]);
|
|
61
52
|
const [search, setSearch] = useState("");
|
|
62
53
|
const [sort, setSort] = useState("-updatedAt");
|
|
63
|
-
const [counts, setCounts] = useState({ all: 0, open: 0, waiting: 0, resolved: 0 });
|
|
54
|
+
const [counts, setCounts] = useState({ all: 0, open: 0, waiting: 0, resolved: 0, breach: 0 });
|
|
64
55
|
const [selectedIdx, setSelectedIdx] = useState(-1);
|
|
65
56
|
const [checkedIds, setCheckedIds] = useState(/* @__PURE__ */ new Set());
|
|
66
57
|
const [bulkAction, setBulkAction] = useState("");
|
|
@@ -93,8 +84,32 @@ const TicketInboxClient = () => {
|
|
|
93
84
|
setBulkProcessing(false);
|
|
94
85
|
};
|
|
95
86
|
const fetchTickets = useCallback(async () => {
|
|
96
|
-
const params = [
|
|
97
|
-
|
|
87
|
+
const params = [
|
|
88
|
+
`limit=30`,
|
|
89
|
+
`sort=${sort}`,
|
|
90
|
+
`depth=1`,
|
|
91
|
+
`select[id]=true`,
|
|
92
|
+
`select[ticketNumber]=true`,
|
|
93
|
+
`select[subject]=true`,
|
|
94
|
+
`select[status]=true`,
|
|
95
|
+
`select[priority]=true`,
|
|
96
|
+
`select[category]=true`,
|
|
97
|
+
`select[client]=true`,
|
|
98
|
+
`select[updatedAt]=true`,
|
|
99
|
+
`select[lastClientMessageAt]=true`,
|
|
100
|
+
`select[lastAdminReadAt]=true`,
|
|
101
|
+
`select[slaFirstResponseDue]=true`,
|
|
102
|
+
`select[slaFirstResponseBreached]=true`,
|
|
103
|
+
`select[slaResolutionDue]=true`,
|
|
104
|
+
`select[slaResolutionBreached]=true`,
|
|
105
|
+
`select[firstResponseAt]=true`
|
|
106
|
+
];
|
|
107
|
+
if (tab === "sla_breach") {
|
|
108
|
+
params.push(`where[or][0][slaFirstResponseBreached][equals]=true`);
|
|
109
|
+
params.push(`where[or][1][slaResolutionBreached][equals]=true`);
|
|
110
|
+
} else if (tab !== "all") {
|
|
111
|
+
params.push(`where[status][equals]=${tab}`);
|
|
112
|
+
}
|
|
98
113
|
if (search.trim()) {
|
|
99
114
|
params.push(`where[or][0][subject][contains]=${encodeURIComponent(search)}`);
|
|
100
115
|
params.push(`where[or][1][ticketNumber][contains]=${encodeURIComponent(search)}`);
|
|
@@ -120,14 +135,21 @@ const TicketInboxClient = () => {
|
|
|
120
135
|
useEffect(() => {
|
|
121
136
|
const fetchCounts = async () => {
|
|
122
137
|
try {
|
|
123
|
-
const [all, openRes, waiting, resolved] = await Promise.all([
|
|
138
|
+
const [all, openRes, waiting, resolved, breach] = await Promise.all([
|
|
124
139
|
fetch("/api/tickets?limit=0&depth=0", { credentials: "include" }),
|
|
125
140
|
fetch("/api/tickets?limit=0&depth=0&where[status][equals]=open", { credentials: "include" }),
|
|
126
141
|
fetch("/api/tickets?limit=0&depth=0&where[status][equals]=waiting_client", { credentials: "include" }),
|
|
127
|
-
fetch("/api/tickets?limit=0&depth=0&where[status][equals]=resolved", { credentials: "include" })
|
|
142
|
+
fetch("/api/tickets?limit=0&depth=0&where[status][equals]=resolved", { credentials: "include" }),
|
|
143
|
+
fetch("/api/tickets?limit=0&depth=0&where[or][0][slaFirstResponseBreached][equals]=true&where[or][1][slaResolutionBreached][equals]=true", { credentials: "include" })
|
|
128
144
|
]);
|
|
129
|
-
const [a, o, w, r] = await Promise.all([all.json(), openRes.json(), waiting.json(), resolved.json()]);
|
|
130
|
-
setCounts({
|
|
145
|
+
const [a, o, w, r, b] = await Promise.all([all.json(), openRes.json(), waiting.json(), resolved.json(), breach.json()]);
|
|
146
|
+
setCounts({
|
|
147
|
+
all: a.totalDocs || 0,
|
|
148
|
+
open: o.totalDocs || 0,
|
|
149
|
+
waiting: w.totalDocs || 0,
|
|
150
|
+
resolved: r.totalDocs || 0,
|
|
151
|
+
breach: b.totalDocs || 0
|
|
152
|
+
});
|
|
131
153
|
} catch {
|
|
132
154
|
}
|
|
133
155
|
};
|
|
@@ -159,11 +181,22 @@ const TicketInboxClient = () => {
|
|
|
159
181
|
{ key: "all", label: t("inbox.tabs.all"), count: counts.all },
|
|
160
182
|
{ key: "open", label: t("inbox.tabs.open"), count: counts.open },
|
|
161
183
|
{ key: "waiting_client", label: t("inbox.tabs.waiting"), count: counts.waiting },
|
|
184
|
+
{ key: "sla_breach", label: t("inbox.tabs.slaBreach"), count: counts.breach, alert: true },
|
|
162
185
|
{ key: "resolved", label: t("inbox.tabs.resolved"), count: counts.resolved }
|
|
163
186
|
];
|
|
164
187
|
return /* @__PURE__ */ jsxs("div", { className: s.page, children: [
|
|
165
188
|
/* @__PURE__ */ jsxs("div", { className: s.header, children: [
|
|
166
|
-
/* @__PURE__ */
|
|
189
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
190
|
+
/* @__PURE__ */ jsx("h1", { className: s.title, children: t("inbox.title") }),
|
|
191
|
+
/* @__PURE__ */ jsxs("p", { className: s.subtitle, children: [
|
|
192
|
+
tickets.length > 0 && `${tickets.length} sur ${counts.all} \xB7 `,
|
|
193
|
+
counts.breach > 0 ? /* @__PURE__ */ jsxs("span", { className: s.subtitleBreach, children: [
|
|
194
|
+
counts.breach,
|
|
195
|
+
" SLA d\xE9pass\xE9",
|
|
196
|
+
counts.breach > 1 ? "s" : ""
|
|
197
|
+
] }) : /* @__PURE__ */ jsx("span", { children: "Aucun SLA en alerte" })
|
|
198
|
+
] })
|
|
199
|
+
] }),
|
|
167
200
|
/* @__PURE__ */ jsxs("div", { className: s.headerRight, children: [
|
|
168
201
|
/* @__PURE__ */ jsxs("div", { className: s.searchWrap, children: [
|
|
169
202
|
/* @__PURE__ */ jsxs("svg", { className: s.searchIcon, width: "14", height: "14", viewBox: "0 0 16 16", fill: "none", children: [
|
|
@@ -185,25 +218,26 @@ const TicketInboxClient = () => {
|
|
|
185
218
|
/* @__PURE__ */ jsx(Link, { href: "/admin/support/new-ticket", className: s.newTicketBtn, children: t("inbox.newTicketBtn") })
|
|
186
219
|
] })
|
|
187
220
|
] }),
|
|
188
|
-
/* @__PURE__ */ jsx("div", { className: s.tabs, children: tabs.map((tk) =>
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
221
|
+
/* @__PURE__ */ jsx("div", { className: s.tabs, role: "tablist", children: tabs.map((tk) => {
|
|
222
|
+
const isAlert = tk.alert && tk.count > 0;
|
|
223
|
+
return /* @__PURE__ */ jsxs(
|
|
224
|
+
"button",
|
|
225
|
+
{
|
|
226
|
+
role: "tab",
|
|
227
|
+
"aria-selected": tab === tk.key,
|
|
228
|
+
className: `${s.tab} ${tab === tk.key ? s.tabActive : ""} ${isAlert ? s.tabAlert : ""}`,
|
|
229
|
+
onClick: () => {
|
|
230
|
+
setTab(tk.key);
|
|
231
|
+
setSelectedIdx(-1);
|
|
232
|
+
},
|
|
233
|
+
children: [
|
|
234
|
+
tk.label,
|
|
235
|
+
/* @__PURE__ */ jsx("span", { className: `${s.tabCount} ${isAlert ? s.tabCountAlert : ""}`, children: tk.count })
|
|
236
|
+
]
|
|
195
237
|
},
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
"(",
|
|
200
|
-
tk.count,
|
|
201
|
-
")"
|
|
202
|
-
] })
|
|
203
|
-
]
|
|
204
|
-
},
|
|
205
|
-
tk.key
|
|
206
|
-
)) }),
|
|
238
|
+
tk.key
|
|
239
|
+
);
|
|
240
|
+
}) }),
|
|
207
241
|
/* @__PURE__ */ jsx("div", { className: s.sortRow, children: checkedIds.size > 0 ? /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 8, alignItems: "center", flex: 1 }, children: [
|
|
208
242
|
/* @__PURE__ */ jsx("span", { style: { fontSize: 13, fontWeight: 600, color: "var(--theme-text)" }, children: checkedIds.size > 1 ? t("inbox.selectedPlural", { count: String(checkedIds.size) }) : t("inbox.selected", { count: String(checkedIds.size) }) }),
|
|
209
243
|
/* @__PURE__ */ jsx("button", { className: s.sortSelect, onClick: () => handleBulkAction("close"), disabled: bulkProcessing, children: t("inbox.closeAction") }),
|
|
@@ -233,11 +267,19 @@ const TicketInboxClient = () => {
|
|
|
233
267
|
const displayClient = clientName ? `${clientName}${clientCompany ? `, ${clientCompany}` : ""}` : "\u2014";
|
|
234
268
|
const isUnread = tk.lastClientMessageAt && (!tk.lastAdminReadAt || new Date(tk.lastClientMessageAt) > new Date(tk.lastAdminReadAt));
|
|
235
269
|
const priorityColor = PRIORITY_COLORS[tk.priority] || "transparent";
|
|
270
|
+
const sla = computeSlaState(tk);
|
|
271
|
+
const isBreach = sla.state === "breach";
|
|
272
|
+
const statusVariant = STATUS_VARIANT[tk.status] || "open";
|
|
236
273
|
return /* @__PURE__ */ jsxs(
|
|
237
274
|
"a",
|
|
238
275
|
{
|
|
239
276
|
href: `/admin/support/ticket?id=${tk.id}`,
|
|
240
|
-
className:
|
|
277
|
+
className: [
|
|
278
|
+
s.row,
|
|
279
|
+
idx === selectedIdx ? s.rowSelected : "",
|
|
280
|
+
isUnread ? s.rowUnread : "",
|
|
281
|
+
isBreach ? s.rowBreach : ""
|
|
282
|
+
].filter(Boolean).join(" "),
|
|
241
283
|
onClick: (e) => {
|
|
242
284
|
e.preventDefault();
|
|
243
285
|
window.location.href = `/admin/support/ticket?id=${tk.id}`;
|
|
@@ -250,20 +292,36 @@ const TicketInboxClient = () => {
|
|
|
250
292
|
checked: checkedIds.has(tk.id),
|
|
251
293
|
onClick: (e) => e.stopPropagation(),
|
|
252
294
|
onChange: () => toggleCheck(tk.id),
|
|
253
|
-
|
|
295
|
+
className: s.rowCheck,
|
|
296
|
+
"aria-label": `S\xE9lectionner ${tk.ticketNumber}`
|
|
297
|
+
}
|
|
298
|
+
),
|
|
299
|
+
/* @__PURE__ */ jsx(
|
|
300
|
+
"span",
|
|
301
|
+
{
|
|
302
|
+
className: s.priorityDot,
|
|
303
|
+
style: { background: priorityColor },
|
|
304
|
+
"aria-label": `Priorit\xE9 ${tk.priority}`,
|
|
305
|
+
title: `Priorit\xE9 ${tk.priority}`
|
|
254
306
|
}
|
|
255
307
|
),
|
|
256
|
-
/* @__PURE__ */ jsx(
|
|
257
|
-
/* @__PURE__ */ jsx("span", { className: s.statusText, children: t(STATUS_LABEL_KEYS[tk.status] || "ticket.status.open") }),
|
|
308
|
+
/* @__PURE__ */ jsx(StatusPill, { status: isBreach ? "sla-breach" : statusVariant }),
|
|
258
309
|
/* @__PURE__ */ jsx("span", { className: s.ticketNum, children: tk.ticketNumber }),
|
|
259
310
|
/* @__PURE__ */ jsx("span", { className: s.subject, children: tk.subject }),
|
|
260
311
|
/* @__PURE__ */ jsx("span", { className: s.client, children: displayClient }),
|
|
261
|
-
|
|
262
|
-
"
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
312
|
+
/* @__PURE__ */ jsx(
|
|
313
|
+
"span",
|
|
314
|
+
{
|
|
315
|
+
className: [
|
|
316
|
+
s.slaCell,
|
|
317
|
+
sla.state === "breach" ? s.slaBreach : "",
|
|
318
|
+
sla.state === "warn" ? s.slaWarn : "",
|
|
319
|
+
sla.state === "ok" ? s.slaOk : ""
|
|
320
|
+
].filter(Boolean).join(" "),
|
|
321
|
+
title: sla.due ? new Date(sla.due).toLocaleString("fr-FR") : "",
|
|
322
|
+
children: formatSlaRemaining(sla.remainingMs)
|
|
323
|
+
}
|
|
324
|
+
),
|
|
267
325
|
/* @__PURE__ */ jsx("span", { className: s.timeAgo, children: relativeTime(tk.updatedAt) }),
|
|
268
326
|
isUnread ? /* @__PURE__ */ jsx("div", { className: s.unreadDot }) : /* @__PURE__ */ jsx("span", {})
|
|
269
327
|
]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
4
|
+
var s = require('./StatusPill.module.scss');
|
|
5
|
+
|
|
6
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
7
|
+
|
|
8
|
+
var s__default = /*#__PURE__*/_interopDefault(s);
|
|
9
|
+
|
|
10
|
+
const VARIANT_MAP = {
|
|
11
|
+
open: { cls: s__default.default.open, defaultLabel: "Ouvert" },
|
|
12
|
+
pending: { cls: s__default.default.pending, defaultLabel: "En attente" },
|
|
13
|
+
waiting_client: { cls: s__default.default.pending, defaultLabel: "Attente client" },
|
|
14
|
+
resolved: { cls: s__default.default.resolved, defaultLabel: "R\xE9solu" },
|
|
15
|
+
closed: { cls: s__default.default.neutral, defaultLabel: "Ferm\xE9" },
|
|
16
|
+
escalated: { cls: s__default.default.escalated, defaultLabel: "Escalad\xE9" },
|
|
17
|
+
"sla-breach": { cls: s__default.default.breach, defaultLabel: "SLA d\xE9pass\xE9" }
|
|
18
|
+
};
|
|
19
|
+
const StatusPill = ({ status, label, className }) => {
|
|
20
|
+
const v = VARIANT_MAP[status] || VARIANT_MAP.open;
|
|
21
|
+
const text = label ?? v.defaultLabel;
|
|
22
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
23
|
+
"span",
|
|
24
|
+
{
|
|
25
|
+
className: [s__default.default.pill, v.cls, className].filter(Boolean).join(" "),
|
|
26
|
+
role: "status",
|
|
27
|
+
"aria-label": `Statut : ${text}`,
|
|
28
|
+
children: [
|
|
29
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: s__default.default.dot, "aria-hidden": true }),
|
|
30
|
+
text
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
exports.StatusPill = StatusPill;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
2
|
+
import s from './StatusPill.module.scss';
|
|
3
|
+
|
|
4
|
+
const VARIANT_MAP = {
|
|
5
|
+
open: { cls: s.open, defaultLabel: "Ouvert" },
|
|
6
|
+
pending: { cls: s.pending, defaultLabel: "En attente" },
|
|
7
|
+
waiting_client: { cls: s.pending, defaultLabel: "Attente client" },
|
|
8
|
+
resolved: { cls: s.resolved, defaultLabel: "R\xE9solu" },
|
|
9
|
+
closed: { cls: s.neutral, defaultLabel: "Ferm\xE9" },
|
|
10
|
+
escalated: { cls: s.escalated, defaultLabel: "Escalad\xE9" },
|
|
11
|
+
"sla-breach": { cls: s.breach, defaultLabel: "SLA d\xE9pass\xE9" }
|
|
12
|
+
};
|
|
13
|
+
const StatusPill = ({ status, label, className }) => {
|
|
14
|
+
const v = VARIANT_MAP[status] || VARIANT_MAP.open;
|
|
15
|
+
const text = label ?? v.defaultLabel;
|
|
16
|
+
return /* @__PURE__ */ jsxs(
|
|
17
|
+
"span",
|
|
18
|
+
{
|
|
19
|
+
className: [s.pill, v.cls, className].filter(Boolean).join(" "),
|
|
20
|
+
role: "status",
|
|
21
|
+
"aria-label": `Statut : ${text}`,
|
|
22
|
+
children: [
|
|
23
|
+
/* @__PURE__ */ jsx("span", { className: s.dot, "aria-hidden": true }),
|
|
24
|
+
text
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export { StatusPill };
|