@consilioweb/payload-support 0.9.13 → 0.10.1
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/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 +117 -58
- package/dist/views/TicketInboxView/client.js +117 -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/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 +107 -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
|
@@ -4,30 +4,22 @@ 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 { StatusPill } from '../shared/StatusPill.js';
|
|
8
|
+
import { computeSlaState, formatSlaRemaining } from '../shared/sla.js';
|
|
7
9
|
import s from '../../styles/TicketInbox.module.scss';
|
|
8
10
|
|
|
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
11
|
const PRIORITY_COLORS = {
|
|
20
|
-
urgent: "
|
|
21
|
-
high: "
|
|
22
|
-
normal: "
|
|
23
|
-
low: "
|
|
12
|
+
urgent: "var(--theme-error-500)",
|
|
13
|
+
high: "var(--theme-warning-500)",
|
|
14
|
+
normal: "var(--theme-info-500, #2a6bd8)",
|
|
15
|
+
low: "var(--theme-elevation-400)"
|
|
24
16
|
};
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
17
|
+
const STATUS_VARIANT = {
|
|
18
|
+
open: "open",
|
|
19
|
+
waiting_client: "pending",
|
|
20
|
+
pending: "pending",
|
|
21
|
+
resolved: "resolved",
|
|
22
|
+
closed: "closed"
|
|
31
23
|
};
|
|
32
24
|
function relativeTime(dateStr) {
|
|
33
25
|
const diff = Date.now() - new Date(dateStr).getTime();
|
|
@@ -47,12 +39,12 @@ const TicketInboxClient = () => {
|
|
|
47
39
|
const [loading, setLoading] = useState(true);
|
|
48
40
|
const [tab, setTab] = useState(() => {
|
|
49
41
|
const urlTab = searchParams.get("tab");
|
|
50
|
-
if (urlTab && ["all", "open", "waiting_client", "resolved"].includes(urlTab)) return urlTab;
|
|
42
|
+
if (urlTab && ["all", "open", "waiting_client", "resolved", "sla_breach"].includes(urlTab)) return urlTab;
|
|
51
43
|
return "all";
|
|
52
44
|
});
|
|
53
45
|
useEffect(() => {
|
|
54
46
|
const urlTab = searchParams.get("tab");
|
|
55
|
-
if (urlTab && ["all", "open", "waiting_client", "resolved"].includes(urlTab)) {
|
|
47
|
+
if (urlTab && ["all", "open", "waiting_client", "resolved", "sla_breach"].includes(urlTab)) {
|
|
56
48
|
setTab(urlTab);
|
|
57
49
|
} else if (!urlTab) {
|
|
58
50
|
setTab("all");
|
|
@@ -60,7 +52,7 @@ const TicketInboxClient = () => {
|
|
|
60
52
|
}, [searchParams]);
|
|
61
53
|
const [search, setSearch] = useState("");
|
|
62
54
|
const [sort, setSort] = useState("-updatedAt");
|
|
63
|
-
const [counts, setCounts] = useState({ all: 0, open: 0, waiting: 0, resolved: 0 });
|
|
55
|
+
const [counts, setCounts] = useState({ all: 0, open: 0, waiting: 0, resolved: 0, breach: 0 });
|
|
64
56
|
const [selectedIdx, setSelectedIdx] = useState(-1);
|
|
65
57
|
const [checkedIds, setCheckedIds] = useState(/* @__PURE__ */ new Set());
|
|
66
58
|
const [bulkAction, setBulkAction] = useState("");
|
|
@@ -93,8 +85,32 @@ const TicketInboxClient = () => {
|
|
|
93
85
|
setBulkProcessing(false);
|
|
94
86
|
};
|
|
95
87
|
const fetchTickets = useCallback(async () => {
|
|
96
|
-
const params = [
|
|
97
|
-
|
|
88
|
+
const params = [
|
|
89
|
+
`limit=30`,
|
|
90
|
+
`sort=${sort}`,
|
|
91
|
+
`depth=1`,
|
|
92
|
+
`select[id]=true`,
|
|
93
|
+
`select[ticketNumber]=true`,
|
|
94
|
+
`select[subject]=true`,
|
|
95
|
+
`select[status]=true`,
|
|
96
|
+
`select[priority]=true`,
|
|
97
|
+
`select[category]=true`,
|
|
98
|
+
`select[client]=true`,
|
|
99
|
+
`select[updatedAt]=true`,
|
|
100
|
+
`select[lastClientMessageAt]=true`,
|
|
101
|
+
`select[lastAdminReadAt]=true`,
|
|
102
|
+
`select[slaFirstResponseDue]=true`,
|
|
103
|
+
`select[slaFirstResponseBreached]=true`,
|
|
104
|
+
`select[slaResolutionDue]=true`,
|
|
105
|
+
`select[slaResolutionBreached]=true`,
|
|
106
|
+
`select[firstResponseAt]=true`
|
|
107
|
+
];
|
|
108
|
+
if (tab === "sla_breach") {
|
|
109
|
+
params.push(`where[or][0][slaFirstResponseBreached][equals]=true`);
|
|
110
|
+
params.push(`where[or][1][slaResolutionBreached][equals]=true`);
|
|
111
|
+
} else if (tab !== "all") {
|
|
112
|
+
params.push(`where[status][equals]=${tab}`);
|
|
113
|
+
}
|
|
98
114
|
if (search.trim()) {
|
|
99
115
|
params.push(`where[or][0][subject][contains]=${encodeURIComponent(search)}`);
|
|
100
116
|
params.push(`where[or][1][ticketNumber][contains]=${encodeURIComponent(search)}`);
|
|
@@ -120,14 +136,21 @@ const TicketInboxClient = () => {
|
|
|
120
136
|
useEffect(() => {
|
|
121
137
|
const fetchCounts = async () => {
|
|
122
138
|
try {
|
|
123
|
-
const [all, openRes, waiting, resolved] = await Promise.all([
|
|
139
|
+
const [all, openRes, waiting, resolved, breach] = await Promise.all([
|
|
124
140
|
fetch("/api/tickets?limit=0&depth=0", { credentials: "include" }),
|
|
125
141
|
fetch("/api/tickets?limit=0&depth=0&where[status][equals]=open", { credentials: "include" }),
|
|
126
142
|
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" })
|
|
143
|
+
fetch("/api/tickets?limit=0&depth=0&where[status][equals]=resolved", { credentials: "include" }),
|
|
144
|
+
fetch("/api/tickets?limit=0&depth=0&where[or][0][slaFirstResponseBreached][equals]=true&where[or][1][slaResolutionBreached][equals]=true", { credentials: "include" })
|
|
128
145
|
]);
|
|
129
|
-
const [a, o, w, r] = await Promise.all([all.json(), openRes.json(), waiting.json(), resolved.json()]);
|
|
130
|
-
setCounts({
|
|
146
|
+
const [a, o, w, r, b] = await Promise.all([all.json(), openRes.json(), waiting.json(), resolved.json(), breach.json()]);
|
|
147
|
+
setCounts({
|
|
148
|
+
all: a.totalDocs || 0,
|
|
149
|
+
open: o.totalDocs || 0,
|
|
150
|
+
waiting: w.totalDocs || 0,
|
|
151
|
+
resolved: r.totalDocs || 0,
|
|
152
|
+
breach: b.totalDocs || 0
|
|
153
|
+
});
|
|
131
154
|
} catch {
|
|
132
155
|
}
|
|
133
156
|
};
|
|
@@ -159,11 +182,22 @@ const TicketInboxClient = () => {
|
|
|
159
182
|
{ key: "all", label: t("inbox.tabs.all"), count: counts.all },
|
|
160
183
|
{ key: "open", label: t("inbox.tabs.open"), count: counts.open },
|
|
161
184
|
{ key: "waiting_client", label: t("inbox.tabs.waiting"), count: counts.waiting },
|
|
185
|
+
{ key: "sla_breach", label: t("inbox.tabs.slaBreach"), count: counts.breach, alert: true },
|
|
162
186
|
{ key: "resolved", label: t("inbox.tabs.resolved"), count: counts.resolved }
|
|
163
187
|
];
|
|
164
188
|
return /* @__PURE__ */ jsxs("div", { className: s.page, children: [
|
|
165
189
|
/* @__PURE__ */ jsxs("div", { className: s.header, children: [
|
|
166
|
-
/* @__PURE__ */
|
|
190
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
191
|
+
/* @__PURE__ */ jsx("h1", { className: s.title, children: t("inbox.title") }),
|
|
192
|
+
/* @__PURE__ */ jsxs("p", { className: s.subtitle, children: [
|
|
193
|
+
tickets.length > 0 && `${tickets.length} sur ${counts.all} \xB7 `,
|
|
194
|
+
counts.breach > 0 ? /* @__PURE__ */ jsxs("span", { className: s.subtitleBreach, children: [
|
|
195
|
+
counts.breach,
|
|
196
|
+
" SLA d\xE9pass\xE9",
|
|
197
|
+
counts.breach > 1 ? "s" : ""
|
|
198
|
+
] }) : /* @__PURE__ */ jsx("span", { children: "Aucun SLA en alerte" })
|
|
199
|
+
] })
|
|
200
|
+
] }),
|
|
167
201
|
/* @__PURE__ */ jsxs("div", { className: s.headerRight, children: [
|
|
168
202
|
/* @__PURE__ */ jsxs("div", { className: s.searchWrap, children: [
|
|
169
203
|
/* @__PURE__ */ jsxs("svg", { className: s.searchIcon, width: "14", height: "14", viewBox: "0 0 16 16", fill: "none", children: [
|
|
@@ -185,25 +219,26 @@ const TicketInboxClient = () => {
|
|
|
185
219
|
/* @__PURE__ */ jsx(Link, { href: "/admin/support/new-ticket", className: s.newTicketBtn, children: t("inbox.newTicketBtn") })
|
|
186
220
|
] })
|
|
187
221
|
] }),
|
|
188
|
-
/* @__PURE__ */ jsx("div", { className: s.tabs, children: tabs.map((tk) =>
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
222
|
+
/* @__PURE__ */ jsx("div", { className: s.tabs, role: "tablist", children: tabs.map((tk) => {
|
|
223
|
+
const isAlert = tk.alert && tk.count > 0;
|
|
224
|
+
return /* @__PURE__ */ jsxs(
|
|
225
|
+
"button",
|
|
226
|
+
{
|
|
227
|
+
role: "tab",
|
|
228
|
+
"aria-selected": tab === tk.key,
|
|
229
|
+
className: `${s.tab} ${tab === tk.key ? s.tabActive : ""} ${isAlert ? s.tabAlert : ""}`,
|
|
230
|
+
onClick: () => {
|
|
231
|
+
setTab(tk.key);
|
|
232
|
+
setSelectedIdx(-1);
|
|
233
|
+
},
|
|
234
|
+
children: [
|
|
235
|
+
tk.label,
|
|
236
|
+
/* @__PURE__ */ jsx("span", { className: `${s.tabCount} ${isAlert ? s.tabCountAlert : ""}`, children: tk.count })
|
|
237
|
+
]
|
|
195
238
|
},
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
"(",
|
|
200
|
-
tk.count,
|
|
201
|
-
")"
|
|
202
|
-
] })
|
|
203
|
-
]
|
|
204
|
-
},
|
|
205
|
-
tk.key
|
|
206
|
-
)) }),
|
|
239
|
+
tk.key
|
|
240
|
+
);
|
|
241
|
+
}) }),
|
|
207
242
|
/* @__PURE__ */ jsx("div", { className: s.sortRow, children: checkedIds.size > 0 ? /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 8, alignItems: "center", flex: 1 }, children: [
|
|
208
243
|
/* @__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
244
|
/* @__PURE__ */ jsx("button", { className: s.sortSelect, onClick: () => handleBulkAction("close"), disabled: bulkProcessing, children: t("inbox.closeAction") }),
|
|
@@ -233,11 +268,19 @@ const TicketInboxClient = () => {
|
|
|
233
268
|
const displayClient = clientName ? `${clientName}${clientCompany ? `, ${clientCompany}` : ""}` : "\u2014";
|
|
234
269
|
const isUnread = tk.lastClientMessageAt && (!tk.lastAdminReadAt || new Date(tk.lastClientMessageAt) > new Date(tk.lastAdminReadAt));
|
|
235
270
|
const priorityColor = PRIORITY_COLORS[tk.priority] || "transparent";
|
|
271
|
+
const sla = computeSlaState(tk);
|
|
272
|
+
const isBreach = sla.state === "breach";
|
|
273
|
+
const statusVariant = STATUS_VARIANT[tk.status] || "open";
|
|
236
274
|
return /* @__PURE__ */ jsxs(
|
|
237
275
|
"a",
|
|
238
276
|
{
|
|
239
277
|
href: `/admin/support/ticket?id=${tk.id}`,
|
|
240
|
-
className:
|
|
278
|
+
className: [
|
|
279
|
+
s.row,
|
|
280
|
+
idx === selectedIdx ? s.rowSelected : "",
|
|
281
|
+
isUnread ? s.rowUnread : "",
|
|
282
|
+
isBreach ? s.rowBreach : ""
|
|
283
|
+
].filter(Boolean).join(" "),
|
|
241
284
|
onClick: (e) => {
|
|
242
285
|
e.preventDefault();
|
|
243
286
|
window.location.href = `/admin/support/ticket?id=${tk.id}`;
|
|
@@ -250,20 +293,36 @@ const TicketInboxClient = () => {
|
|
|
250
293
|
checked: checkedIds.has(tk.id),
|
|
251
294
|
onClick: (e) => e.stopPropagation(),
|
|
252
295
|
onChange: () => toggleCheck(tk.id),
|
|
253
|
-
|
|
296
|
+
className: s.rowCheck,
|
|
297
|
+
"aria-label": `S\xE9lectionner ${tk.ticketNumber}`
|
|
298
|
+
}
|
|
299
|
+
),
|
|
300
|
+
/* @__PURE__ */ jsx(
|
|
301
|
+
"span",
|
|
302
|
+
{
|
|
303
|
+
className: s.priorityDot,
|
|
304
|
+
style: { background: priorityColor },
|
|
305
|
+
"aria-label": `Priorit\xE9 ${tk.priority}`,
|
|
306
|
+
title: `Priorit\xE9 ${tk.priority}`
|
|
254
307
|
}
|
|
255
308
|
),
|
|
256
|
-
/* @__PURE__ */ jsx(
|
|
257
|
-
/* @__PURE__ */ jsx("span", { className: s.statusText, children: t(STATUS_LABEL_KEYS[tk.status] || "ticket.status.open") }),
|
|
309
|
+
/* @__PURE__ */ jsx(StatusPill, { status: isBreach ? "sla-breach" : statusVariant }),
|
|
258
310
|
/* @__PURE__ */ jsx("span", { className: s.ticketNum, children: tk.ticketNumber }),
|
|
259
311
|
/* @__PURE__ */ jsx("span", { className: s.subject, children: tk.subject }),
|
|
260
312
|
/* @__PURE__ */ jsx("span", { className: s.client, children: displayClient }),
|
|
261
|
-
|
|
262
|
-
"
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
313
|
+
/* @__PURE__ */ jsx(
|
|
314
|
+
"span",
|
|
315
|
+
{
|
|
316
|
+
className: [
|
|
317
|
+
s.slaCell,
|
|
318
|
+
sla.state === "breach" ? s.slaBreach : "",
|
|
319
|
+
sla.state === "warn" ? s.slaWarn : "",
|
|
320
|
+
sla.state === "ok" ? s.slaOk : ""
|
|
321
|
+
].filter(Boolean).join(" "),
|
|
322
|
+
title: sla.due ? new Date(sla.due).toLocaleString("fr-FR") : "",
|
|
323
|
+
children: formatSlaRemaining(sla.remainingMs)
|
|
324
|
+
}
|
|
325
|
+
),
|
|
267
326
|
/* @__PURE__ */ jsx("span", { className: s.timeAgo, children: relativeTime(tk.updatedAt) }),
|
|
268
327
|
isUnread ? /* @__PURE__ */ jsx("div", { className: s.unreadDot }) : /* @__PURE__ */ jsx("span", {})
|
|
269
328
|
]
|
|
@@ -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 };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// StatusPill — sémantique unique des statuts ticket
|
|
2
|
+
// Tous les fonds/couleurs sont mappés sur les --theme-* Payload natifs
|
|
3
|
+
// via color-mix() pour rester cohérent en light/dark.
|
|
4
|
+
|
|
5
|
+
.pill {
|
|
6
|
+
display: inline-flex;
|
|
7
|
+
align-items: center;
|
|
8
|
+
gap: 6px;
|
|
9
|
+
height: 22px;
|
|
10
|
+
padding: 0 8px;
|
|
11
|
+
border-radius: 999px;
|
|
12
|
+
font-size: 11px;
|
|
13
|
+
font-weight: 500;
|
|
14
|
+
letter-spacing: 0.01em;
|
|
15
|
+
white-space: nowrap;
|
|
16
|
+
border: 1px solid transparent;
|
|
17
|
+
line-height: 1;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.dot {
|
|
21
|
+
width: 6px;
|
|
22
|
+
height: 6px;
|
|
23
|
+
border-radius: 50%;
|
|
24
|
+
background: currentColor;
|
|
25
|
+
flex-shrink: 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ---------- Variants ----------
|
|
29
|
+
// Each variant uses color-mix on Payload's theme tokens so dark/light just works.
|
|
30
|
+
|
|
31
|
+
.open {
|
|
32
|
+
background: color-mix(in srgb, var(--theme-success-500, #2dbf86) 12%, transparent);
|
|
33
|
+
color: var(--theme-success-500, #16a34a);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.pending {
|
|
37
|
+
background: color-mix(in srgb, var(--theme-warning-500, #e9a23a) 14%, transparent);
|
|
38
|
+
color: var(--theme-warning-500, #c97a0a);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.resolved {
|
|
42
|
+
background: color-mix(in srgb, var(--theme-elevation-500, #7a7a7a) 14%, transparent);
|
|
43
|
+
color: var(--theme-elevation-600, #5e5e5e);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.neutral {
|
|
47
|
+
background: var(--theme-elevation-100, #f3f3f3);
|
|
48
|
+
color: var(--theme-elevation-600, #5e5e5e);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.escalated {
|
|
52
|
+
background: color-mix(in srgb, var(--theme-error-500, #c4392c) 14%, transparent);
|
|
53
|
+
color: var(--theme-error-500, #c4392c);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.breach {
|
|
57
|
+
background: color-mix(in srgb, var(--theme-error-500, #c4392c) 14%, transparent);
|
|
58
|
+
color: var(--theme-error-500, #c4392c);
|
|
59
|
+
border-color: color-mix(in srgb, var(--theme-error-500, #c4392c) 30%, transparent);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
[data-theme='dark'] {
|
|
63
|
+
.open { color: var(--theme-success-500, #2dbf86); }
|
|
64
|
+
.pending { color: var(--theme-warning-500, #e9a23a); }
|
|
65
|
+
.escalated, .breach { color: var(--theme-error-500, #ed5f51); }
|
|
66
|
+
}
|
|
@@ -5,6 +5,8 @@ var adminTokens = require('./adminTokens');
|
|
|
5
5
|
var AdminViewHeader = require('./AdminViewHeader');
|
|
6
6
|
var Skeleton = require('./Skeleton');
|
|
7
7
|
var config = require('./config');
|
|
8
|
+
var StatusPill = require('./StatusPill');
|
|
9
|
+
var sla = require('./sla');
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
|
|
@@ -56,3 +58,15 @@ Object.defineProperty(exports, "saveFeatures", {
|
|
|
56
58
|
enumerable: true,
|
|
57
59
|
get: function () { return config.saveFeatures; }
|
|
58
60
|
});
|
|
61
|
+
Object.defineProperty(exports, "StatusPill", {
|
|
62
|
+
enumerable: true,
|
|
63
|
+
get: function () { return StatusPill.StatusPill; }
|
|
64
|
+
});
|
|
65
|
+
Object.defineProperty(exports, "computeSlaState", {
|
|
66
|
+
enumerable: true,
|
|
67
|
+
get: function () { return sla.computeSlaState; }
|
|
68
|
+
});
|
|
69
|
+
Object.defineProperty(exports, "formatSlaRemaining", {
|
|
70
|
+
enumerable: true,
|
|
71
|
+
get: function () { return sla.formatSlaRemaining; }
|
|
72
|
+
});
|
|
@@ -3,3 +3,5 @@ export { V, btnStyle } from './adminTokens.js';
|
|
|
3
3
|
export { AdminViewHeader } from './AdminViewHeader.js';
|
|
4
4
|
export { Skeleton, SkeletonCard, SkeletonDashboard, SkeletonTable, SkeletonText } from './Skeleton.js';
|
|
5
5
|
export { DEFAULT_FEATURES, getFeatures, saveFeatures } from './config.js';
|
|
6
|
+
export { StatusPill } from './StatusPill.js';
|
|
7
|
+
export { computeSlaState, formatSlaRemaining } from './sla.js';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const HOUR_MS = 36e5;
|
|
4
|
+
const MINUTE_MS = 6e4;
|
|
5
|
+
function computeSlaState(t, now = /* @__PURE__ */ new Date()) {
|
|
6
|
+
const hasFirstResponse = !!t.firstResponseAt;
|
|
7
|
+
const dueRaw = hasFirstResponse ? t.slaResolutionDue : t.slaFirstResponseDue;
|
|
8
|
+
const breached = hasFirstResponse ? !!t.slaResolutionBreached : !!t.slaFirstResponseBreached;
|
|
9
|
+
if (!dueRaw) return { state: "none", remainingMs: null, due: null };
|
|
10
|
+
const dueDate = new Date(dueRaw);
|
|
11
|
+
if (Number.isNaN(dueDate.getTime())) return { state: "none", remainingMs: null, due: null };
|
|
12
|
+
const remaining = dueDate.getTime() - now.getTime();
|
|
13
|
+
if (breached || remaining < 0) {
|
|
14
|
+
return { state: "breach", remainingMs: remaining, due: dueRaw };
|
|
15
|
+
}
|
|
16
|
+
if (remaining < HOUR_MS) {
|
|
17
|
+
return { state: "warn", remainingMs: remaining, due: dueRaw };
|
|
18
|
+
}
|
|
19
|
+
return { state: "ok", remainingMs: remaining, due: dueRaw };
|
|
20
|
+
}
|
|
21
|
+
function formatSlaRemaining(remainingMs) {
|
|
22
|
+
if (remainingMs == null) return "\u2014";
|
|
23
|
+
const abs = Math.abs(remainingMs);
|
|
24
|
+
const sign = remainingMs < 0 ? "\u2212" : "";
|
|
25
|
+
if (abs < HOUR_MS) {
|
|
26
|
+
const m2 = Math.floor(abs / MINUTE_MS);
|
|
27
|
+
return `${sign}${m2}m`;
|
|
28
|
+
}
|
|
29
|
+
const h = Math.floor(abs / HOUR_MS);
|
|
30
|
+
const m = Math.floor(abs % HOUR_MS / MINUTE_MS);
|
|
31
|
+
if (h < 24) return `${sign}${h}h${m > 0 ? ` ${String(m).padStart(2, "0")}` : ""}`;
|
|
32
|
+
const d = Math.floor(h / 24);
|
|
33
|
+
return `${sign}${d}j`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
exports.computeSlaState = computeSlaState;
|
|
37
|
+
exports.formatSlaRemaining = formatSlaRemaining;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const HOUR_MS = 36e5;
|
|
2
|
+
const MINUTE_MS = 6e4;
|
|
3
|
+
function computeSlaState(t, now = /* @__PURE__ */ new Date()) {
|
|
4
|
+
const hasFirstResponse = !!t.firstResponseAt;
|
|
5
|
+
const dueRaw = hasFirstResponse ? t.slaResolutionDue : t.slaFirstResponseDue;
|
|
6
|
+
const breached = hasFirstResponse ? !!t.slaResolutionBreached : !!t.slaFirstResponseBreached;
|
|
7
|
+
if (!dueRaw) return { state: "none", remainingMs: null, due: null };
|
|
8
|
+
const dueDate = new Date(dueRaw);
|
|
9
|
+
if (Number.isNaN(dueDate.getTime())) return { state: "none", remainingMs: null, due: null };
|
|
10
|
+
const remaining = dueDate.getTime() - now.getTime();
|
|
11
|
+
if (breached || remaining < 0) {
|
|
12
|
+
return { state: "breach", remainingMs: remaining, due: dueRaw };
|
|
13
|
+
}
|
|
14
|
+
if (remaining < HOUR_MS) {
|
|
15
|
+
return { state: "warn", remainingMs: remaining, due: dueRaw };
|
|
16
|
+
}
|
|
17
|
+
return { state: "ok", remainingMs: remaining, due: dueRaw };
|
|
18
|
+
}
|
|
19
|
+
function formatSlaRemaining(remainingMs) {
|
|
20
|
+
if (remainingMs == null) return "\u2014";
|
|
21
|
+
const abs = Math.abs(remainingMs);
|
|
22
|
+
const sign = remainingMs < 0 ? "\u2212" : "";
|
|
23
|
+
if (abs < HOUR_MS) {
|
|
24
|
+
const m2 = Math.floor(abs / MINUTE_MS);
|
|
25
|
+
return `${sign}${m2}m`;
|
|
26
|
+
}
|
|
27
|
+
const h = Math.floor(abs / HOUR_MS);
|
|
28
|
+
const m = Math.floor(abs % HOUR_MS / MINUTE_MS);
|
|
29
|
+
if (h < 24) return `${sign}${h}h${m > 0 ? ` ${String(m).padStart(2, "0")}` : ""}`;
|
|
30
|
+
const d = Math.floor(h / 24);
|
|
31
|
+
return `${sign}${d}j`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export { computeSlaState, formatSlaRemaining };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@consilioweb/payload-support",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"description": "Payload CMS plugin — professional support & ticketing system with AI, SLA, time tracking, live chat, and more",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -108,8 +108,16 @@
|
|
|
108
108
|
"all": "All",
|
|
109
109
|
"open": "Open",
|
|
110
110
|
"waiting": "Waiting",
|
|
111
|
+
"slaBreach": "SLA breached",
|
|
111
112
|
"resolved": "Resolved"
|
|
112
113
|
},
|
|
114
|
+
"slaColumn": "SLA",
|
|
115
|
+
"slaBanner": {
|
|
116
|
+
"breached": "SLA breached",
|
|
117
|
+
"responseAwaitedFor": "Response awaited for {{duration}}",
|
|
118
|
+
"target": "Target: {{target}}",
|
|
119
|
+
"escalate": "Escalate"
|
|
120
|
+
},
|
|
113
121
|
"sort": {
|
|
114
122
|
"newest": "Sort by: Newest",
|
|
115
123
|
"oldest": "Sort by: Oldest",
|
|
@@ -108,7 +108,15 @@
|
|
|
108
108
|
"all": "Tous",
|
|
109
109
|
"open": "Ouverts",
|
|
110
110
|
"waiting": "Attente",
|
|
111
|
-
"
|
|
111
|
+
"slaBreach": "SLA dépassé",
|
|
112
|
+
"resolved": "Résolus"
|
|
113
|
+
},
|
|
114
|
+
"slaColumn": "SLA",
|
|
115
|
+
"slaBanner": {
|
|
116
|
+
"breached": "SLA dépassé",
|
|
117
|
+
"responseAwaitedFor": "Réponse attendue depuis {{duration}}",
|
|
118
|
+
"target": "Cible : {{target}}",
|
|
119
|
+
"escalate": "Escalader"
|
|
112
120
|
},
|
|
113
121
|
"sort": {
|
|
114
122
|
"newest": "Tri : Recents",
|