@consilioweb/payload-support 5.0.0 → 6.0.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.
Files changed (116) hide show
  1. package/README.md +249 -22
  2. package/dist/components/RichTextEditor/index.js +1 -1
  3. package/dist/components/TicketConversation/RewriteDropdown.js +1 -1
  4. package/dist/components/TicketConversation/components/AISummaryPanel.js +3 -0
  5. package/dist/components/TicketConversation/components/ActionPanels.js +13 -8
  6. package/dist/components/TicketConversation/components/ClientHistory.js +1 -0
  7. package/dist/components/TicketConversation/components/CodeBlock.js +1 -0
  8. package/dist/components/TicketConversation/components/CodeBlockInserter.js +1 -1
  9. package/dist/components/TicketConversation/components/QuickActions.js +10 -8
  10. package/dist/components/TicketConversation/components/TicketHeader.js +2 -0
  11. package/dist/components/TicketConversation/components/TimeTrackingPanel.js +15 -9
  12. package/dist/components/TicketConversation/index.d.ts +10 -0
  13. package/dist/components/TicketConversation/index.js +25 -7
  14. package/dist/index.cjs +306 -60
  15. package/dist/index.d.cts +107 -38
  16. package/dist/index.d.ts +107 -38
  17. package/dist/index.js +300 -59
  18. package/dist/styles/BillingView.module.scss +11 -11
  19. package/dist/styles/ChatView.module.scss +7 -7
  20. package/dist/styles/CommandPalette.module.scss +1 -1
  21. package/dist/styles/CrmView.module.scss +9 -9
  22. package/dist/styles/EmailTracking.module.scss +6 -6
  23. package/dist/styles/ImportConversation.module.scss +5 -5
  24. package/dist/styles/Logs.module.scss +5 -5
  25. package/dist/styles/NewTicket.module.scss +2 -2
  26. package/dist/styles/PendingEmails.module.scss +13 -13
  27. package/dist/styles/SupportDashboard.module.scss +24 -8
  28. package/dist/styles/TicketDetail.module.scss +20 -20
  29. package/dist/styles/TicketInbox.module.scss +6 -6
  30. package/dist/styles/TicketingSettings.module.scss +10 -10
  31. package/dist/styles/TimeDashboard.module.scss +6 -6
  32. package/dist/styles/_tokens.scss +3 -1
  33. package/dist/views/BillingView/client.js +15 -8
  34. package/dist/views/ChatView/client.js +2 -0
  35. package/dist/views/CrmView/client.js +2 -0
  36. package/dist/views/EmailTrackingView/client.js +23 -11
  37. package/dist/views/ImportConversationView/client.js +1 -0
  38. package/dist/views/LogsView/client.js +1 -1
  39. package/dist/views/NewTicketView/client.js +20 -13
  40. package/dist/views/PendingEmailsView/client.js +9 -3
  41. package/dist/views/SupportDashboardView/client.js +17 -19
  42. package/dist/views/TicketDetailView/client.js +23 -11
  43. package/dist/views/TicketInboxView/client.js +52 -26
  44. package/dist/views/TicketingSettingsView/TicketingSettings.module.scss +7 -7
  45. package/dist/views/TicketingSettingsView/client.js +44 -26
  46. package/dist/views/TimeDashboardView/client.js +10 -7
  47. package/dist/views/shared/ErrorBoundary.d.ts +36 -1
  48. package/dist/views/shared/ErrorBoundary.js +59 -28
  49. package/dist/views/shared/adminTokens.d.ts +15 -5
  50. package/dist/views/shared/adminTokens.js +23 -7
  51. package/dist/views/shared/icons.d.ts +49 -0
  52. package/dist/views/shared/icons.js +92 -0
  53. package/dist/views/shared/locales/en.json +15 -1
  54. package/dist/views/shared/locales/fr.json +15 -1
  55. package/package.json +6 -2
  56. package/scripts/uninstall-data.mjs +178 -0
  57. package/scripts/uninstall.mjs +184 -0
  58. package/src/collections/Tickets.ts +7 -0
  59. package/src/collections/TimeEntries.ts +11 -3
  60. package/src/components/RichTextEditor/index.tsx +1 -1
  61. package/src/components/TicketConversation/RewriteDropdown.tsx +1 -1
  62. package/src/components/TicketConversation/components/AISummaryPanel.tsx +3 -0
  63. package/src/components/TicketConversation/components/ActionPanels.tsx +13 -8
  64. package/src/components/TicketConversation/components/ClientHistory.tsx +1 -0
  65. package/src/components/TicketConversation/components/CodeBlock.tsx +1 -0
  66. package/src/components/TicketConversation/components/CodeBlockInserter.tsx +1 -1
  67. package/src/components/TicketConversation/components/QuickActions.tsx +10 -8
  68. package/src/components/TicketConversation/components/TicketHeader.tsx +2 -0
  69. package/src/components/TicketConversation/components/TimeTrackingPanel.tsx +17 -9
  70. package/src/components/TicketConversation/index.tsx +41 -7
  71. package/src/endpoints/delete-account.ts +191 -47
  72. package/src/endpoints/export-data.ts +104 -4
  73. package/src/endpoints/purge-logs.ts +6 -15
  74. package/src/index.ts +2 -0
  75. package/src/plugin.ts +51 -1
  76. package/src/portal/LiveChat.tsx +1 -1
  77. package/src/portal/auth/profile/page.tsx +3 -2
  78. package/src/portal/icons.tsx +91 -0
  79. package/src/styles/BillingView.module.scss +11 -11
  80. package/src/styles/ChatView.module.scss +7 -7
  81. package/src/styles/CommandPalette.module.scss +1 -1
  82. package/src/styles/CrmView.module.scss +9 -9
  83. package/src/styles/EmailTracking.module.scss +6 -6
  84. package/src/styles/ImportConversation.module.scss +5 -5
  85. package/src/styles/Logs.module.scss +5 -5
  86. package/src/styles/NewTicket.module.scss +2 -2
  87. package/src/styles/PendingEmails.module.scss +13 -13
  88. package/src/styles/SupportDashboard.module.scss +24 -8
  89. package/src/styles/TicketDetail.module.scss +20 -20
  90. package/src/styles/TicketInbox.module.scss +6 -6
  91. package/src/styles/TicketingSettings.module.scss +10 -10
  92. package/src/styles/TimeDashboard.module.scss +6 -6
  93. package/src/styles/_tokens.scss +3 -1
  94. package/src/types.ts +20 -0
  95. package/src/utils/retention.ts +111 -0
  96. package/src/utils/slugs.ts +15 -0
  97. package/src/views/BillingView/client.tsx +17 -8
  98. package/src/views/ChatView/client.tsx +2 -0
  99. package/src/views/CrmView/client.tsx +2 -0
  100. package/src/views/EmailTrackingView/client.tsx +18 -7
  101. package/src/views/ImportConversationView/client.tsx +1 -0
  102. package/src/views/LogsView/client.tsx +1 -1
  103. package/src/views/NewTicketView/client.tsx +22 -13
  104. package/src/views/PendingEmailsView/client.tsx +9 -3
  105. package/src/views/SupportDashboardView/client.tsx +11 -5
  106. package/src/views/TicketDetailView/client.tsx +25 -11
  107. package/src/views/TicketInboxView/client.tsx +47 -7
  108. package/src/views/TicketingSettingsView/TicketingSettings.module.scss +7 -7
  109. package/src/views/TicketingSettingsView/client.tsx +201 -144
  110. package/src/views/TimeDashboardView/client.tsx +10 -7
  111. package/src/views/shared/ErrorBoundary.tsx +95 -31
  112. package/src/views/shared/adminTokens.ts +28 -11
  113. package/src/views/shared/icons.tsx +146 -0
  114. package/src/views/shared/locales/en.json +15 -1
  115. package/src/views/shared/locales/fr.json +15 -1
  116. package/src/types/lucide-react.d.ts +0 -42
