@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.
@@ -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: "#ef4444",
27
- high: "#f97316",
28
- normal: "transparent",
29
- low: "transparent"
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 CATEGORY_LABEL_KEYS = {
32
- bug: "ticket.category.bug",
33
- content: "ticket.category.content",
34
- feature: "ticket.category.feature",
35
- question: "ticket.category.question",
36
- hosting: "ticket.category.hosting"
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 = [`limit=30`, `sort=${sort}`, `depth=1`, `select[id]=true`, `select[ticketNumber]=true`, `select[subject]=true`, `select[status]=true`, `select[priority]=true`, `select[category]=true`, `select[client]=true`, `select[updatedAt]=true`, `select[lastClientMessageAt]=true`, `select[lastAdminReadAt]=true`];
103
- if (tab !== "all") params.push(`where[status][equals]=${tab}`);
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({ all: a.totalDocs || 0, open: o.totalDocs || 0, waiting: w.totalDocs || 0, resolved: r.totalDocs || 0 });
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.jsx("h1", { className: s__default.default.title, children: t("inbox.title") }),
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) => /* @__PURE__ */ jsxRuntime.jsxs(
195
- "button",
196
- {
197
- className: `${s__default.default.tab} ${tab === tk.key ? s__default.default.tabActive : ""}`,
198
- onClick: () => {
199
- setTab(tk.key);
200
- setSelectedIdx(-1);
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
- children: [
203
- tk.label,
204
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: s__default.default.tabCount, children: [
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: `${s__default.default.row} ${idx === selectedIdx ? s__default.default.rowSelected : ""}`,
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
- style: { width: 14, height: 14, cursor: "pointer", accentColor: "#2563eb" }
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("div", { className: s__default.default.statusDot, style: { backgroundColor: STATUS_DOTS[tk.status] || "#94a3b8" } }),
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
- tk.category ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: s__default.default.categoryChip, children: [
268
- "[",
269
- t(CATEGORY_LABEL_KEYS[tk.category] || "ticket.category.bug"),
270
- "]"
271
- ] }) : /* @__PURE__ */ jsxRuntime.jsx("span", {}),
272
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: s__default.default.priorityBar, style: { backgroundColor: priorityColor } }),
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: "#ef4444",
21
- high: "#f97316",
22
- normal: "transparent",
23
- low: "transparent"
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 CATEGORY_LABEL_KEYS = {
26
- bug: "ticket.category.bug",
27
- content: "ticket.category.content",
28
- feature: "ticket.category.feature",
29
- question: "ticket.category.question",
30
- hosting: "ticket.category.hosting"
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 = [`limit=30`, `sort=${sort}`, `depth=1`, `select[id]=true`, `select[ticketNumber]=true`, `select[subject]=true`, `select[status]=true`, `select[priority]=true`, `select[category]=true`, `select[client]=true`, `select[updatedAt]=true`, `select[lastClientMessageAt]=true`, `select[lastAdminReadAt]=true`];
97
- if (tab !== "all") params.push(`where[status][equals]=${tab}`);
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({ all: a.totalDocs || 0, open: o.totalDocs || 0, waiting: w.totalDocs || 0, resolved: r.totalDocs || 0 });
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__ */ jsx("h1", { className: s.title, children: t("inbox.title") }),
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) => /* @__PURE__ */ jsxs(
189
- "button",
190
- {
191
- className: `${s.tab} ${tab === tk.key ? s.tabActive : ""}`,
192
- onClick: () => {
193
- setTab(tk.key);
194
- setSelectedIdx(-1);
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
- children: [
197
- tk.label,
198
- /* @__PURE__ */ jsxs("span", { className: s.tabCount, children: [
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: `${s.row} ${idx === selectedIdx ? s.rowSelected : ""}`,
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
- style: { width: 14, height: 14, cursor: "pointer", accentColor: "#2563eb" }
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("div", { className: s.statusDot, style: { backgroundColor: STATUS_DOTS[tk.status] || "#94a3b8" } }),
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
- tk.category ? /* @__PURE__ */ jsxs("span", { className: s.categoryChip, children: [
262
- "[",
263
- t(CATEGORY_LABEL_KEYS[tk.category] || "ticket.category.bug"),
264
- "]"
265
- ] }) : /* @__PURE__ */ jsx("span", {}),
266
- /* @__PURE__ */ jsx("div", { className: s.priorityBar, style: { backgroundColor: priorityColor } }),
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 };