@redacto.io/consent-sdk-react 9.2.0-beta.0 → 9.2.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +15 -10
- package/CHANGELOG.md +6 -0
- package/dist/privacy-center.d.mts +56 -2
- package/dist/privacy-center.d.ts +56 -2
- package/dist/privacy-center.js +4213 -1256
- package/dist/privacy-center.mjs +4030 -1073
- package/package.json +1 -1
- package/src/RedactoPrivacyCenter/PrivacyCenterHomePage.tsx +3 -0
- package/src/RedactoPrivacyCenter/PrivacyCenterMyReceiptPage.tsx +26 -0
- package/src/RedactoPrivacyCenter/api/actions.ts +66 -0
- package/src/RedactoPrivacyCenter/api/client.ts +79 -0
- package/src/RedactoPrivacyCenter/components/DataRequest/ProductSelectionSection.tsx +222 -0
- package/src/RedactoPrivacyCenter/components/Form.tsx +828 -33
- package/src/RedactoPrivacyCenter/components/ReceiptsList.tsx +492 -0
- package/src/RedactoPrivacyCenter/components/RequestSubmit.tsx +284 -71
- package/src/RedactoPrivacyCenter/components/SideMenuBarPC/constants.tsx +6 -1
- package/src/RedactoPrivacyCenter/components/groupHelpers.ts +56 -0
- package/src/RedactoPrivacyCenter/components/receiptColumns.tsx +203 -0
- package/src/RedactoPrivacyCenter/components/types.ts +26 -2
- package/src/RedactoPrivacyCenter/context/NavigationContext.tsx +2 -2
- package/src/RedactoPrivacyCenter/lib/api.ts +95 -1
- package/src/RedactoPrivacyCenter/lib/constants.ts +5 -0
- package/src/RedactoPrivacyCenter/lib/types.ts +105 -0
- package/src/RedactoPrivacyCenter/locales/as/translation.json +39 -1
- package/src/RedactoPrivacyCenter/locales/bho/translation.json +39 -1
- package/src/RedactoPrivacyCenter/locales/bn/translation.json +39 -1
- package/src/RedactoPrivacyCenter/locales/brx/translation.json +39 -1
- package/src/RedactoPrivacyCenter/locales/doi/translation.json +39 -1
- package/src/RedactoPrivacyCenter/locales/en/translation.json +44 -1
- package/src/RedactoPrivacyCenter/locales/gom/translation.json +39 -1
- package/src/RedactoPrivacyCenter/locales/gu/translation.json +39 -1
- package/src/RedactoPrivacyCenter/locales/hi/translation.json +39 -1
- package/src/RedactoPrivacyCenter/locales/kn/translation.json +39 -1
- package/src/RedactoPrivacyCenter/locales/ks/translation.json +39 -1
- package/src/RedactoPrivacyCenter/locales/mai/translation.json +39 -1
- package/src/RedactoPrivacyCenter/locales/ml/translation.json +39 -1
- package/src/RedactoPrivacyCenter/locales/mni-Mtei/translation.json +39 -1
- package/src/RedactoPrivacyCenter/locales/mr/translation.json +39 -1
- package/src/RedactoPrivacyCenter/locales/ne/translation.json +39 -1
- package/src/RedactoPrivacyCenter/locales/or/translation.json +39 -1
- package/src/RedactoPrivacyCenter/locales/pa/translation.json +39 -1
- package/src/RedactoPrivacyCenter/locales/sa/translation.json +39 -1
- package/src/RedactoPrivacyCenter/locales/sat/translation.json +39 -1
- package/src/RedactoPrivacyCenter/locales/sd/translation.json +39 -1
- package/src/RedactoPrivacyCenter/locales/ta/translation.json +39 -1
- package/src/RedactoPrivacyCenter/locales/te/translation.json +39 -1
- package/src/RedactoPrivacyCenter/locales/ur/translation.json +39 -1
- package/src/RedactoPrivacyCenter/styles/injectStyles.ts +301 -0
- package/tests/Form.test.tsx +1170 -0
- package/tests/ProductSelectionSection.test.tsx +293 -0
- package/tests/RequestSubmit.test.tsx +295 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from 'react';
|
|
1
2
|
import character from '../assets/character.svg';
|
|
2
3
|
import greenTick from '../assets/greenTick.svg';
|
|
3
4
|
import sentImg from '../assets/sentImg.svg';
|
|
@@ -9,14 +10,234 @@ import { Mail } from 'lucide-react';
|
|
|
9
10
|
|
|
10
11
|
const getVar = (varName: string): string => `var(${varName})`;
|
|
11
12
|
|
|
13
|
+
type SummaryCardProps = Readonly<{
|
|
14
|
+
children: ReactNode;
|
|
15
|
+
backgroundColor?: CSSProperties['backgroundColor'];
|
|
16
|
+
borderColor?: CSSProperties['borderColor'];
|
|
17
|
+
}>;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Shared visual container for the post-submit success / partial-success
|
|
21
|
+
* blocks. Same shape is used for the single Case-ID card, the multi-case
|
|
22
|
+
* list, and the failures list (with warning colors overriding the
|
|
23
|
+
* defaults).
|
|
24
|
+
*/
|
|
25
|
+
const SummaryCard = ({
|
|
26
|
+
children,
|
|
27
|
+
backgroundColor,
|
|
28
|
+
borderColor,
|
|
29
|
+
}: SummaryCardProps) => (
|
|
30
|
+
<div
|
|
31
|
+
style={{
|
|
32
|
+
marginTop: pcStyles.mtMd.marginTop,
|
|
33
|
+
padding: pcStyles.pSm.padding,
|
|
34
|
+
...(backgroundColor === undefined
|
|
35
|
+
? pcStyles.bgSecondary
|
|
36
|
+
: { backgroundColor }),
|
|
37
|
+
borderRadius: pcStyles.borderRadiusMd,
|
|
38
|
+
border: `1px solid ${borderColor ?? pcStyles.borderColor}`,
|
|
39
|
+
}}
|
|
40
|
+
>
|
|
41
|
+
{children}
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Success / partial-success surface shown after a case (or batch of cases)
|
|
47
|
+
* is submitted. See `RequestSubmitProps` for the shape contract.
|
|
48
|
+
*
|
|
49
|
+
* Rendering rules:
|
|
50
|
+
* - Exactly one succeeded entry + no failures → single Case-ID card
|
|
51
|
+
* (same look as the pre-P2-SDK-4 behaviour). Used by legacy single-
|
|
52
|
+
* case paths (nomination / grievance / non-grouped DSAR fallback).
|
|
53
|
+
* - More than one succeeded entry → list of `product_name: case_id`
|
|
54
|
+
* rows under a "{n} cases created" header.
|
|
55
|
+
* - Any failed entries → a separate "Some requests failed" block with
|
|
56
|
+
* a per-row Retry button that calls `onRetry(product_uuid)`.
|
|
57
|
+
*/
|
|
12
58
|
export default function RequestSubmit({
|
|
13
|
-
|
|
59
|
+
result,
|
|
14
60
|
contact,
|
|
15
61
|
onCreateRequest,
|
|
62
|
+
onRetry,
|
|
63
|
+
retryingGroupKeys,
|
|
16
64
|
revokeConsentOnFulfilment,
|
|
17
65
|
revokedPurposeNames,
|
|
18
66
|
}: RequestSubmitProps) {
|
|
19
67
|
const { t } = useTranslation();
|
|
68
|
+
|
|
69
|
+
const succeeded = result.succeeded;
|
|
70
|
+
const failed = result.failed;
|
|
71
|
+
const successCount = succeeded.length;
|
|
72
|
+
const hasFailures = failed.length > 0;
|
|
73
|
+
const isSingleCase = successCount === 1 && !hasFailures;
|
|
74
|
+
|
|
75
|
+
const renderCaseList = () => {
|
|
76
|
+
if (successCount === 0) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
if (isSingleCase) {
|
|
80
|
+
const only = succeeded[0];
|
|
81
|
+
return (
|
|
82
|
+
<SummaryCard>
|
|
83
|
+
{only.product_name !== '' && (
|
|
84
|
+
<PCText
|
|
85
|
+
size="sm"
|
|
86
|
+
weight="medium"
|
|
87
|
+
style={{
|
|
88
|
+
marginBottom: pcStyles.mbXs.marginBottom,
|
|
89
|
+
wordBreak: 'break-word',
|
|
90
|
+
}}
|
|
91
|
+
>
|
|
92
|
+
{only.product_name}
|
|
93
|
+
</PCText>
|
|
94
|
+
)}
|
|
95
|
+
<PCText
|
|
96
|
+
size="base"
|
|
97
|
+
weight="bold"
|
|
98
|
+
style={{
|
|
99
|
+
color: pcStyles.primary,
|
|
100
|
+
wordBreak: 'break-word',
|
|
101
|
+
marginBottom: pcStyles.mbXs.marginBottom,
|
|
102
|
+
}}
|
|
103
|
+
className="pc-text-responsive"
|
|
104
|
+
>
|
|
105
|
+
{t('caseID')}: {only.case_id}
|
|
106
|
+
</PCText>
|
|
107
|
+
<PCText size="xs" weight="medium" color="secondary">
|
|
108
|
+
{t('saveThisID')}.
|
|
109
|
+
</PCText>
|
|
110
|
+
</SummaryCard>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
return (
|
|
114
|
+
<SummaryCard>
|
|
115
|
+
<PCText
|
|
116
|
+
size="sm"
|
|
117
|
+
weight="semibold"
|
|
118
|
+
style={{
|
|
119
|
+
color: pcStyles.primary,
|
|
120
|
+
marginBottom: pcStyles.mbSm.marginBottom,
|
|
121
|
+
}}
|
|
122
|
+
>
|
|
123
|
+
{t('casesCreatedCount', { count: successCount })}
|
|
124
|
+
</PCText>
|
|
125
|
+
<ul
|
|
126
|
+
style={{
|
|
127
|
+
margin: 0,
|
|
128
|
+
padding: 0,
|
|
129
|
+
listStyle: 'none',
|
|
130
|
+
display: 'flex',
|
|
131
|
+
flexDirection: 'column',
|
|
132
|
+
gap: getVar('--pc-spacing-xs'),
|
|
133
|
+
}}
|
|
134
|
+
>
|
|
135
|
+
{succeeded.map((entry) => (
|
|
136
|
+
<li
|
|
137
|
+
key={`${entry.group_key}:${entry.case_id}`}
|
|
138
|
+
style={{ wordBreak: 'break-word' }}
|
|
139
|
+
>
|
|
140
|
+
<PCText size="sm" weight="medium">
|
|
141
|
+
{entry.product_name}
|
|
142
|
+
</PCText>
|
|
143
|
+
<PCText
|
|
144
|
+
size="sm"
|
|
145
|
+
weight="bold"
|
|
146
|
+
style={{ color: pcStyles.primary, wordBreak: 'break-word' }}
|
|
147
|
+
>
|
|
148
|
+
{t('caseID')}: {entry.case_id}
|
|
149
|
+
</PCText>
|
|
150
|
+
</li>
|
|
151
|
+
))}
|
|
152
|
+
</ul>
|
|
153
|
+
<PCText
|
|
154
|
+
size="xs"
|
|
155
|
+
weight="medium"
|
|
156
|
+
color="secondary"
|
|
157
|
+
style={{ marginTop: pcStyles.mtSm.marginTop }}
|
|
158
|
+
>
|
|
159
|
+
{t('saveThisID')}.
|
|
160
|
+
</PCText>
|
|
161
|
+
</SummaryCard>
|
|
162
|
+
);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const renderFailures = () => {
|
|
166
|
+
if (!hasFailures) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
return (
|
|
170
|
+
<SummaryCard
|
|
171
|
+
backgroundColor={pcStyles.warningBg}
|
|
172
|
+
borderColor={pcStyles.warningBorder}
|
|
173
|
+
>
|
|
174
|
+
<PCText
|
|
175
|
+
size="sm"
|
|
176
|
+
weight="semibold"
|
|
177
|
+
style={{
|
|
178
|
+
color: pcStyles.warningText,
|
|
179
|
+
marginBottom: pcStyles.mbSm.marginBottom,
|
|
180
|
+
}}
|
|
181
|
+
>
|
|
182
|
+
{t('someRequestsFailed')}
|
|
183
|
+
</PCText>
|
|
184
|
+
<ul
|
|
185
|
+
style={{
|
|
186
|
+
margin: 0,
|
|
187
|
+
padding: 0,
|
|
188
|
+
listStyle: 'none',
|
|
189
|
+
display: 'flex',
|
|
190
|
+
flexDirection: 'column',
|
|
191
|
+
gap: getVar('--pc-spacing-sm'),
|
|
192
|
+
}}
|
|
193
|
+
>
|
|
194
|
+
{failed.map((entry) => {
|
|
195
|
+
const isRetrying =
|
|
196
|
+
retryingGroupKeys?.has(entry.group_key) === true;
|
|
197
|
+
return (
|
|
198
|
+
<li
|
|
199
|
+
key={entry.group_key}
|
|
200
|
+
style={{
|
|
201
|
+
display: 'flex',
|
|
202
|
+
alignItems: 'flex-start',
|
|
203
|
+
justifyContent: 'space-between',
|
|
204
|
+
gap: getVar('--pc-spacing-sm'),
|
|
205
|
+
flexWrap: 'wrap',
|
|
206
|
+
}}
|
|
207
|
+
>
|
|
208
|
+
<div
|
|
209
|
+
style={{
|
|
210
|
+
minWidth: 0,
|
|
211
|
+
flex: 1,
|
|
212
|
+
wordBreak: 'break-word',
|
|
213
|
+
}}
|
|
214
|
+
>
|
|
215
|
+
<PCText size="sm" weight="medium">
|
|
216
|
+
{entry.product_name}
|
|
217
|
+
</PCText>
|
|
218
|
+
<PCText size="xs" weight="medium" color="secondary">
|
|
219
|
+
{entry.error}
|
|
220
|
+
</PCText>
|
|
221
|
+
</div>
|
|
222
|
+
{onRetry !== undefined && (
|
|
223
|
+
<PCButton
|
|
224
|
+
variant="outline"
|
|
225
|
+
size="sm"
|
|
226
|
+
onClick={() => onRetry(entry.group_key)}
|
|
227
|
+
disabled={isRetrying}
|
|
228
|
+
isLoading={isRetrying}
|
|
229
|
+
>
|
|
230
|
+
{isRetrying ? t('retrying') : t('retry')}
|
|
231
|
+
</PCButton>
|
|
232
|
+
)}
|
|
233
|
+
</li>
|
|
234
|
+
);
|
|
235
|
+
})}
|
|
236
|
+
</ul>
|
|
237
|
+
</SummaryCard>
|
|
238
|
+
);
|
|
239
|
+
};
|
|
240
|
+
|
|
20
241
|
return (
|
|
21
242
|
<div style={{ marginTop: pcStyles.mtMd.marginTop, position: 'relative' }}>
|
|
22
243
|
<img
|
|
@@ -33,37 +254,47 @@ export default function RequestSubmit({
|
|
|
33
254
|
filter: 'brightness(0) saturate(100%) invert(var(--pc-icon-filter, 0))',
|
|
34
255
|
}}
|
|
35
256
|
/>
|
|
36
|
-
<div
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}} className="pc-flex-responsive">
|
|
50
|
-
<div style={{
|
|
257
|
+
<div
|
|
258
|
+
style={{
|
|
259
|
+
...pcStyles.card,
|
|
260
|
+
borderRadius: pcStyles.borderRadiusXl,
|
|
261
|
+
padding: pcStyles.pLg.padding,
|
|
262
|
+
position: 'relative',
|
|
263
|
+
...pcStyles.bgPrimary,
|
|
264
|
+
border: `1px solid ${pcStyles.borderColor}`,
|
|
265
|
+
}}
|
|
266
|
+
className="pc-padding-responsive"
|
|
267
|
+
>
|
|
268
|
+
<div
|
|
269
|
+
style={{
|
|
51
270
|
...pcStyles.flex,
|
|
271
|
+
...pcStyles.flexCol,
|
|
52
272
|
...pcStyles.itemsStart,
|
|
53
273
|
...pcStyles.gapMd,
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
274
|
+
}}
|
|
275
|
+
className="pc-flex-responsive"
|
|
276
|
+
>
|
|
277
|
+
<div
|
|
278
|
+
style={{
|
|
279
|
+
...pcStyles.flex,
|
|
280
|
+
...pcStyles.itemsStart,
|
|
281
|
+
...pcStyles.gapMd,
|
|
282
|
+
width: '100%',
|
|
283
|
+
}}
|
|
284
|
+
>
|
|
285
|
+
<div
|
|
286
|
+
style={{
|
|
287
|
+
height: '2.5rem',
|
|
288
|
+
width: '2.5rem',
|
|
289
|
+
flexShrink: 0,
|
|
290
|
+
display: 'flex',
|
|
291
|
+
alignItems: 'center',
|
|
292
|
+
justifyContent: 'center',
|
|
293
|
+
borderRadius: '50%',
|
|
294
|
+
backgroundColor: `${getVar('--pc-success')}33`,
|
|
295
|
+
padding: '0.375rem',
|
|
296
|
+
}}
|
|
297
|
+
>
|
|
67
298
|
<img
|
|
68
299
|
src={greenTick}
|
|
69
300
|
alt="success"
|
|
@@ -94,42 +325,19 @@ export default function RequestSubmit({
|
|
|
94
325
|
>
|
|
95
326
|
{t('thankYouForSubmittingYourRequest')}.
|
|
96
327
|
</PCText>
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
marginBottom: pcStyles.mbXs.marginBottom,
|
|
111
|
-
}}
|
|
112
|
-
className="pc-text-responsive"
|
|
113
|
-
>
|
|
114
|
-
{t('caseID')}: {caseId}
|
|
115
|
-
</PCText>
|
|
116
|
-
<PCText
|
|
117
|
-
size="xs"
|
|
118
|
-
weight="medium"
|
|
119
|
-
color="secondary"
|
|
120
|
-
>
|
|
121
|
-
{t('saveThisID')}.
|
|
122
|
-
</PCText>
|
|
123
|
-
</div>
|
|
124
|
-
<div style={{
|
|
125
|
-
...pcStyles.flex,
|
|
126
|
-
...pcStyles.itemsStart,
|
|
127
|
-
...pcStyles.gapSm,
|
|
128
|
-
marginTop: pcStyles.mtMd.marginTop,
|
|
129
|
-
padding: '0.5rem 0.625rem',
|
|
130
|
-
...pcStyles.bgSecondary,
|
|
131
|
-
borderRadius: pcStyles.borderRadiusMd,
|
|
132
|
-
}}>
|
|
328
|
+
{renderCaseList()}
|
|
329
|
+
{renderFailures()}
|
|
330
|
+
<div
|
|
331
|
+
style={{
|
|
332
|
+
...pcStyles.flex,
|
|
333
|
+
...pcStyles.itemsStart,
|
|
334
|
+
...pcStyles.gapSm,
|
|
335
|
+
marginTop: pcStyles.mtMd.marginTop,
|
|
336
|
+
padding: '0.5rem 0.625rem',
|
|
337
|
+
...pcStyles.bgSecondary,
|
|
338
|
+
borderRadius: pcStyles.borderRadiusMd,
|
|
339
|
+
}}
|
|
340
|
+
>
|
|
133
341
|
<Mail
|
|
134
342
|
size={16}
|
|
135
343
|
style={{
|
|
@@ -187,11 +395,13 @@ export default function RequestSubmit({
|
|
|
187
395
|
opacity: 0.9,
|
|
188
396
|
}}
|
|
189
397
|
/>
|
|
190
|
-
<div
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
398
|
+
<div
|
|
399
|
+
style={{
|
|
400
|
+
marginTop: pcStyles.mtMd.marginTop,
|
|
401
|
+
...pcStyles.flex,
|
|
402
|
+
...pcStyles.justifyCenter,
|
|
403
|
+
}}
|
|
404
|
+
>
|
|
195
405
|
<PCButton
|
|
196
406
|
variant="primary"
|
|
197
407
|
size="sm"
|
|
@@ -203,7 +413,10 @@ export default function RequestSubmit({
|
|
|
203
413
|
</div>
|
|
204
414
|
</div>
|
|
205
415
|
</div>
|
|
206
|
-
<div
|
|
416
|
+
<div
|
|
417
|
+
className="pc-block-xs pc-hidden-md"
|
|
418
|
+
style={{ marginTop: pcStyles.mtMd.marginTop }}
|
|
419
|
+
>
|
|
207
420
|
<div style={{ ...pcStyles.flex, ...pcStyles.justifyCenter }}>
|
|
208
421
|
<PCButton
|
|
209
422
|
variant="primary"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Activity, MessageCircleWarning, ShieldCheck } from 'lucide-react';
|
|
1
|
+
import { Activity, MessageCircleWarning, ReceiptText, ShieldCheck } from 'lucide-react';
|
|
2
2
|
import type { MenuItem } from './types';
|
|
3
3
|
|
|
4
4
|
export const menuItemsPC: MenuItem[] = [
|
|
@@ -17,4 +17,9 @@ export const menuItemsPC: MenuItem[] = [
|
|
|
17
17
|
href: 'activity',
|
|
18
18
|
icon: <Activity size={18} strokeWidth={1.75} />,
|
|
19
19
|
},
|
|
20
|
+
{
|
|
21
|
+
name: 'My Receipt',
|
|
22
|
+
href: 'receipts',
|
|
23
|
+
icon: <ReceiptText size={18} strokeWidth={1.75} />,
|
|
24
|
+
},
|
|
20
25
|
];
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { DataElement, FormProductGroup, Purpose } from '../lib/types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Build the per-purpose state used by `PurposeSelectionSection` for a single
|
|
5
|
+
* product group. Mirrors the legacy `data.purposes`-derived shape so the
|
|
6
|
+
* existing checkbox component works unchanged: data-elements default to
|
|
7
|
+
* `selected: false` and purposes are filtered to those with at least one
|
|
8
|
+
* data element.
|
|
9
|
+
*
|
|
10
|
+
* Lives in its own module so both `Form.tsx` (which seeds the per-group
|
|
11
|
+
* state Map) and `ProductSelectionSection.tsx` (which computes the
|
|
12
|
+
* activeCount shown on each product card) can share the same filter —
|
|
13
|
+
* otherwise the card header drifts from the list-body it summarises.
|
|
14
|
+
*/
|
|
15
|
+
export const buildGroupPurposes = (group: FormProductGroup): Purpose[] =>
|
|
16
|
+
group.purposes
|
|
17
|
+
.filter((p): p is Purpose => p !== null && p !== undefined)
|
|
18
|
+
.map((p) => {
|
|
19
|
+
const validDataElements = Array.isArray(p.data_elements)
|
|
20
|
+
? p.data_elements
|
|
21
|
+
.filter((de) => de !== null && de !== undefined && Boolean(de.uuid))
|
|
22
|
+
.map((de) => ({ ...de, selected: false }))
|
|
23
|
+
: [];
|
|
24
|
+
return {
|
|
25
|
+
...p,
|
|
26
|
+
selected: false,
|
|
27
|
+
data_elements: validDataElements,
|
|
28
|
+
};
|
|
29
|
+
})
|
|
30
|
+
.filter((p) => p.data_elements.length > 0);
|
|
31
|
+
|
|
32
|
+
export type GroupCorrectionDataElement = DataElement & { purposeName: string };
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Build a product-scoped deduped data-element list for grouped CORRECTION.
|
|
36
|
+
* Dedup is done by data-element UUID within the product group.
|
|
37
|
+
*/
|
|
38
|
+
export const buildGroupDataElements = (
|
|
39
|
+
group: FormProductGroup,
|
|
40
|
+
): GroupCorrectionDataElement[] => {
|
|
41
|
+
const byUuid = new Map<string, GroupCorrectionDataElement>();
|
|
42
|
+
for (const purpose of group.purposes) {
|
|
43
|
+
if (!purpose) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const dataElements = Array.isArray(purpose.data_elements)
|
|
47
|
+
? purpose.data_elements
|
|
48
|
+
: [];
|
|
49
|
+
for (const de of dataElements) {
|
|
50
|
+
if (de && de.uuid && !byUuid.has(de.uuid)) {
|
|
51
|
+
byUuid.set(de.uuid, { ...de, purposeName: purpose.name });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return Array.from(byUuid.values());
|
|
56
|
+
};
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { createColumnHelper } from '@tanstack/react-table';
|
|
4
|
+
import dayjs from 'dayjs';
|
|
5
|
+
import { Download } from 'lucide-react';
|
|
6
|
+
import { PCText, PCButton, PCBadge } from '../ui';
|
|
7
|
+
import { pcStyles } from '../styles/pcStyles';
|
|
8
|
+
import { STATUS_COLORS } from '../lib/constants';
|
|
9
|
+
import { getVar } from '../lib/utils';
|
|
10
|
+
import type { Receipt } from '../lib/types';
|
|
11
|
+
|
|
12
|
+
const columnHelper = createColumnHelper<Receipt>();
|
|
13
|
+
|
|
14
|
+
type EventPalette = { color: string; bg: string };
|
|
15
|
+
type BadgeVariant = 'success' | 'error' | 'warning' | 'info';
|
|
16
|
+
|
|
17
|
+
// Map the raw event_type enum to a status palette + a fallback i18n label.
|
|
18
|
+
// The localized `event_type_display` from the API is preferred when present.
|
|
19
|
+
const EVENT_PALETTE: Record<string, EventPalette> = {
|
|
20
|
+
GRANTED: STATUS_COLORS.success,
|
|
21
|
+
WITHDRAWN: STATUS_COLORS.warning,
|
|
22
|
+
DECLINED: STATUS_COLORS.error,
|
|
23
|
+
EXPIRED: STATUS_COLORS.warning,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const EVENT_LABEL_KEY: Record<string, string> = {
|
|
27
|
+
GRANTED: 'consentGranted',
|
|
28
|
+
WITHDRAWN: 'consentRevoked',
|
|
29
|
+
DECLINED: 'consentDeclined',
|
|
30
|
+
EXPIRED: 'consentExpired',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const FALLBACK_PALETTE: EventPalette = STATUS_COLORS.info;
|
|
34
|
+
const EVENT_BADGE_VARIANT: Record<string, BadgeVariant> = {
|
|
35
|
+
GRANTED: 'success',
|
|
36
|
+
WITHDRAWN: 'warning',
|
|
37
|
+
DECLINED: 'error',
|
|
38
|
+
EXPIRED: 'warning',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// First two alphanumerics of the product name, e.g. "Website" → "WE", "Mobile App" → "MO".
|
|
42
|
+
function getProductInitials(name: string): string {
|
|
43
|
+
const cleaned = name.replace(/[^a-zA-Z0-9]/g, '');
|
|
44
|
+
return cleaned.slice(0, 2).toUpperCase() || '?';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const cellPad = { paddingTop: pcStyles.pySm.paddingTop, paddingBottom: pcStyles.pySm.paddingBottom };
|
|
48
|
+
|
|
49
|
+
export const useReceiptColumns = ({
|
|
50
|
+
onDownload,
|
|
51
|
+
downloadingUuid,
|
|
52
|
+
}: {
|
|
53
|
+
onDownload: (receipt: Receipt) => void;
|
|
54
|
+
downloadingUuid: string | null;
|
|
55
|
+
}) => {
|
|
56
|
+
const { t } = useTranslation();
|
|
57
|
+
return useMemo(
|
|
58
|
+
() => [
|
|
59
|
+
columnHelper.accessor('event_type', {
|
|
60
|
+
id: 'event',
|
|
61
|
+
header: t('event'),
|
|
62
|
+
cell: (info) => {
|
|
63
|
+
const row = info.row.original;
|
|
64
|
+
const raw = String(row.event_type ?? '').toUpperCase();
|
|
65
|
+
const palette = EVENT_PALETTE[raw] ?? FALLBACK_PALETTE;
|
|
66
|
+
const badgeVariant = EVENT_BADGE_VARIANT[raw] ?? 'info';
|
|
67
|
+
const labelKey = EVENT_LABEL_KEY[raw];
|
|
68
|
+
const label = row.event_type_display || (labelKey ? t(labelKey) : row.event_type || '—');
|
|
69
|
+
return (
|
|
70
|
+
<div style={cellPad}>
|
|
71
|
+
<PCBadge
|
|
72
|
+
variant={badgeVariant}
|
|
73
|
+
size="sm"
|
|
74
|
+
style={{
|
|
75
|
+
display: 'inline-flex',
|
|
76
|
+
alignItems: 'center',
|
|
77
|
+
gap: '0.4375rem',
|
|
78
|
+
fontWeight: 600,
|
|
79
|
+
whiteSpace: 'nowrap',
|
|
80
|
+
}}
|
|
81
|
+
>
|
|
82
|
+
<span
|
|
83
|
+
aria-hidden="true"
|
|
84
|
+
style={{
|
|
85
|
+
width: 7,
|
|
86
|
+
height: 7,
|
|
87
|
+
borderRadius: '50%',
|
|
88
|
+
backgroundColor: palette.color,
|
|
89
|
+
flexShrink: 0,
|
|
90
|
+
}}
|
|
91
|
+
/>
|
|
92
|
+
{label}
|
|
93
|
+
</PCBadge>
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
},
|
|
97
|
+
enableSorting: false,
|
|
98
|
+
enableColumnFilter: false,
|
|
99
|
+
}),
|
|
100
|
+
columnHelper.accessor('product_name', {
|
|
101
|
+
id: 'product',
|
|
102
|
+
header: t('product'),
|
|
103
|
+
cell: (info) => {
|
|
104
|
+
const row = info.row.original;
|
|
105
|
+
const name = row.product_name_display || row.product_name || '—';
|
|
106
|
+
return (
|
|
107
|
+
<div style={{ ...cellPad, display: 'flex', alignItems: 'center', gap: '0.625rem' }}>
|
|
108
|
+
<span
|
|
109
|
+
aria-hidden="true"
|
|
110
|
+
style={{
|
|
111
|
+
display: 'inline-flex',
|
|
112
|
+
alignItems: 'center',
|
|
113
|
+
justifyContent: 'center',
|
|
114
|
+
width: 32,
|
|
115
|
+
height: 32,
|
|
116
|
+
borderRadius: 8,
|
|
117
|
+
backgroundColor: getVar('--pc-bg-tertiary'),
|
|
118
|
+
color: getVar('--pc-text-secondary'),
|
|
119
|
+
border: `1px solid ${getVar('--pc-border-color')}`,
|
|
120
|
+
fontSize: '0.6875rem',
|
|
121
|
+
fontWeight: 600,
|
|
122
|
+
flexShrink: 0,
|
|
123
|
+
}}
|
|
124
|
+
>
|
|
125
|
+
{getProductInitials(name)}
|
|
126
|
+
</span>
|
|
127
|
+
<PCText size="sm" weight="medium" color="primary">
|
|
128
|
+
{name}
|
|
129
|
+
</PCText>
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
},
|
|
133
|
+
enableSorting: false,
|
|
134
|
+
enableColumnFilter: false,
|
|
135
|
+
}),
|
|
136
|
+
columnHelper.accessor('created_at', {
|
|
137
|
+
id: 'date',
|
|
138
|
+
header: t('date'),
|
|
139
|
+
cell: (info) => {
|
|
140
|
+
const d = dayjs(info.getValue());
|
|
141
|
+
return (
|
|
142
|
+
<div style={cellPad}>
|
|
143
|
+
<PCText size="sm" color="primary" weight="medium">
|
|
144
|
+
{d.isValid() ? d.format('MMM D, YYYY') : '—'}
|
|
145
|
+
</PCText>
|
|
146
|
+
</div>
|
|
147
|
+
);
|
|
148
|
+
},
|
|
149
|
+
enableSorting: false,
|
|
150
|
+
enableColumnFilter: false,
|
|
151
|
+
}),
|
|
152
|
+
columnHelper.display({
|
|
153
|
+
id: 'time',
|
|
154
|
+
header: t('time'),
|
|
155
|
+
cell: (info) => {
|
|
156
|
+
const d = dayjs(info.row.original.created_at);
|
|
157
|
+
return (
|
|
158
|
+
<div style={cellPad}>
|
|
159
|
+
<PCText size="sm" color="secondary">
|
|
160
|
+
{d.isValid() ? d.format('h:mm A') : '—'}
|
|
161
|
+
</PCText>
|
|
162
|
+
</div>
|
|
163
|
+
);
|
|
164
|
+
},
|
|
165
|
+
enableSorting: false,
|
|
166
|
+
enableColumnFilter: false,
|
|
167
|
+
}),
|
|
168
|
+
columnHelper.display({
|
|
169
|
+
id: 'download',
|
|
170
|
+
header: t('download'),
|
|
171
|
+
meta: { align: 'right' },
|
|
172
|
+
cell: (info) => {
|
|
173
|
+
const row = info.row.original;
|
|
174
|
+
const isDownloading = downloadingUuid === row.uuid;
|
|
175
|
+
return (
|
|
176
|
+
<div style={{ ...cellPad, display: 'flex', justifyContent: 'flex-end' }}>
|
|
177
|
+
<PCButton
|
|
178
|
+
variant="outline"
|
|
179
|
+
size="sm"
|
|
180
|
+
isLoading={isDownloading}
|
|
181
|
+
disabled={isDownloading || !row.uuid}
|
|
182
|
+
onClick={() => onDownload(row)}
|
|
183
|
+
style={{
|
|
184
|
+
display: 'inline-flex',
|
|
185
|
+
alignItems: 'center',
|
|
186
|
+
gap: '0.375rem',
|
|
187
|
+
minWidth: '6.75rem',
|
|
188
|
+
justifyContent: 'center',
|
|
189
|
+
}}
|
|
190
|
+
>
|
|
191
|
+
{!isDownloading && <Download size={15} strokeWidth={1.9} />}
|
|
192
|
+
{t('download')}
|
|
193
|
+
</PCButton>
|
|
194
|
+
</div>
|
|
195
|
+
);
|
|
196
|
+
},
|
|
197
|
+
enableSorting: false,
|
|
198
|
+
enableColumnFilter: false,
|
|
199
|
+
}),
|
|
200
|
+
],
|
|
201
|
+
[t, onDownload, downloadingUuid]
|
|
202
|
+
);
|
|
203
|
+
};
|