@@ -52,7 +52,7 @@
52
52
  font-family: ui-monospace, 'SF Mono', 'JetBrains Mono', Menlo, Consolas, monospace;
53
53
  font-size: 12px;
54
54
  font-weight: 400;
55
- color: var(--theme-text-tertiary, var(--theme-elevation-500));
55
+ color: var(--theme-text-tertiary, var(--theme-elevation-650));
56
56
  display: inline-flex;
57
57
  align-items: center;
58
58
  gap: 8px;
@@ -64,7 +64,7 @@
64
64
 
65
65
  > .ticketNumberCat,
66
66
  > .ticketNumberSource {
67
- color: var(--theme-text-tertiary, var(--theme-elevation-500));
67
+ color: var(--theme-text-tertiary, var(--theme-elevation-650));
68
68
  display: inline-flex;
69
69
  align-items: center;
70
70
  gap: 4px;
@@ -194,7 +194,7 @@
194
194
  text-transform: uppercase;
195
195
  letter-spacing: 0.08em;
196
196
  line-height: 1.3;
197
- color: var(--theme-text-tertiary, var(--theme-elevation-500));
197
+ color: var(--theme-text-tertiary, var(--theme-elevation-650));
198
198
  margin: 0 0 12px;
199
199
  display: flex;
200
200
  justify-content: space-between;
@@ -353,7 +353,7 @@
353
353
  font-size: 11px;
354
354
  font-weight: 500;
355
355
  line-height: 1.3;
356
- color: var(--theme-text-tertiary, var(--theme-elevation-500));
356
+ color: var(--theme-text-tertiary, var(--theme-elevation-650));
357
357
  white-space: nowrap;
358
358
  text-transform: uppercase;
359
359
  letter-spacing: 0.08em;
@@ -450,7 +450,7 @@
450
450
  font-size: 11px;
451
451
  font-weight: 500;
452
452
  line-height: 1.3;
453
- color: var(--theme-text-tertiary, var(--theme-elevation-500));
453
+ color: var(--theme-text-tertiary, var(--theme-elevation-650));
454
454
  }
