@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.
@@ -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.12",
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
- "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",
package/src/index.ts CHANGED
@@ -25,6 +25,8 @@ export { readSupportSettings, readUserPrefs, DEFAULT_SETTINGS, DEFAULT_USER_PREF
25
25
  export type { SupportSettings, UserPrefs } from './utils/readSettings'
26
26
  export { createAdminNotification } from './utils/adminNotification'
27
27
  export { dispatchWebhook } from './utils/webhookDispatcher'
28
+ export { generateTicketSynthesis } from './utils/generateTicketSynthesis'
29
+ export type { TicketSynthesisResult } from './utils/generateTicketSynthesis'
28
30
 
29
31
  // Hooks
30
32
  export { createAssignSlaDeadlines, createCheckSlaOnResolve, createCheckSlaOnReply, calculateBusinessHoursDeadline } from './hooks/checkSLA'
@@ -273,15 +273,39 @@
273
273
  padding: 14px 12px;
274
274
  border-radius: 10px;
275
275
  position: relative;
276
- transition: background 80ms;
276
+ transition: background 80ms ease;
277
277
 
278
278
  &:hover { background: var(--theme-elevation-50); }
279
279
  }
280
280
 
281
+ // Client bubble — warm tint for fast scanning (left side of the convo).
282
+ .messageClient {
283
+ background: color-mix(in srgb, var(--theme-warning-500, #c97a0a) 5%, transparent);
284
+ border-left: 3px solid color-mix(in srgb, var(--theme-warning-500, #c97a0a) 35%, transparent);
285
+
286
+ &:hover {
287
+ background: color-mix(in srgb, var(--theme-warning-500, #c97a0a) 10%, transparent);
288
+ }
289
+ }
290
+
291
+ // Admin/support bubble — neutral, the user expects to author here.
292
+ .messageAdmin {
293
+ background: var(--theme-elevation-0, #fff);
294
+ border-left: 3px solid color-mix(in srgb, var(--theme-info-500, #2a6bd8) 35%, transparent);
295
+
296
+ &:hover {
297
+ background: var(--theme-elevation-50);
298
+ }
299
+ }
300
+
301
+ // Internal note — visible to agents only, takes precedence over admin styles.
281
302
  .messageInternal {
282
- background: #fefce8;
283
- border-left: 3px dashed #d97706;
284
- &:hover { background: #fef9c3; }
303
+ background: color-mix(in srgb, var(--theme-warning-500, #c97a0a) 12%, transparent) !important;
304
+ border-left: 3px dashed var(--theme-warning-500, #c97a0a) !important;
305
+
306
+ &:hover {
307
+ background: color-mix(in srgb, var(--theme-warning-500, #c97a0a) 18%, transparent) !important;
308
+ }
285
309
  }
286
310
 
287
311
  .avatar {
@@ -402,25 +426,30 @@
402
426
  position: relative;
403
427
  background: var(--theme-elevation-0, #fff);
404
428
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
405
- transition: border-color 150ms, box-shadow 150ms;
429
+ transition: border-color 150ms, box-shadow 150ms, background 150ms;
406
430
 
407
431
  &:focus-within {
408
- border-color: #2563eb;
409
- box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.08);
432
+ border-color: var(--theme-info-500, #2a6bd8);
433
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--theme-info-500, #2a6bd8) 18%, transparent);
410
434
  }
411
435
  }
412
436
 
413
437
  .composerInternal {
414
- border-color: #d97706;
438
+ background: color-mix(in srgb, var(--theme-warning-500, #c97a0a) 8%, var(--theme-elevation-0, #fff));
439
+ border-color: var(--theme-warning-500, #c97a0a);
415
440
  border-style: dashed;
416
- &:focus-within { border-color: #d97706; box-shadow: 0 0 0 3px rgba(217, 119, 6, 0.08); }
441
+
442
+ &:focus-within {
443
+ border-color: var(--theme-warning-500, #c97a0a);
444
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--theme-warning-500, #c97a0a) 18%, transparent);
445
+ }
417
446
  }
418
447
 
419
448
  // #5 — Drag & drop highlight
420
449
  .composerDragOver {
421
- border-color: #2563eb;
422
- background: rgba(37, 99, 235, 0.04);
423
- box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.12);
450
+ border-color: var(--theme-info-500, #2a6bd8);
451
+ background: color-mix(in srgb, var(--theme-info-500, #2a6bd8) 6%, transparent);
452
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--theme-info-500, #2a6bd8) 18%, transparent);
424
453
  }
425
454
 
426
455
  .composerToolbar {
@@ -452,6 +481,27 @@
452
481
  }
453
482
 
454
483
  &:disabled { opacity: 0.3; cursor: not-allowed; }
484
+ }
485
+
486
+ // AI variant — promoted, distinct color so it doesn't blend with toolbar icons.
487
+ .toolbarBtnAi {
488
+ width: auto;
489
+ height: 28px;
490
+ padding: 0 10px;
491
+ font-size: 11px;
492
+ font-weight: 600;
493
+ background: color-mix(in srgb, var(--theme-accent-500, #6b46e8) 12%, transparent);
494
+ border: 1px solid color-mix(in srgb, var(--theme-accent-500, #6b46e8) 30%, transparent);
495
+ color: var(--theme-accent-500, #6b46e8);
496
+ gap: 4px;
497
+
498
+ &:hover:not(:disabled) {
499
+ background: color-mix(in srgb, var(--theme-accent-500, #6b46e8) 18%, transparent);
500
+ color: var(--theme-accent-500, #6b46e8);
501
+ }
502
+ }
503
+
504
+ .toolbarBtn {
455
505
 
456
506
  // Tooltip
457
507
  &::after {
@@ -550,27 +600,61 @@
550
600
  }
551
601
  }
552
602
 
603
+ .sendShortcut {
604
+ display: inline-flex;
605
+ align-items: center;
606
+ gap: 2px;
607
+ color: var(--theme-elevation-400);
608
+ font-size: 11px;
609
+ margin-right: 8px;
610
+
611
+ kbd {
612
+ display: inline-flex;
613
+ align-items: center;
614
+ justify-content: center;
615
+ min-width: 18px;
616
+ height: 18px;
617
+ padding: 0 4px;
618
+ border-radius: 3px;
619
+ background: var(--theme-elevation-100);
620
+ border: 1px solid var(--theme-elevation-200);
621
+ font-family: inherit;
622
+ font-size: 10px;
623
+ font-weight: 600;
624
+ color: var(--theme-elevation-600);
625
+ }
626
+ }
627
+
553
628
  .sendBtn {
554
629
  padding: 8px 22px;
555
630
  border-radius: 8px;
556
- border: none;
557
- background: #2563eb;
558
- color: #fff;
631
+ border: 1px solid var(--theme-text);
632
+ background: var(--theme-text);
633
+ color: var(--theme-bg, #fff);
559
634
  font-size: 13px;
560
- font-weight: 700;
635
+ font-weight: 600;
561
636
  cursor: pointer;
562
- transition: background 100ms;
637
+ transition: background 100ms, border-color 100ms;
563
638
  display: flex;
564
639
  align-items: center;
565
640
  gap: 6px;
566
641
 
567
- &:hover { background: #1d4ed8; }
642
+ &:hover {
643
+ background: var(--theme-elevation-800);
644
+ border-color: var(--theme-elevation-800);
645
+ }
568
646
  &:disabled { opacity: 0.4; cursor: not-allowed; }
569
647
  }
570
648
 
571
649
  .sendBtnInternal {
572
- background: #d97706;
573
- &:hover { background: #b45309; }
650
+ background: var(--theme-warning-500, #c97a0a);
651
+ border-color: var(--theme-warning-500, #c97a0a);
652
+ color: #fff;
653
+
654
+ &:hover {
655
+ background: color-mix(in srgb, var(--theme-warning-500, #c97a0a) 80%, black);
656
+ border-color: color-mix(in srgb, var(--theme-warning-500, #c97a0a) 80%, black);
657
+ }
574
658
  }
575
659
 
576
660
  // ─── ATTACHMENTS ───
@@ -1048,3 +1132,169 @@
1048
1132
  font-size: 12px;
1049
1133
  }
1050
1134
  }
1135
+
1136
+ // ─── SLA BREACH BANNER ───
1137
+ // Top-of-ticket alert when first-response or resolution SLA is exceeded.
1138
+ // Uses --theme-error-* tokens for light/dark coherence.
1139
+ .slaBanner {
1140
+ display: flex;
1141
+ align-items: center;
1142
+ gap: 12px;
1143
+ padding: 10px 14px;
1144
+ margin: 0 0 16px;
1145
+ border-radius: 8px;
1146
+ background: color-mix(in srgb, var(--theme-error-500, #c4392c) 8%, transparent);
1147
+ border: 1px solid color-mix(in srgb, var(--theme-error-500, #c4392c) 30%, transparent);
1148
+ color: var(--theme-error-500, #c4392c);
1149
+ font-size: 13px;
1150
+ line-height: 1.4;
1151
+ }
1152
+
1153
+ .slaBannerIcon {
1154
+ width: 28px;
1155
+ height: 28px;
1156
+ border-radius: 50%;
1157
+ background: var(--theme-elevation-0, #fff);
1158
+ color: var(--theme-error-500, #c4392c);
1159
+ display: inline-flex;
1160
+ align-items: center;
1161
+ justify-content: center;
1162
+ flex-shrink: 0;
1163
+ }
1164
+
1165
+ .slaBannerBody {
1166
+ flex: 1;
1167
+ display: flex;
1168
+ align-items: baseline;
1169
+ flex-wrap: wrap;
1170
+ gap: 6px;
1171
+ color: var(--theme-text);
1172
+
1173
+ strong {
1174
+ color: var(--theme-error-500, #c4392c);
1175
+ font-weight: 700;
1176
+ }
1177
+ }
1178
+
1179
+ .slaBannerSep {
1180
+ color: var(--theme-elevation-400);
1181
+ }
1182
+
1183
+ .slaBannerTarget {
1184
+ color: var(--theme-elevation-500);
1185
+ font-weight: 400;
1186
+ }
1187
+
1188
+ .slaBannerCta {
1189
+ display: inline-flex;
1190
+ align-items: center;
1191
+ gap: 4px;
1192
+ padding: 6px 12px;
1193
+ border-radius: 6px;
1194
+ border: 1px solid var(--theme-error-500, #c4392c);
1195
+ background: var(--theme-elevation-0, #fff);
1196
+ color: var(--theme-error-500, #c4392c);
1197
+ font-size: 12px;
1198
+ font-weight: 600;
1199
+ cursor: pointer;
1200
+ white-space: nowrap;
1201
+ transition: background 100ms ease;
1202
+
1203
+ &:hover {
1204
+ background: var(--theme-error-500, #c4392c);
1205
+ color: var(--theme-elevation-0, #fff);
1206
+ }
1207
+
1208
+ &:focus-visible {
1209
+ outline: 0;
1210
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--theme-error-500, #c4392c) 25%, transparent);
1211
+ }
1212
+ }
1213
+
1214
+ // ─── AI SYNTHESIS CARD (promoted top of sidebar) ───
1215
+ .aiCard {
1216
+ background: linear-gradient(180deg,
1217
+ color-mix(in srgb, var(--theme-accent-500, #6b46e8) 10%, transparent) 0%,
1218
+ var(--theme-elevation-0, #fff) 70%);
1219
+ border: 1px solid color-mix(in srgb, var(--theme-accent-500, #6b46e8) 25%, transparent);
1220
+ }
1221
+
1222
+ .aiCardTitle {
1223
+ display: flex;
1224
+ align-items: center;
1225
+ gap: 6px;
1226
+ font-size: 11px;
1227
+ font-weight: 700;
1228
+ text-transform: uppercase;
1229
+ letter-spacing: 0.06em;
1230
+ color: var(--theme-accent-500, #6b46e8);
1231
+ margin-bottom: 10px;
1232
+ }
1233
+
1234
+ .aiCardIcon {
1235
+ display: inline-flex;
1236
+ align-items: center;
1237
+ justify-content: center;
1238
+ width: 20px;
1239
+ height: 20px;
1240
+ border-radius: 50%;
1241
+ background: var(--theme-accent-500, #6b46e8);
1242
+ color: #fff;
1243
+ font-size: 11px;
1244
+ font-weight: 700;
1245
+ }
1246
+
1247
+ .aiCardLead {
1248
+ font-size: 12px;
1249
+ line-height: 1.55;
1250
+ color: var(--theme-text);
1251
+ margin: 0 0 10px;
1252
+ text-wrap: pretty;
1253
+ }
1254
+
1255
+ .aiCardChips {
1256
+ display: flex;
1257
+ flex-wrap: wrap;
1258
+ gap: 4px;
1259
+ margin-bottom: 10px;
1260
+ }
1261
+
1262
+ .aiChip {
1263
+ display: inline-flex;
1264
+ align-items: center;
1265
+ padding: 2px 8px;
1266
+ border-radius: 999px;
1267
+ font-size: 10px;
1268
+ font-weight: 600;
1269
+ color: var(--theme-text);
1270
+ background: var(--theme-elevation-0, #fff);
1271
+ border: 1px solid var(--theme-elevation-200);
1272
+ }
1273
+
1274
+ .aiCardFacts {
1275
+ list-style: none;
1276
+ padding: 0;
1277
+ margin: 0;
1278
+ display: flex;
1279
+ flex-direction: column;
1280
+ gap: 6px;
1281
+ }
1282
+
1283
+ .aiCardFact {
1284
+ display: flex;
1285
+ align-items: flex-start;
1286
+ gap: 6px;
1287
+ font-size: 12px;
1288
+ line-height: 1.5;
1289
+ color: var(--theme-text);
1290
+ }
1291
+
1292
+ .aiCardDot {
1293
+ display: inline-block;
1294
+ width: 6px;
1295
+ height: 6px;
1296
+ border-radius: 50%;
1297
+ margin-top: 6px;
1298
+ background: var(--theme-accent-500, #6b46e8);
1299
+ flex-shrink: 0;
1300
+ }