@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.
@@ -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: "#ef4444",
21
- high: "#f97316",
22
- normal: "transparent",
23
- low: "transparent"
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 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"
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 = [`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}`);
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({ all: a.totalDocs || 0, open: o.totalDocs || 0, waiting: w.totalDocs || 0, resolved: r.totalDocs || 0 });
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__ */ jsx("h1", { className: s.title, children: t("inbox.title") }),
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) => /* @__PURE__ */ jsxs(
189
- "button",
190
- {
191
- className: `${s.tab} ${tab === tk.key ? s.tabActive : ""}`,
192
- onClick: () => {
193
- setTab(tk.key);
194
- setSelectedIdx(-1);
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
- children: [
197
- tk.label,
198
- /* @__PURE__ */ jsxs("span", { className: s.tabCount, children: [
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: `${s.row} ${idx === selectedIdx ? s.rowSelected : ""}`,
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
- style: { width: 14, height: 14, cursor: "pointer", accentColor: "#2563eb" }
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("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") }),
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
- 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 } }),
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.9.13",
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
- "resolved": "Resolus"
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",