455
455
 
456
456
  // Pills "Interne" / "Solution" — pill style, 22px height
@@ -499,7 +499,7 @@
499
499
  margin-top: 4px;
500
500
  font-size: 11px;
501
501
  font-weight: 500;
502
- color: var(--theme-text-tertiary, var(--theme-elevation-500));
502
+ color: var(--theme-text-tertiary, var(--theme-elevation-650));
503
503
  opacity: 0;
504
504
  transition: opacity 80ms ease;
505
505
  }
@@ -514,7 +514,7 @@
514
514
  font-size: 11px;
515
515
  font-weight: 500;
516
516
  line-height: 1.3;
517
- color: var(--theme-text-tertiary, var(--theme-elevation-500));
517
+ color: var(--theme-text-tertiary, var(--theme-elevation-650));
518
518
  display: inline-flex;
519
519
  align-items: center;
520
520
  height: auto;
@@ -749,7 +749,7 @@
749
749
  display: inline-flex;
750
750
  align-items: center;
751
751
  gap: 4px;
752
- color: var(--theme-text-tertiary, var(--theme-elevation-500));
752
+ color: var(--theme-text-tertiary, var(--theme-elevation-650));
753
753
  font-size: 11px;
754
754
  font-weight: 500;
755
755
  margin-left: auto;
@@ -910,7 +910,7 @@
910
910
  background: none;
911
911
  border: none;
912
912
  cursor: pointer;
913
- color: var(--theme-elevation-500);
913
+ color: var(--theme-elevation-650);
914
914
  font-size: 13px;
915
915
  font-weight: 600;
916
916
  padding: 0;
@@ -941,7 +941,7 @@
941
941
 
942
942
  .activityContent { flex: 1; min-width: 0; }
943
943
  .activityText { font-weight: 500; color: var(--theme-text); font-size: 12px; }
944
- .activityTime { font-size: 11px; color: var(--theme-elevation-500); margin-top: 1px; }
944
+ .activityTime { font-size: 11px; color: var(--theme-elevation-650); margin-top: 1px; }
945
945
 
946
946
  // ─── DROPDOWN ───
947
947
  .dropdown { position: relative; display: inline-block; }
@@ -1055,7 +1055,7 @@
1055
1055
 
1056
1056
  .splitPreview {
1057
1057
  font-size: 12px;
1058
- color: var(--theme-elevation-500);
1058
+ color: var(--theme-elevation-650);
1059
1059
  margin: 0 0 16px;
1060
1060
  line-height: 1.5;
1061
1061
  max-height: 80px;
@@ -1092,7 +1092,7 @@
1092
1092
  background: none;
1093
1093
  font-size: 13px;
1094
1094
  font-weight: 600;
1095
- color: var(--theme-elevation-500);
1095
+ color: var(--theme-elevation-650);
1096
1096
  cursor: pointer;
1097
1097
  &:hover { background: var(--theme-elevation-100); }
1098
1098
  }
@@ -1145,7 +1145,7 @@
1145
1145
  cursor: pointer;
1146
1146
  font-size: 11px;
1147
1147
  line-height: 1;
1148
- color: var(--theme-text-tertiary, var(--theme-elevation-500));
1148
+ color: var(--theme-text-tertiary, var(--theme-elevation-650));
1149
1149
  padding: 0;
1150
1150
  display: inline-flex;
1151
1151
  align-items: center;
@@ -1166,7 +1166,7 @@
1166
1166
  font-size: 11px;
1167
1167
  font-weight: 500;
1168
1168
  line-height: 1;
1169
- color: var(--theme-text-tertiary, var(--theme-elevation-500));
1169
+ color: var(--theme-text-tertiary, var(--theme-elevation-650));
1170
1170
  cursor: pointer;
1171
1171
  transition: border-color 80ms ease, color 80ms ease;
