@consilioweb/payload-support 0.9.13 → 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/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/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
|
@@ -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 };
|
|
@@ -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.0",
|
|
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",
|