1172
1172
  &:hover {
@@ -1607,7 +1607,7 @@
1607
1607
  align-items: center;
1608
1608
  gap: 6px;
1609
1609
  font-size: 10px;
1610
- color: var(--theme-elevation-500);
1610
+ color: var(--theme-elevation-650);
1611
1611
  }
1612
1612
 
1613
1613
  .previousTicketsStatus {
@@ -1755,7 +1755,7 @@
1755
1755
  text-transform: uppercase;
1756
1756
  letter-spacing: 0.08em;
1757
1757
  line-height: 1.3;
1758
- color: var(--theme-text-tertiary, var(--theme-elevation-500));
1758
+ color: var(--theme-text-tertiary, var(--theme-elevation-650));
1759
1759
  margin: 12px 0 8px;
1760
1760
  }
1761
1761
  .aiCardNextActions {
@@ -1805,13 +1805,13 @@
1805
1805
  }
1806
1806
  .aiCardNextLabelDone {
1807
1807
  text-decoration: line-through;
1808
- color: var(--theme-text-tertiary, var(--theme-elevation-500));
1808
+ color: var(--theme-text-tertiary, var(--theme-elevation-650));
1809
1809
  }
1810
1810
  .aiCardNextWhen {
1811
1811
  font-size: 11px;
1812
1812
  font-weight: 500;
1813
1813
  line-height: 1.3;
1814
- color: var(--theme-text-tertiary, var(--theme-elevation-500));
1814
+ color: var(--theme-text-tertiary, var(--theme-elevation-650));
1815
1815
  flex-shrink: 0;
1816
1816
  }
1817
1817
 
@@ -1856,12 +1856,12 @@
1856
1856
  margin: 0 2px;
1857
1857
  }
1858
1858
  .ticketNumberCat {
1859
- color: var(--theme-text-tertiary, var(--theme-elevation-500));
1859
+ color: var(--theme-text-tertiary, var(--theme-elevation-650));
1860
1860
  font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
1861
1861
  font-weight: 400;
1862
1862
  }
1863
1863
  .ticketNumberSource {
1864
- color: var(--theme-text-tertiary, var(--theme-elevation-500));
1864
+ color: var(--theme-text-tertiary, var(--theme-elevation-650));
1865
1865
  font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
1866
1866
  display: inline-flex;
1867
1867
  align-items: center;
@@ -26,7 +26,7 @@
26
26
 
27
27
  .subtitle {
28
28
  font-size: 13px;
29
- color: var(--theme-elevation-500);
29
+ color: var(--theme-elevation-650);
30
30
  margin: 2px 0 0;
31
31
  }
32
32
 
@@ -125,7 +125,7 @@
125
125
  padding: 0 12px;
126
126
  font-size: 13px;
127
127
  font-weight: 500;
128
- color: var(--theme-elevation-500);
128
+ color: var(--theme-elevation-650);
129
129
  background: none;
130
130
  border: none;
131
131
  border-radius: 6px;
@@ -155,7 +155,7 @@
155
155
  padding: 0 6px;
156
156
  font-size: 11px;
157
157
  font-weight: 600;
158
- color: var(--theme-elevation-500);
158
+ color: var(--theme-elevation-650);
159
159
  background: var(--theme-elevation-200);
160
160
  border-radius: 999px;
161
161
  }
@@ -179,7 +179,7 @@
179
179
  .sortSelect {
180
180
  font-size: 12px;
181
181
  font-weight: 600;
182
- color: var(--theme-elevation-500);
182
+ color: var(--theme-elevation-650);
183
183
  background: none;
184
184
  border: none;
185
185
  cursor: pointer;
@@ -268,7 +268,7 @@
268
268
 
269
269
  .client {
270
270
  font-size: 12px;
271
- color: var(--theme-elevation-500);
271
+ color: var(--theme-elevation-650);
272
272
  overflow: hidden;
273
273
  text-overflow: ellipsis;
274
274
  white-space: nowrap;
@@ -333,7 +333,7 @@
333
333
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
334
334
  font-size: 11px;
335
335
  font-weight: 500;
336
- color: var(--theme-elevation-500);
336
+ color: var(--theme-elevation-650);
337
337
  display: flex;
338
338
  gap: 12px;
339
339
  z-index: 10;
@@ -15,7 +15,7 @@ $green: #16a34a;
15
15
 
16
16
  .intro {
17
17
  font-size: 13px;
18
- color: var(--theme-elevation-500);
18
+ color: var(--theme-elevation-650);
19
19
  margin: 0 0 24px;
20
20
  line-height: 1.6;
21
21
  }
@@ -59,7 +59,7 @@ $green: #16a34a;
59
59
 
60
60
  .sectionChevron {
61
61
  font-size: 16px;
62
- color: var(--theme-elevation-500);
62
+ color: var(--theme-elevation-650);
63
63
  transition: transform 150ms;
64
64
 
65
65
  &.open {
@@ -73,7 +73,7 @@ $green: #16a34a;
73
73
 
74
74
  .sectionDescription {
75
75
  font-size: 12px;
76
- color: var(--theme-elevation-500);
76
+ color: var(--theme-elevation-650);
77
77
  margin: 0 0 16px;
78
78
  line-height: 1.5;
79
79
  }
@@ -97,7 +97,7 @@ $green: #16a34a;
97
97
 
98
98
  .fieldDescription {
99
99
  font-size: 11px;
100
- color: var(--theme-elevation-500);
100
+ color: var(--theme-elevation-650);
101
101
  font-weight: 400;
102
102
  margin-top: 2px;
103
103
  }
@@ -131,7 +131,7 @@ $green: #16a34a;
131
131
  border-radius: 6px;
132
132
  border: 1px solid var(--theme-elevation-300);
133
133
  font-size: 13px;
134
- color: var(--theme-elevation-500);
134
+ color: var(--theme-elevation-650);
135
135
  background-color: var(--theme-elevation-100);
136
136
  width: 100%;
137
137
  max-width: 400px;
@@ -179,7 +179,7 @@ $green: #16a34a;
179
179
 
180
180
  .inlineDesc {
181
181
  font-size: 12px;
182
- color: var(--theme-elevation-500);
182
+ color: var(--theme-elevation-650);
183
183
  }
184
184
 
185
185
  // ─── BADGE ───
@@ -251,7 +251,7 @@ $green: #16a34a;
251
251
 
252
252
  .featureDesc {
253
253
  font-size: 12px;
254
- color: var(--theme-elevation-500);
254
+ color: var(--theme-elevation-650);
255
255
  line-height: 1.4;
256
256
  }
257
257
 
@@ -271,7 +271,7 @@ $green: #16a34a;
271
271
 
272
272
  .aiToggleDesc {
273
273
  font-size: 12px;
274
- color: var(--theme-elevation-500);
274
+ color: var(--theme-elevation-650);
275
275
  margin-left: 8px;
276
276
  }
277
277
 
@@ -290,7 +290,7 @@ $green: #16a34a;
290
290
  background-color: transparent;
291
291
  cursor: pointer;
292
292
  font-size: 12px;
293
- color: var(--theme-elevation-500);
293
+ color: var(--theme-elevation-650);
294
294
  white-space: nowrap;
295
295
  }
296
296
 
@@ -303,7 +303,7 @@ $green: #16a34a;
303
303
 
304
304
  .slaHint {
305
305
  font-size: 12px;
306
- color: var(--theme-elevation-500);
306
+ color: var(--theme-elevation-650);
307
307
  }
308
308
 
309
309
  // ─── AI SUB FEATURES LABEL ───
@@ -28,7 +28,7 @@
28
28
  }
29
29
 
30
30
  .subtitle {
31
- color: var(--theme-elevation-500);
31
+ color: var(--theme-elevation-650);
32
32
  margin: 4px 0 0;
33
33
  font-size: 14px;
34
34
  }
@@ -80,7 +80,7 @@
80
80
  .label {
81
81
  font-size: 11px;
82
82
  font-weight: 700;
83
- color: var(--theme-elevation-500);
83
+ color: var(--theme-elevation-650);
84
84
  margin-bottom: 3px;
85
85
  }
86
86
 
@@ -146,7 +146,7 @@
146
146
  font-size: 10px;
147
147
  font-weight: 700;
148
148
  text-transform: uppercase;
149
- color: var(--theme-elevation-500);
149
+ color: var(--theme-elevation-650);
150
150
  }
151
151
 
152
152
  .kpiValue {
@@ -201,7 +201,7 @@
201
201
  border: 1px solid var(--theme-elevation-300);
202
202
  border-radius: 8px;
203
203
  background: var(--theme-elevation-100);
204
- color: var(--theme-elevation-500);
204
+ color: var(--theme-elevation-650);
205
205
  font-size: 14px;
206
206
  }
207
207
 
@@ -257,13 +257,13 @@
257
257
 
258
258
  .tdSubject {
259
259
  padding: 6px 8px;
260
- color: var(--theme-elevation-500);
260
+ color: var(--theme-elevation-650);
261
261
  font-size: 12px;
262
262
  }
263
263
 
264
264
  .tdDescription {
265
265
  padding: 6px 8px;
266
- color: var(--theme-elevation-500);
266
+ color: var(--theme-elevation-650);
267
267
  font-size: 12px;
268
268
  }
269
269
 
@@ -1,6 +1,8 @@
1
1
  // Design tokens — based on Payload CMS CSS variables
2
2
  $text: var(--theme-text);
3
- $text-secondary: var(--theme-elevation-500);
3
+ // elevation-650, not 500: Payload's dark theme skips the 500 step, leaving it
4
+ // at rgb(128,128,128) — 3.62:1 light / 4.03:1 dark, below WCAG AA in both.
5
+ $text-secondary: var(--theme-elevation-650);
4
6
  $text-muted: var(--theme-elevation-400);
5
7
  $bg: var(--theme-elevation-50);
6
8
  $bg-card: var(--theme-elevation-100);
package/src/types.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { RateLimitStore } from './utils/rateLimiter'
2
+ import type { RetentionConfig } from './utils/retention'
2
3
  import type { InboundEmailInput } from './utils/webhookSecurity'
3
4
  import type { Payload, PayloadRequest } from 'payload'
4
5
 
@@ -151,6 +152,25 @@ export interface SupportPluginConfig {
151
152
  /** Shared rate-limit storage. Defaults to process-local memory. */
152
153
  rateLimitStore?: RateLimitStore | 'payload'
153
154
 
155
+ /**
156
+ * Automatic retention of the two journals the plugin writes.
157
+ *
158
+ * The plugin registers a daily Payload Jobs task that deletes rows older
159
+ * than the configured number of days. Set a value to 0 to keep that journal
160
+ * forever (the manual `DELETE /api/support/purge-logs` endpoint stays
161
+ * available either way).
162
+ *
163
+ * Defaults: auth-logs 180 days, email-logs 365 days, cron `0 30 3 * * *`.
164
+ *
165
+ * `false` registers no task at all. That is the ONLY value that changes the
166
+ * schema footprint: registering a task turns Payload's job queue on, which
167
+ * adds the `payload-jobs` collection and the `payload-jobs-stats` global to
168
+ * an app that had no jobs — two tables you would have to migrate. Pass
169
+ * `false` if you would rather purge from your own cron, calling
170
+ * `DELETE /api/support/purge-logs`.
171
+ */
172
+ retention?: RetentionConfig | false
173
+
154
174
  /** Sequential ticket number formatting. */
155
175
  ticketNumber?: { prefix?: string; padding?: number }
156
176
 
@@ -0,0 +1,111 @@
1
+ import type { Payload, PayloadRequest } from 'payload'
2
+ import type { CollectionSlugs } from './slugs'
3
+ import { dbDelete } from './db'
4
+
5
+ /**
6
+ * How long the two journals are kept, in days.
7
+ *
8
+ * Defaults follow the CNIL guidance the README documents: six months would be
9
+ * the floor for connection logs, a year is the ceiling usually accepted, and
10
+ * these two values sit inside that range while staying long enough to
11
+ * investigate an intrusion. Set a value to 0 to disable the automatic purge of
12
+ * that journal — never to mean "delete everything", which is what `days=0`
13
+ * means on the manual endpoint.
14
+ */
15
+ export interface RetentionConfig {
16
+ /** Retention of `auth-logs` in days. `0` disables the scheduled purge. */
17
+ authLogsDays?: number
18
+ /** Retention of `email-logs` in days. `0` disables the scheduled purge. */
19
+ emailLogsDays?: number
20
+ /**
21
+ * Cron for the scheduled purge. Payload's cron takes an optional leading
22
+ * seconds field. Default: every day at 03:30.
23
+ */
24
+ cron?: string
25
+ /** Queue the scheduled job is pushed to. Default: `default`. */
26
+ queue?: string
27
+ }
28
+
29
+ export const DEFAULT_RETENTION = {
30
+ authLogsDays: 180,
31
+ emailLogsDays: 365,
32
+ cron: '0 30 3 * * *',
33
+ queue: 'default',
34
+ } as const
35
+
36
+ export const PURGE_LOGS_TASK_SLUG = 'support-purge-logs'
37
+
38
+ /** Journals the purge is allowed to touch, keyed by their public name. */
39
+ export function purgeableCollections(slugs: CollectionSlugs): Record<string, string> {
40
+ return {
41
+ 'email-logs': slugs.emailLogs,
42
+ 'auth-logs': slugs.authLogs,
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Delete rows of `slug` created more than `days` days ago.
48
+ *
49
+ * `days <= 0` means "everything" and is only reachable from the manual
50
+ * admin-only endpoint. The scheduled task refuses it: a misconfigured cron
51
+ * that wipes the whole security journal every night is a far worse outcome
52
+ * than a journal that grows.
53
+ */
54
+ export async function purgeOlderThan(
55
+ payload: Payload,
56
+ slug: string,
57
+ days: number,
58
+ req?: PayloadRequest,
59
+ ): Promise<number> {
60
+ const cutoff = days > 0 ? new Date(Date.now() - days * 86400000).toISOString() : null
61
+
62
+ const result = await dbDelete(payload, slug, {
63
+ where: cutoff
64
+ ? { createdAt: { less_than: cutoff } }
65
+ : { id: { exists: true } },
66
+ overrideAccess: true,
67
+ ...(req ? { req } : {}),
68
+ })
69
+
70
+ return Array.isArray(result?.docs) ? result.docs.length : 0
71
+ }
72
+
73
+ export interface ScheduledPurgeResult {
74
+ purged: Record<string, number>
75
+ skipped: string[]
76
+ }
77
+
78
+ /**
79
+ * The scheduled half of the purge. Never accepts `days <= 0`, and silently
80
+ * skips a journal whose collection is not registered (its feature flag is off).
81
+ */
82
+ export async function runScheduledPurge(
83
+ payload: Payload,
84
+ slugs: CollectionSlugs,
85
+ retention: RetentionConfig | false | undefined,
86
+ req?: PayloadRequest,
87
+ ): Promise<ScheduledPurgeResult> {
88
+ if (retention === false) return { purged: {}, skipped: ['auth-logs', 'email-logs'] }
89
+
90
+ const plan: Array<[string, string, number]> = [
91
+ ['auth-logs', slugs.authLogs, retention?.authLogsDays ?? DEFAULT_RETENTION.authLogsDays],
92
+ ['email-logs', slugs.emailLogs, retention?.emailLogsDays ?? DEFAULT_RETENTION.emailLogsDays],
93
+ ]
94
+
95
+ const purged: Record<string, number> = {}
96
+ const skipped: string[] = []
97
+
98
+ for (const [name, slug, days] of plan) {
99
+ if (!Number.isFinite(days) || days <= 0) {
100
+ skipped.push(name)
101
+ continue
102
+ }
103
+ if (!(payload.collections as Record<string, unknown> | undefined)?.[slug]) {
104
+ skipped.push(name)
105
+ continue
106
+ }
107
+ purged[name] = await purgeOlderThan(payload, slug, days, req)
108
+ }
109
+
110
+ return { purged, skipped }
111
+ }
@@ -58,6 +58,21 @@ export const DEFAULT_SLUGS: CollectionSlugs = {
58
58
  media: 'media',
59
59
  }
60
60
 
61
+ /**
62
+ * Collections the plugin registers under a FIXED slug.
63
+ *
64
+ * Unlike everything in `CollectionSlugs`, these three hardcode their `slug`
65
+ * (ClientSummaries.ts, TicketCollaborators.ts, TicketFeedback.ts), so any code
66
+ * that reads or deletes their rows must use these literals rather than a
67
+ * configurable slug — otherwise an app that overrides `collectionSlugs` would
68
+ * address a collection that does not exist.
69
+ */
70
+ export const FIXED_SLUGS = {
71
+ clientSummaries: 'client-summaries',
72
+ ticketCollaborators: 'ticket-collaborators',
73
+ ticketFeedback: 'ticket-feedback',
74
+ } as const
75
+
61
76
  /**
62
77
  * Resolve collection slugs merging user overrides with defaults.
63
78
  */
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import React, { useState, useCallback } from 'react'
3
+ import React, { useState, useCallback, useId } from 'react'
4
4
  import { useTranslation } from '../../components/TicketConversation/hooks/useTranslation'
5
5
  import { DATE_LOCALE } from '../shared/dateLocale'
6
6
  import styles from '../../styles/BillingView.module.scss'
@@ -86,6 +86,12 @@ function getQuarterRange(offset: number): { from: string; to: string } {
86
86
 
87
87
  export const BillingClient: React.FC = () => {
88
88
  const { t } = useTranslation()
89
+ // The `<label>`s in the filter row already carry the translated text; they
90
+ // just were not associated with their control.
91
+ const fromId = useId()
92
+ const toId = useId()
93
+ const projectFilterId = useId()
94
+ const rateId = useId()
89
95
  const [from, setFrom] = useState(() => getMonthRange(0).from)
90
96
  const [to, setTo] = useState(() => getMonthRange(0).to)
91
97
  const [projectId, setProjectId] = useState('')
@@ -287,16 +293,16 @@ export const BillingClient: React.FC = () => {
287
293
 
288
294
  <div className={styles.filterRow}>
289
295
  <div className={styles.fieldGroup}>
290
- <label className={styles.label}>{t('billing.filters.from')}</label>
291
- <input type="date" value={from} onChange={(e) => setFrom(e.target.value)} className={styles.input} />
296
+ <label className={styles.label} htmlFor={fromId}>{t('billing.filters.from')}</label>
297
+ <input id={fromId} type="date" value={from} onChange={(e) => setFrom(e.target.value)} className={styles.input} />
292
298
  </div>
293
299
  <div className={styles.fieldGroup}>
294
- <label className={styles.label}>{t('billing.filters.to')}</label>
295
- <input type="date" value={to} onChange={(e) => setTo(e.target.value)} className={styles.input} />
300
+ <label className={styles.label} htmlFor={toId}>{t('billing.filters.to')}</label>
301
+ <input id={toId} type="date" value={to} onChange={(e) => setTo(e.target.value)} className={styles.input} />
296
302
  </div>
297
303
  <div className={styles.fieldGroup}>
298
- <label className={styles.label}>{t('billing.filters.project')}</label>
299
- <select value={projectId} onChange={(e) => setProjectId(e.target.value)} className={styles.select}>
304
+ <label className={styles.label} htmlFor={projectFilterId}>{t('billing.filters.project')}</label>
305
+ <select id={projectFilterId} value={projectId} onChange={(e) => setProjectId(e.target.value)} className={styles.select}>
300
306
  <option value="">{t('ticket.allProjects')}</option>
301
307
  {projects.map((p) => (
302
308
  <option key={p.id} value={p.id}>{p.name}</option>
@@ -304,9 +310,10 @@ export const BillingClient: React.FC = () => {
304
310
  </select>
305
311
  </div>
306
312
  <div className={styles.fieldGroup}>
307
- <label className={styles.label}>{t('billing.filters.hourlyRate')}</label>
313
+ <label className={styles.label} htmlFor={rateId}>{t('billing.filters.hourlyRate')}</label>
308
314
  <div className={styles.rateRow}>
309
315
  <input
316
+ id={rateId}
310
317
  type="number"
311
318
  value={rate}
312
319
  onChange={(e) => setRate(Number(e.target.value))}
@@ -370,6 +377,7 @@ export const BillingClient: React.FC = () => {
370
377
  <th className={styles.thCheckbox}>
371
378
  <input
372
379
  type="checkbox"
380
+ aria-label={t('billing.tableCheckboxTitle')}
373
381
  checked={group.tickets.every((t) => billedTickets.has(t.id))}
374
382
  onChange={() => {
375
383
  const ids = group.tickets.map((t) => t.id)
@@ -405,6 +413,7 @@ export const BillingClient: React.FC = () => {
405
413
  <td className={styles.td} rowSpan={rowSpan} style={{ verticalAlign: 'middle' }}>
406
414
  <input
407
415
  type="checkbox"
416
+ aria-label={isBilled ? t('billing.markUnbilledTitle') : t('billing.markBilledTitle')}
408
417
  checked={isBilled}
409
418
  onChange={() => toggleBilled(ticket.id)}
410
419
  className={styles.checkbox}
@@ -343,6 +343,7 @@ export const ChatViewClient: React.FC = () => {
343
343
  <form onSubmit={sendMessage} className={styles.composer}>
344
344
  {cannedResponses.length > 0 && (
345
345
  <select
346
+ aria-label={t('chat.quickReply')}
346
347
  onChange={(e) => {
347
348
  const cr = cannedResponses.find((c) => String(c.id) === e.target.value)
348
349
  if (cr) setInput(cr.body)
@@ -359,6 +360,7 @@ export const ChatViewClient: React.FC = () => {
359
360
  <div className={styles.composerRow}>
360
361
  <input
361
362
  type="text"
363
+ aria-label={t('chat.inputPlaceholder')}
362
364
  value={input}
363
365
  onChange={(e) => setInput(e.target.value)}
364
366
  placeholder={t('chat.inputPlaceholder')}
@@ -275,6 +275,7 @@ export const CrmClient: React.FC = () => {
275
275
  <div className={styles.sidebarSearch}>
276
276
  <input
277
277
  type="text"
278
+ aria-label={t('crm.searchLabel')}
278
279
  placeholder={t('crm.searchPlaceholder')}
279
280
  value={search}
280
281
  onChange={(e) => setSearch(e.target.value)}
@@ -379,6 +380,7 @@ export const CrmClient: React.FC = () => {
379
380
  </p>
380
381
  <input
381
382
  type="text"
383
+ aria-label={t('crm.mergeTitle')}
382
384
  value={mergeSearch}
383
385
  onChange={(e) => setMergeSearch(e.target.value)}
384
386
  placeholder={t('crm.mergeSearchPlaceholder')}