@redacto.io/consent-sdk-react 5.3.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.
- package/.turbo/turbo-build.log +24 -0
- package/CHANGELOG.md +6 -0
- package/dist/privacy-center.d.mts +98 -38
- package/dist/privacy-center.d.ts +98 -38
- package/dist/privacy-center.js +3679 -1491
- package/dist/privacy-center.mjs +3614 -1423
- package/package.json +1 -1
- package/src/RedactoPrivacyCenter/api/actions.ts +52 -8
- package/src/RedactoPrivacyCenter/api/client.ts +58 -15
- package/src/RedactoPrivacyCenter/components/ConsentManager.tsx +722 -273
- package/src/RedactoPrivacyCenter/components/ConsentManagerCells.tsx +187 -0
- package/src/RedactoPrivacyCenter/components/ConsentManagerColumns.tsx +65 -165
- package/src/RedactoPrivacyCenter/components/ConsentManagerExpandedRow.tsx +179 -0
- package/src/RedactoPrivacyCenter/components/ConsentManagerGroupedColumns.tsx +318 -0
- package/src/RedactoPrivacyCenter/components/ConsentManagerProductCards.tsx +333 -0
- package/src/RedactoPrivacyCenter/components/ConsentManagerPurposeCards.tsx +102 -0
- package/src/RedactoPrivacyCenter/components/ConsentManagerTableSkeleton.tsx +6 -2
- package/src/RedactoPrivacyCenter/components/ModifyConsentModal.tsx +8 -5
- package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationBell.tsx +1 -1
- package/src/RedactoPrivacyCenter/components/PrivacyCenterLayout.tsx +102 -53
- package/src/RedactoPrivacyCenter/components/types.ts +2 -5
- package/src/RedactoPrivacyCenter/context/PageChromeContext.tsx +21 -1
- package/src/RedactoPrivacyCenter/lib/api.ts +70 -28
- package/src/RedactoPrivacyCenter/lib/consent-manager/utils.tsx +4 -2
- package/src/RedactoPrivacyCenter/lib/types.ts +57 -36
- package/src/RedactoPrivacyCenter/locales/as/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/bho/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/bn/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/brx/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/doi/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/en/translation.json +16 -1
- package/src/RedactoPrivacyCenter/locales/gom/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/gu/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/hi/translation.json +15 -1
- package/src/RedactoPrivacyCenter/locales/kn/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/ks/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/mai/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/ml/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/mni-Mtei/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/mr/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/ne/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/or/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/pa/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/sa/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/sat/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/sd/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/ta/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/te/translation.json +12 -1
- package/src/RedactoPrivacyCenter/locales/ur/translation.json +12 -1
- package/src/RedactoPrivacyCenter/styles/injectStyles.ts +527 -5
- package/src/RedactoPrivacyCenter/styles/variables.css +5 -0
- package/src/RedactoPrivacyCenter/table/AdvancedTable.tsx +36 -16
- package/src/RedactoPrivacyCenter/table/AdvancedTablePagination.tsx +4 -12
- package/src/RedactoPrivacyCenter/ui/PCInput.tsx +1 -1
- package/src/RedactoPrivacyCenter/ui/PCSelect.tsx +51 -5
- package/src/RedactoPrivacyCenter/ui/ThemeToggle.tsx +2 -4
- package/src/privacy-center.ts +7 -3
|
@@ -1,24 +1,47 @@
|
|
|
1
1
|
import { useState, useEffect, useCallback, useMemo } from 'react';
|
|
2
2
|
import { useTranslation } from 'react-i18next';
|
|
3
3
|
import { PCInput, PCSelect, PCButton, PCText, PCModal, type SelectOption } from '../ui';
|
|
4
|
-
import { Search, ArrowUpToLine } from 'lucide-react';
|
|
4
|
+
import { Search, ArrowUpToLine, Filter, Circle } from 'lucide-react';
|
|
5
5
|
import { pcStyles } from '../styles/pcStyles';
|
|
6
6
|
import { AdvancedTable, AdvancedTablePagination } from '../table';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
getCoreRowModel,
|
|
9
|
+
getExpandedRowModel,
|
|
10
|
+
useReactTable,
|
|
11
|
+
type ExpandedState,
|
|
12
|
+
type PaginationState,
|
|
13
|
+
type Row,
|
|
14
|
+
type Updater,
|
|
15
|
+
} from '@tanstack/react-table';
|
|
8
16
|
import {
|
|
9
17
|
fetchUserConsents,
|
|
18
|
+
fetchGroupPurposes,
|
|
10
19
|
manageConsent,
|
|
11
20
|
} from '../lib/api';
|
|
12
21
|
import { useOrganization } from '../context/OrganizationContext';
|
|
13
22
|
import { useAuth } from '../context/AuthContext';
|
|
14
23
|
import { useBaseUrl } from '../context/BaseUrlContext';
|
|
15
|
-
import
|
|
24
|
+
import { usePageChrome } from '../context/PageChromeContext';
|
|
25
|
+
import { ConsentStatusEnum } from '../lib/types';
|
|
26
|
+
import type {
|
|
27
|
+
ConsentAction,
|
|
28
|
+
ConsentGroup,
|
|
29
|
+
ConsentModalRecord,
|
|
30
|
+
DisplayProductGroup,
|
|
31
|
+
DisplayPurposeRow,
|
|
32
|
+
PurposeItem,
|
|
33
|
+
UserConsentDetail,
|
|
34
|
+
} from '../lib/types';
|
|
16
35
|
import { useCurrentLanguage } from '../lib/useCurrentLanguage';
|
|
17
36
|
import { exportTableToCSV, useStatusOptions } from '../lib/consent-manager/utils';
|
|
18
37
|
import { toast } from '../utils/toast';
|
|
19
38
|
import { ConsentManagerTableSkeleton } from './ConsentManagerTableSkeleton';
|
|
20
39
|
import { ModifyConsentModal } from './ModifyConsentModal';
|
|
40
|
+
import { useConsentManagerGroupedColumns } from './ConsentManagerGroupedColumns';
|
|
21
41
|
import { useConsentManagerColumns } from './ConsentManagerColumns';
|
|
42
|
+
import { ConsentManagerExpandedRow } from './ConsentManagerExpandedRow';
|
|
43
|
+
import { ConsentManagerProductCards } from './ConsentManagerProductCards';
|
|
44
|
+
import { ConsentManagerPurposeCards } from './ConsentManagerPurposeCards';
|
|
22
45
|
import type {
|
|
23
46
|
ConsentManagerProps,
|
|
24
47
|
StatusFilterOption,
|
|
@@ -30,12 +53,15 @@ import {
|
|
|
30
53
|
DEFAULT_PAGE_SIZE,
|
|
31
54
|
} from './ConsentManagerConstants';
|
|
32
55
|
|
|
56
|
+
type ConsentViewMode = 'product' | 'purpose';
|
|
57
|
+
|
|
33
58
|
export default function ConsentManager({ variant }: ConsentManagerProps) {
|
|
34
59
|
const { t } = useTranslation();
|
|
35
60
|
const statusOptions = useStatusOptions();
|
|
36
61
|
const { organization } = useOrganization();
|
|
37
62
|
const { accessToken, refreshToken, onError, updateTokens } = useAuth();
|
|
38
63
|
const { consentClient } = useBaseUrl();
|
|
64
|
+
const { setHeaderStatus } = usePageChrome();
|
|
39
65
|
const organizationId = organization?.uuid;
|
|
40
66
|
const workspaceId = organization?.workspaces[0]?.uuid;
|
|
41
67
|
const contact = sessionStorage.getItem('email') || '';
|
|
@@ -53,9 +79,10 @@ export default function ConsentManager({ variant }: ConsentManagerProps) {
|
|
|
53
79
|
|
|
54
80
|
// State management
|
|
55
81
|
const [activeTab, setActiveTab] = useState<'direct' | 'nominated'>('direct');
|
|
82
|
+
const [viewMode, setViewMode] = useState<ConsentViewMode>('product');
|
|
56
83
|
const [consentDetail, setConsentDetail] = useState<UserConsentDetail | null>(null);
|
|
57
|
-
const [
|
|
58
|
-
const [
|
|
84
|
+
const [consentGroups, setConsentGroups] = useState<DisplayProductGroup[]>([]);
|
|
85
|
+
const [consentRows, setConsentRows] = useState<DisplayPurposeRow[]>([]);
|
|
59
86
|
const [isLoading, setIsLoading] = useState(true);
|
|
60
87
|
const [searchQuery, setSearchQuery] = useState('');
|
|
61
88
|
const [debouncedSearch, setDebouncedSearch] = useState('');
|
|
@@ -68,13 +95,45 @@ export default function ConsentManager({ variant }: ConsentManagerProps) {
|
|
|
68
95
|
[]
|
|
69
96
|
);
|
|
70
97
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
71
|
-
const [selectedRecord, setSelectedRecord] = useState<
|
|
98
|
+
const [selectedRecord, setSelectedRecord] = useState<ConsentModalRecord | null>(
|
|
72
99
|
null
|
|
73
100
|
);
|
|
74
101
|
const [exportModal, setExportModal] = useState(false);
|
|
75
102
|
const [isManageLoading, setIsManageLoading] = useState(false);
|
|
76
103
|
const [currentPage, setCurrentPage] = useState(DEFAULT_PAGE);
|
|
77
104
|
const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE);
|
|
105
|
+
const [expanded, setExpanded] = useState<ExpandedState>({});
|
|
106
|
+
const [extraPurposesByKey, setExtraPurposesByKey] = useState<Record<string, PurposeItem[]>>({});
|
|
107
|
+
const [hasMoreByKey, setHasMoreByKey] = useState<Record<string, boolean>>({});
|
|
108
|
+
const [loadingMoreByKey, setLoadingMoreByKey] = useState<Record<string, boolean>>({});
|
|
109
|
+
const composedSearchPlaceholder = useMemo(
|
|
110
|
+
() => `${t('search')} ${t('consent')}, ${t('product')}, ${t('purpose')}...`,
|
|
111
|
+
[t]
|
|
112
|
+
);
|
|
113
|
+
const allStatusOption = useMemo<StatusFilterOption>(
|
|
114
|
+
() => ({
|
|
115
|
+
label: `${t('all')} ${t('status')}`,
|
|
116
|
+
value: '__all__',
|
|
117
|
+
}),
|
|
118
|
+
[t]
|
|
119
|
+
);
|
|
120
|
+
const allProductOption = useMemo<ProductFilterOption>(
|
|
121
|
+
() => ({
|
|
122
|
+
label: `${t('all')} ${t('product')}`,
|
|
123
|
+
value: '__all__',
|
|
124
|
+
}),
|
|
125
|
+
[t]
|
|
126
|
+
);
|
|
127
|
+
const statusFilterOptions = useMemo<StatusFilterOption[]>(
|
|
128
|
+
() => [allStatusOption, ...statusOptions],
|
|
129
|
+
[allStatusOption, statusOptions]
|
|
130
|
+
);
|
|
131
|
+
const productFilterOptions = useMemo<ProductFilterOption[]>(
|
|
132
|
+
() => [allProductOption, ...productOptions],
|
|
133
|
+
[allProductOption, productOptions]
|
|
134
|
+
);
|
|
135
|
+
const statusSelectValue = statusFilter ?? allStatusOption;
|
|
136
|
+
const productSelectValue = productFilter ?? allProductOption;
|
|
78
137
|
|
|
79
138
|
// Sync selected status filter label when language changes
|
|
80
139
|
useEffect(() => {
|
|
@@ -96,6 +155,19 @@ export default function ConsentManager({ variant }: ConsentManagerProps) {
|
|
|
96
155
|
}
|
|
97
156
|
}, [productOptions, productFilter]);
|
|
98
157
|
|
|
158
|
+
// Push user_status into the page header so it renders inline with the page title
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
const status = consentDetail?.user_status;
|
|
161
|
+
if (status === 'TRANSFERRED') {
|
|
162
|
+
setHeaderStatus({ label: t('statusTransferred'), variant: 'warning' });
|
|
163
|
+
} else if (status === 'ACTIVE') {
|
|
164
|
+
setHeaderStatus({ label: t('statusActive'), variant: 'success' });
|
|
165
|
+
} else {
|
|
166
|
+
setHeaderStatus(null);
|
|
167
|
+
}
|
|
168
|
+
return () => setHeaderStatus(null);
|
|
169
|
+
}, [consentDetail?.user_status, setHeaderStatus, t]);
|
|
170
|
+
|
|
99
171
|
// Debounce search query
|
|
100
172
|
useEffect(() => {
|
|
101
173
|
const timer = setTimeout(() => {
|
|
@@ -104,25 +176,132 @@ export default function ConsentManager({ variant }: ConsentManagerProps) {
|
|
|
104
176
|
return () => clearTimeout(timer);
|
|
105
177
|
}, [searchQuery]);
|
|
106
178
|
|
|
107
|
-
const handleOpenModal = useCallback((record:
|
|
179
|
+
const handleOpenModal = useCallback((record: ConsentModalRecord) => {
|
|
108
180
|
setSelectedRecord(record);
|
|
109
181
|
setIsModalOpen(true);
|
|
110
182
|
}, []);
|
|
111
183
|
|
|
184
|
+
const getCardKey = useCallback(
|
|
185
|
+
(group: ConsentGroup): string =>
|
|
186
|
+
group.nominator
|
|
187
|
+
? `nominated:${group.nominator.uuid}:${group.product_uuid}`
|
|
188
|
+
: `direct:${group.product_uuid}`,
|
|
189
|
+
[]
|
|
190
|
+
);
|
|
191
|
+
|
|
112
192
|
const handleCloseModal = useCallback(() => {
|
|
113
193
|
setIsModalOpen(false);
|
|
114
194
|
setSelectedRecord(null);
|
|
115
195
|
}, []);
|
|
116
196
|
|
|
197
|
+
const matchesPurpose = useCallback(
|
|
198
|
+
(
|
|
199
|
+
purpose: PurposeItem,
|
|
200
|
+
group: ConsentGroup,
|
|
201
|
+
normalizedSearch: string,
|
|
202
|
+
statusValue?: string
|
|
203
|
+
) => {
|
|
204
|
+
const matchesSearch =
|
|
205
|
+
!normalizedSearch ||
|
|
206
|
+
[
|
|
207
|
+
purpose.name,
|
|
208
|
+
purpose.description,
|
|
209
|
+
group.product_name,
|
|
210
|
+
group.product_description,
|
|
211
|
+
purpose.method,
|
|
212
|
+
purpose.status,
|
|
213
|
+
]
|
|
214
|
+
.filter(Boolean)
|
|
215
|
+
.some((value) => String(value).toLowerCase().includes(normalizedSearch));
|
|
216
|
+
|
|
217
|
+
const matchesStatus =
|
|
218
|
+
!statusValue || purpose.status?.toLowerCase() === statusValue.toLowerCase();
|
|
219
|
+
|
|
220
|
+
return matchesSearch && matchesStatus;
|
|
221
|
+
},
|
|
222
|
+
[]
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
const buildDisplayGroups = useCallback(
|
|
226
|
+
(
|
|
227
|
+
groups: ConsentGroup[] | undefined,
|
|
228
|
+
search: string,
|
|
229
|
+
statusValue?: string,
|
|
230
|
+
productUuid?: string
|
|
231
|
+
): DisplayProductGroup[] => {
|
|
232
|
+
if (!groups || groups.length === 0) return [];
|
|
233
|
+
const normalizedSearch = search.trim().toLowerCase();
|
|
234
|
+
|
|
235
|
+
return groups.reduce<DisplayProductGroup[]>((acc, group) => {
|
|
236
|
+
if (productUuid && group.product_uuid !== productUuid) {
|
|
237
|
+
return acc;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const filteredPurposes = (group.purposes || []).filter((purpose) =>
|
|
241
|
+
matchesPurpose(purpose, group, normalizedSearch, statusValue)
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
if (filteredPurposes.length === 0) {
|
|
245
|
+
return acc;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const counts = filteredPurposes.reduce(
|
|
249
|
+
(totals, purpose) => {
|
|
250
|
+
const normalized = (typeof purpose.status === 'string' ? purpose.status : '').toUpperCase();
|
|
251
|
+
switch (normalized) {
|
|
252
|
+
case ConsentStatusEnum.ACTIVE:
|
|
253
|
+
totals.active += 1;
|
|
254
|
+
break;
|
|
255
|
+
case ConsentStatusEnum.WITHDRAWN:
|
|
256
|
+
case ConsentStatusEnum.REVOKED:
|
|
257
|
+
totals.withdrawn += 1;
|
|
258
|
+
break;
|
|
259
|
+
case ConsentStatusEnum.EXPIRED:
|
|
260
|
+
totals.expired += 1;
|
|
261
|
+
break;
|
|
262
|
+
case ConsentStatusEnum.DECLINED:
|
|
263
|
+
totals.declined += 1;
|
|
264
|
+
break;
|
|
265
|
+
default:
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
return totals;
|
|
269
|
+
},
|
|
270
|
+
{ active: 0, withdrawn: 0, expired: 0, declined: 0 }
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
const latestGivenDate = filteredPurposes.reduce<string | null>((latest, purpose) => {
|
|
274
|
+
if (!purpose.given_date) return latest;
|
|
275
|
+
if (!latest) return purpose.given_date;
|
|
276
|
+
return new Date(purpose.given_date).getTime() > new Date(latest).getTime()
|
|
277
|
+
? purpose.given_date
|
|
278
|
+
: latest;
|
|
279
|
+
}, null);
|
|
280
|
+
|
|
281
|
+
acc.push({
|
|
282
|
+
...group,
|
|
283
|
+
purposes: filteredPurposes,
|
|
284
|
+
withdrawn_purposes: counts.withdrawn,
|
|
285
|
+
expired_purposes: counts.expired,
|
|
286
|
+
declined_purposes: counts.declined,
|
|
287
|
+
latest_given_date: latestGivenDate,
|
|
288
|
+
});
|
|
289
|
+
return acc;
|
|
290
|
+
}, []);
|
|
291
|
+
},
|
|
292
|
+
[matchesPurpose]
|
|
293
|
+
);
|
|
294
|
+
|
|
117
295
|
const flattenProductGroups = useCallback(
|
|
118
|
-
(groups:
|
|
296
|
+
(groups: ConsentGroup[] | undefined): DisplayPurposeRow[] => {
|
|
119
297
|
if (!groups || groups.length === 0) return [];
|
|
120
298
|
return groups.flatMap((group) =>
|
|
121
299
|
(group.purposes || []).map((purpose) => ({
|
|
122
300
|
...purpose,
|
|
123
|
-
product_uuid:
|
|
124
|
-
product_name:
|
|
125
|
-
product_description:
|
|
301
|
+
product_uuid: group.product_uuid,
|
|
302
|
+
product_name: group.product_name,
|
|
303
|
+
product_description: group.product_description,
|
|
304
|
+
nominator: group.nominator,
|
|
126
305
|
}))
|
|
127
306
|
);
|
|
128
307
|
},
|
|
@@ -131,19 +310,18 @@ export default function ConsentManager({ variant }: ConsentManagerProps) {
|
|
|
131
310
|
|
|
132
311
|
const filterConsentRows = useCallback(
|
|
133
312
|
(
|
|
134
|
-
rows:
|
|
313
|
+
rows: DisplayPurposeRow[],
|
|
135
314
|
search: string,
|
|
136
315
|
statusValue?: string,
|
|
137
316
|
productUuid?: string
|
|
138
|
-
) => {
|
|
317
|
+
): DisplayPurposeRow[] => {
|
|
139
318
|
const normalizedSearch = search.trim().toLowerCase();
|
|
140
|
-
|
|
141
319
|
return rows.filter((row) => {
|
|
142
320
|
const matchesSearch =
|
|
143
321
|
!normalizedSearch ||
|
|
144
322
|
[
|
|
145
|
-
row.
|
|
146
|
-
row.
|
|
323
|
+
row.name,
|
|
324
|
+
row.description,
|
|
147
325
|
row.product_name,
|
|
148
326
|
row.product_description,
|
|
149
327
|
row.method,
|
|
@@ -156,9 +334,7 @@ export default function ConsentManager({ variant }: ConsentManagerProps) {
|
|
|
156
334
|
|
|
157
335
|
const matchesStatus =
|
|
158
336
|
!statusValue || row.status?.toLowerCase() === statusValue.toLowerCase();
|
|
159
|
-
|
|
160
|
-
const matchesProduct =
|
|
161
|
-
!productUuid || row.product_uuid === productUuid;
|
|
337
|
+
const matchesProduct = !productUuid || row.product_uuid === productUuid;
|
|
162
338
|
|
|
163
339
|
return matchesSearch && matchesStatus && matchesProduct;
|
|
164
340
|
});
|
|
@@ -167,7 +343,7 @@ export default function ConsentManager({ variant }: ConsentManagerProps) {
|
|
|
167
343
|
);
|
|
168
344
|
|
|
169
345
|
const buildProductOptionsFromGroups = useCallback(
|
|
170
|
-
(groups:
|
|
346
|
+
(groups: ConsentGroup[] | undefined) => {
|
|
171
347
|
if (!groups || groups.length === 0) return [];
|
|
172
348
|
const productMap = new Map<string, { label: string; value: string }>();
|
|
173
349
|
groups.forEach((group) => {
|
|
@@ -183,31 +359,42 @@ export default function ConsentManager({ variant }: ConsentManagerProps) {
|
|
|
183
359
|
[]
|
|
184
360
|
);
|
|
185
361
|
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
362
|
+
const groupedColumns = useConsentManagerGroupedColumns();
|
|
363
|
+
const purposeColumns = useConsentManagerColumns({
|
|
364
|
+
onOpenModal: handleOpenModal,
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
const groupedTableClassNames = useMemo(
|
|
368
|
+
() => ({
|
|
369
|
+
container: 'pc-consent-table-container',
|
|
370
|
+
tableClassName: 'pc-consent-table',
|
|
371
|
+
headerClassName: 'pc-consent-table-header',
|
|
372
|
+
bodyClassName: 'pc-consent-table-body',
|
|
373
|
+
rowClassName: 'pc-consent-table-row',
|
|
374
|
+
cellClassName: 'pc-consent-table-cell',
|
|
375
|
+
headerCellClassName: 'pc-consent-table-head-cell',
|
|
376
|
+
expandedRowClassName: 'pc-consent-table-expanded-row',
|
|
377
|
+
expandedCellClassName: 'pc-consent-table-expanded-cell',
|
|
378
|
+
}),
|
|
201
379
|
[]
|
|
202
380
|
);
|
|
203
381
|
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
382
|
+
const purposeTableClassNames = useMemo(
|
|
383
|
+
() => ({
|
|
384
|
+
container: 'pc-consent-table-container',
|
|
385
|
+
tableClassName: 'pc-consent-table pc-consent-table-purpose',
|
|
386
|
+
headerClassName: 'pc-consent-table-header',
|
|
387
|
+
bodyClassName: 'pc-consent-table-body',
|
|
388
|
+
rowClassName: 'pc-consent-table-row',
|
|
389
|
+
cellClassName: 'pc-consent-table-cell',
|
|
390
|
+
headerCellClassName: 'pc-consent-table-head-cell',
|
|
391
|
+
}),
|
|
392
|
+
[]
|
|
393
|
+
);
|
|
207
394
|
|
|
208
395
|
// Single data fetch function
|
|
209
396
|
const fetchData = useCallback(async () => {
|
|
210
|
-
if (!organizationId || !workspaceId
|
|
397
|
+
if (!organizationId || !workspaceId) {
|
|
211
398
|
return;
|
|
212
399
|
}
|
|
213
400
|
|
|
@@ -218,31 +405,39 @@ export default function ConsentManager({ variant }: ConsentManagerProps) {
|
|
|
218
405
|
const statusParam = statusFilter?.value;
|
|
219
406
|
const productUuidParam = productFilter?.value;
|
|
220
407
|
|
|
221
|
-
const response = await fetchUserConsents(
|
|
408
|
+
const response = await fetchUserConsents({
|
|
222
409
|
organizationId,
|
|
223
410
|
workspaceId,
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
411
|
+
offset: activeTab === 'direct' ? offset : undefined,
|
|
412
|
+
limit: activeTab === 'direct' ? pageSize : undefined,
|
|
413
|
+
nominatedOffset: activeTab === 'nominated' ? offset : undefined,
|
|
414
|
+
nominatedLimit: activeTab === 'nominated' ? pageSize : undefined,
|
|
415
|
+
search: searchParam,
|
|
416
|
+
status: statusParam,
|
|
417
|
+
product_uuid: productUuidParam,
|
|
418
|
+
language: langParam,
|
|
230
419
|
authContext,
|
|
231
420
|
consentClient,
|
|
232
|
-
|
|
233
|
-
);
|
|
421
|
+
});
|
|
234
422
|
|
|
235
423
|
if (!response) {
|
|
236
424
|
throw new Error('Failed to fetch consent data');
|
|
237
425
|
}
|
|
238
426
|
|
|
239
427
|
setConsentDetail(response);
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
428
|
+
setExtraPurposesByKey({});
|
|
429
|
+
setHasMoreByKey({});
|
|
430
|
+
setLoadingMoreByKey({});
|
|
243
431
|
|
|
244
432
|
const selectedGroups =
|
|
245
|
-
activeTab === 'direct' ?
|
|
433
|
+
activeTab === 'direct' ? response.direct : response.nominated;
|
|
434
|
+
const displayGroups = buildDisplayGroups(
|
|
435
|
+
selectedGroups,
|
|
436
|
+
debouncedSearch,
|
|
437
|
+
statusParam,
|
|
438
|
+
productUuidParam
|
|
439
|
+
);
|
|
440
|
+
setConsentGroups(displayGroups);
|
|
246
441
|
const flattenedRows = flattenProductGroups(selectedGroups);
|
|
247
442
|
const filteredRows = filterConsentRows(
|
|
248
443
|
flattenedRows,
|
|
@@ -250,32 +445,23 @@ export default function ConsentManager({ variant }: ConsentManagerProps) {
|
|
|
250
445
|
statusParam,
|
|
251
446
|
productUuidParam
|
|
252
447
|
);
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
const directCount = response?.status_summary?.direct_purposes;
|
|
256
|
-
const nominatedCount = response?.status_summary?.nominated_purposes;
|
|
257
|
-
const fallbackTotal = response?.pagination?.total_count ?? 0;
|
|
258
|
-
const hasActiveFilters = Boolean(searchParam || statusParam || productUuidParam);
|
|
259
|
-
setTotalCount(
|
|
260
|
-
hasActiveFilters
|
|
261
|
-
? filteredRows.length
|
|
262
|
-
: activeTab === 'direct'
|
|
263
|
-
? (directCount ?? fallbackTotal)
|
|
264
|
-
: (nominatedCount ?? fallbackTotal)
|
|
265
|
-
);
|
|
448
|
+
setConsentRows(filteredRows);
|
|
266
449
|
|
|
267
450
|
// Extract unique products from all groups
|
|
268
451
|
const combinedGroups = [
|
|
269
|
-
...(response
|
|
270
|
-
...
|
|
452
|
+
...(response.direct || []),
|
|
453
|
+
...(response.nominated || []),
|
|
271
454
|
];
|
|
272
455
|
setProductOptions(buildProductOptionsFromGroups(combinedGroups));
|
|
273
456
|
} catch (error) {
|
|
274
457
|
console.error(error);
|
|
275
458
|
setConsentDetail(null);
|
|
276
|
-
|
|
277
|
-
|
|
459
|
+
setConsentGroups([]);
|
|
460
|
+
setConsentRows([]);
|
|
278
461
|
setProductOptions([]);
|
|
462
|
+
setExtraPurposesByKey({});
|
|
463
|
+
setHasMoreByKey({});
|
|
464
|
+
setLoadingMoreByKey({});
|
|
279
465
|
toast.error(t('failedToFetchConsentData'));
|
|
280
466
|
} finally {
|
|
281
467
|
setIsLoading(false);
|
|
@@ -283,7 +469,6 @@ export default function ConsentManager({ variant }: ConsentManagerProps) {
|
|
|
283
469
|
}, [
|
|
284
470
|
organizationId,
|
|
285
471
|
workspaceId,
|
|
286
|
-
contact,
|
|
287
472
|
currentPage,
|
|
288
473
|
pageSize,
|
|
289
474
|
debouncedSearch,
|
|
@@ -293,10 +478,10 @@ export default function ConsentManager({ variant }: ConsentManagerProps) {
|
|
|
293
478
|
authContext,
|
|
294
479
|
consentClient,
|
|
295
480
|
langParam,
|
|
481
|
+
buildDisplayGroups,
|
|
296
482
|
flattenProductGroups,
|
|
297
483
|
filterConsentRows,
|
|
298
484
|
buildProductOptionsFromGroups,
|
|
299
|
-
getNominatedProductGroups,
|
|
300
485
|
t,
|
|
301
486
|
]);
|
|
302
487
|
|
|
@@ -305,55 +490,234 @@ export default function ConsentManager({ variant }: ConsentManagerProps) {
|
|
|
305
490
|
fetchData();
|
|
306
491
|
}, [fetchData]);
|
|
307
492
|
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
493
|
+
const hasActiveFilters = Boolean(
|
|
494
|
+
debouncedSearch.trim() || statusFilter?.value || productFilter?.value
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
const groupedPurposeCount = useMemo(
|
|
498
|
+
() => consentGroups.reduce((sum, group) => sum + group.purposes.length, 0),
|
|
499
|
+
[consentGroups]
|
|
500
|
+
);
|
|
501
|
+
|
|
502
|
+
const visibleConsentCount =
|
|
503
|
+
viewMode === 'product' ? groupedPurposeCount : consentRows.length;
|
|
504
|
+
|
|
505
|
+
const visibleProductCount = useMemo(() => {
|
|
506
|
+
if (viewMode === 'product') {
|
|
507
|
+
return consentGroups.length;
|
|
508
|
+
}
|
|
509
|
+
const productIds = new Set(
|
|
510
|
+
consentRows.map((row) => row.product_uuid || row.product_name || '')
|
|
511
|
+
);
|
|
512
|
+
productIds.delete('');
|
|
513
|
+
return productIds.size;
|
|
514
|
+
}, [viewMode, consentGroups, consentRows]);
|
|
515
|
+
|
|
516
|
+
const tableTotalCount = useMemo(() => {
|
|
517
|
+
const pagination =
|
|
518
|
+
activeTab === 'direct'
|
|
519
|
+
? consentDetail?.direct_pagination
|
|
520
|
+
: consentDetail?.nominated_pagination;
|
|
521
|
+
return pagination?.total_count ?? 0;
|
|
522
|
+
}, [activeTab, consentDetail]);
|
|
523
|
+
|
|
524
|
+
const tablePageCount = Math.ceil(tableTotalCount / pageSize) || 0;
|
|
525
|
+
|
|
526
|
+
const handlePaginationChange = useCallback(
|
|
527
|
+
(updater: Updater<PaginationState>) => {
|
|
528
|
+
const next =
|
|
529
|
+
typeof updater === 'function'
|
|
530
|
+
? updater({ pageIndex: currentPage - 1, pageSize })
|
|
531
|
+
: updater;
|
|
532
|
+
const nextPage = next.pageIndex + 1;
|
|
533
|
+
const nextSize = next.pageSize;
|
|
534
|
+
if (nextPage === currentPage && nextSize === pageSize) return;
|
|
535
|
+
setCurrentPage(nextPage);
|
|
536
|
+
setPageSize(nextSize);
|
|
537
|
+
},
|
|
538
|
+
[currentPage, pageSize]
|
|
539
|
+
);
|
|
540
|
+
|
|
541
|
+
const productTable = useReactTable<DisplayProductGroup>({
|
|
542
|
+
data: consentGroups,
|
|
543
|
+
columns: groupedColumns,
|
|
311
544
|
getCoreRowModel: getCoreRowModel(),
|
|
545
|
+
getExpandedRowModel: getExpandedRowModel(),
|
|
546
|
+
getRowCanExpand: () => true,
|
|
312
547
|
manualPagination: true,
|
|
313
|
-
pageCount:
|
|
548
|
+
pageCount: tablePageCount,
|
|
314
549
|
state: {
|
|
315
550
|
pagination: {
|
|
316
551
|
pageIndex: currentPage - 1,
|
|
317
552
|
pageSize: pageSize,
|
|
318
553
|
},
|
|
554
|
+
expanded,
|
|
555
|
+
},
|
|
556
|
+
onExpandedChange: (updater) => {
|
|
557
|
+
const next = typeof updater === 'function' ? updater(expanded) : updater;
|
|
558
|
+
if (next === true) {
|
|
559
|
+
setExpanded(next);
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
if (!next) {
|
|
563
|
+
setExpanded({});
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const nextMap = next as Record<string, boolean>;
|
|
568
|
+
const nextOpenIds = Object.keys(nextMap).filter((rowId) => nextMap[rowId]);
|
|
569
|
+
if (nextOpenIds.length <= 1) {
|
|
570
|
+
setExpanded(nextOpenIds.length === 1 ? { [nextOpenIds[0]]: true } : {});
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const prevMap =
|
|
575
|
+
expanded && expanded !== true ? (expanded as Record<string, boolean>) : {};
|
|
576
|
+
const newlyOpenedId =
|
|
577
|
+
nextOpenIds.find((rowId) => !prevMap[rowId]) ??
|
|
578
|
+
nextOpenIds[nextOpenIds.length - 1];
|
|
579
|
+
|
|
580
|
+
setExpanded(newlyOpenedId ? { [newlyOpenedId]: true } : {});
|
|
319
581
|
},
|
|
320
582
|
onPaginationChange: (updater) => {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
setCurrentPage(nextPage);
|
|
329
|
-
setPageSize(nextSize);
|
|
583
|
+
handlePaginationChange(updater);
|
|
584
|
+
setExpanded({});
|
|
585
|
+
},
|
|
586
|
+
meta: {
|
|
587
|
+
onRowClick: (_row: DisplayProductGroup, tableRow?: Row<DisplayProductGroup>) => {
|
|
588
|
+
tableRow?.toggleExpanded();
|
|
589
|
+
},
|
|
330
590
|
},
|
|
331
591
|
});
|
|
332
592
|
|
|
593
|
+
const purposeTable = useReactTable<DisplayPurposeRow>({
|
|
594
|
+
data: consentRows,
|
|
595
|
+
columns: purposeColumns,
|
|
596
|
+
getCoreRowModel: getCoreRowModel(),
|
|
597
|
+
manualPagination: true,
|
|
598
|
+
pageCount: tablePageCount,
|
|
599
|
+
state: {
|
|
600
|
+
pagination: {
|
|
601
|
+
pageIndex: currentPage - 1,
|
|
602
|
+
pageSize: pageSize,
|
|
603
|
+
},
|
|
604
|
+
},
|
|
605
|
+
onPaginationChange: handlePaginationChange,
|
|
606
|
+
});
|
|
607
|
+
|
|
333
608
|
const handleStatusFilterChange = (data: SelectOption | null) => {
|
|
334
|
-
|
|
609
|
+
if (!data || String(data.value) === allStatusOption.value) {
|
|
610
|
+
setStatusFilter(null);
|
|
611
|
+
} else {
|
|
612
|
+
setStatusFilter(data as StatusFilterOption);
|
|
613
|
+
}
|
|
335
614
|
setCurrentPage(DEFAULT_PAGE);
|
|
615
|
+
setExpanded({});
|
|
336
616
|
};
|
|
337
617
|
|
|
338
618
|
const handleProductFilterChange = (data: SelectOption | null) => {
|
|
339
|
-
|
|
619
|
+
if (!data || String(data.value) === allProductOption.value) {
|
|
620
|
+
setProductFilter(null);
|
|
621
|
+
} else {
|
|
622
|
+
setProductFilter(data as ProductFilterOption);
|
|
623
|
+
}
|
|
340
624
|
setCurrentPage(DEFAULT_PAGE);
|
|
625
|
+
setExpanded({});
|
|
341
626
|
};
|
|
342
627
|
|
|
343
628
|
const handleTabChange = (tab: 'direct' | 'nominated') => {
|
|
344
629
|
setActiveTab(tab);
|
|
345
630
|
setCurrentPage(DEFAULT_PAGE);
|
|
631
|
+
setExpanded({});
|
|
632
|
+
};
|
|
633
|
+
|
|
634
|
+
const handleViewModeChange = (mode: ConsentViewMode) => {
|
|
635
|
+
if (mode === viewMode) return;
|
|
636
|
+
setViewMode(mode);
|
|
637
|
+
setCurrentPage(DEFAULT_PAGE);
|
|
638
|
+
setExpanded({});
|
|
346
639
|
};
|
|
347
640
|
|
|
348
641
|
const handleExport = useCallback(() => {
|
|
349
|
-
const value =
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
642
|
+
const value = viewMode === 'product'
|
|
643
|
+
? productTable.getRowModel().rows.flatMap((row) =>
|
|
644
|
+
row.original.purposes.map((purpose) => {
|
|
645
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
646
|
+
const { purpose_uuid, link_reason, data_elements, ...rest } = purpose;
|
|
647
|
+
return {
|
|
648
|
+
...rest,
|
|
649
|
+
product_name: row.original.product_name,
|
|
650
|
+
product_description: row.original.product_description,
|
|
651
|
+
};
|
|
652
|
+
})
|
|
653
|
+
)
|
|
654
|
+
: purposeTable.getRowModel().rows.map((row) => {
|
|
655
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
656
|
+
const { purpose_uuid, link_reason, nominator, data_elements, ...rest } = row.original;
|
|
657
|
+
return rest;
|
|
658
|
+
});
|
|
354
659
|
exportTableToCSV(value);
|
|
355
660
|
setExportModal(false);
|
|
356
|
-
}, [
|
|
661
|
+
}, [viewMode, productTable, purposeTable]);
|
|
662
|
+
|
|
663
|
+
const handleLoadMorePurposes = useCallback(
|
|
664
|
+
async (group: DisplayProductGroup) => {
|
|
665
|
+
if (!organizationId || !workspaceId) return;
|
|
666
|
+
const cardKey = getCardKey(group);
|
|
667
|
+
if (loadingMoreByKey[cardKey]) return;
|
|
668
|
+
|
|
669
|
+
const alreadyLoaded = extraPurposesByKey[cardKey]?.length ?? 0;
|
|
670
|
+
const inlineCount = group.purposes.length;
|
|
671
|
+
const offset = inlineCount + alreadyLoaded;
|
|
672
|
+
const statusParam = statusFilter?.value;
|
|
673
|
+
const searchParam = debouncedSearch.trim() || undefined;
|
|
674
|
+
|
|
675
|
+
setLoadingMoreByKey((prev) => ({ ...prev, [cardKey]: true }));
|
|
676
|
+
try {
|
|
677
|
+
const result = await fetchGroupPurposes({
|
|
678
|
+
organizationId,
|
|
679
|
+
workspaceId,
|
|
680
|
+
productUuid: group.product_uuid,
|
|
681
|
+
offset,
|
|
682
|
+
limit: 50,
|
|
683
|
+
status: statusParam,
|
|
684
|
+
search: searchParam,
|
|
685
|
+
language: langParam,
|
|
686
|
+
nominatorUuid: group.nominator?.uuid,
|
|
687
|
+
authContext,
|
|
688
|
+
consentClient,
|
|
689
|
+
});
|
|
690
|
+
if (!result) {
|
|
691
|
+
throw new Error('Failed to load more purposes');
|
|
692
|
+
}
|
|
693
|
+
setExtraPurposesByKey((prev) => ({
|
|
694
|
+
...prev,
|
|
695
|
+
[cardKey]: [...(prev[cardKey] ?? []), ...result.purposes],
|
|
696
|
+
}));
|
|
697
|
+
const fetchedTotal = offset + result.purposes.length;
|
|
698
|
+
const hasMore = fetchedTotal < result.pagination.total_count;
|
|
699
|
+
setHasMoreByKey((prev) => ({ ...prev, [cardKey]: hasMore }));
|
|
700
|
+
} catch (error) {
|
|
701
|
+
console.error('Failed to load more purposes:', error);
|
|
702
|
+
toast.error(t('failedToFetchConsentData'));
|
|
703
|
+
} finally {
|
|
704
|
+
setLoadingMoreByKey((prev) => ({ ...prev, [cardKey]: false }));
|
|
705
|
+
}
|
|
706
|
+
},
|
|
707
|
+
[
|
|
708
|
+
organizationId,
|
|
709
|
+
workspaceId,
|
|
710
|
+
authContext,
|
|
711
|
+
consentClient,
|
|
712
|
+
langParam,
|
|
713
|
+
statusFilter?.value,
|
|
714
|
+
debouncedSearch,
|
|
715
|
+
extraPurposesByKey,
|
|
716
|
+
loadingMoreByKey,
|
|
717
|
+
getCardKey,
|
|
718
|
+
t,
|
|
719
|
+
]
|
|
720
|
+
);
|
|
357
721
|
|
|
358
722
|
const handleManageConsent = useCallback(
|
|
359
723
|
async (
|
|
@@ -419,187 +783,219 @@ export default function ConsentManager({ variant }: ConsentManagerProps) {
|
|
|
419
783
|
|
|
420
784
|
const getVar = (varName: string): string => `var(${varName})`;
|
|
421
785
|
|
|
422
|
-
const directBadge =
|
|
423
|
-
consentDetail?.status_summary?.direct_purposes ??
|
|
424
|
-
(activeTab === 'direct' ? totalCount : '');
|
|
786
|
+
const directBadge: number | '' =
|
|
787
|
+
consentDetail?.status_summary?.direct_purposes ?? '';
|
|
425
788
|
|
|
426
789
|
const nominatedBadge: number | '' =
|
|
427
|
-
consentDetail?.status_summary?.nominated_purposes ??
|
|
428
|
-
(activeTab === 'nominated' ? totalCount : '');
|
|
429
|
-
|
|
430
|
-
const renderUserRelationshipStrip = () => {
|
|
431
|
-
if (!consentDetail) return null;
|
|
432
|
-
|
|
433
|
-
const status = consentDetail.user_status;
|
|
434
|
-
let statusLabel: string | null = null;
|
|
435
|
-
let statusBgColor: string | null = null;
|
|
436
|
-
let statusTextColor: string | null = null;
|
|
437
|
-
if (status === 'ACTIVE') {
|
|
438
|
-
statusLabel = t('statusActive');
|
|
439
|
-
statusBgColor = 'rgba(16, 185, 129, 0.1)';
|
|
440
|
-
statusTextColor = getVar('--pc-success');
|
|
441
|
-
} else if (status === 'TRANSFERRED') {
|
|
442
|
-
statusLabel = t('statusTransferred');
|
|
443
|
-
statusBgColor = 'rgba(245, 158, 11, 0.1)';
|
|
444
|
-
statusTextColor = getVar('--pc-warning');
|
|
445
|
-
}
|
|
790
|
+
consentDetail?.status_summary?.nominated_purposes ?? '';
|
|
446
791
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
return (
|
|
456
|
-
<div style={{
|
|
457
|
-
marginBottom: getVar('--pc-spacing-md'),
|
|
458
|
-
borderRadius: getVar('--pc-border-radius-lg'),
|
|
459
|
-
border: `1px solid ${getVar('--pc-border-color')}`,
|
|
460
|
-
backgroundColor: getVar('--pc-bg-secondary'),
|
|
461
|
-
padding: getVar('--pc-spacing-md'),
|
|
462
|
-
}}>
|
|
463
|
-
<div style={{
|
|
464
|
-
display: 'flex',
|
|
465
|
-
flexDirection: 'column',
|
|
466
|
-
gap: getVar('--pc-spacing-sm'),
|
|
467
|
-
}} className="pc-flex-responsive">
|
|
468
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
|
469
|
-
<PCText size="sm" weight="semibold" style={{ margin: 0, color: getVar('--pc-text-primary') }}>
|
|
470
|
-
{t('consentVisibility')}
|
|
471
|
-
</PCText>
|
|
472
|
-
{nominatorLabels.length > 0 && (
|
|
473
|
-
<PCText size="sm" style={{ margin: 0, color: getVar('--pc-text-secondary') }}>
|
|
474
|
-
{t('managingConsentsFor')}{' '}
|
|
475
|
-
<span style={{ fontWeight: 600, color: getVar('--pc-text-primary') }}>
|
|
476
|
-
{nominatorLabels.join(', ')}
|
|
477
|
-
</span>
|
|
478
|
-
</PCText>
|
|
479
|
-
)}
|
|
480
|
-
</div>
|
|
481
|
-
{statusLabel && (
|
|
482
|
-
<div style={{ flexShrink: 0 }}>
|
|
483
|
-
<span style={{
|
|
484
|
-
display: 'inline-flex',
|
|
485
|
-
alignItems: 'center',
|
|
486
|
-
borderRadius: '9999px',
|
|
487
|
-
padding: '0.25rem 0.75rem',
|
|
488
|
-
fontSize: getVar('--pc-font-size-xs'),
|
|
489
|
-
fontWeight: 600,
|
|
490
|
-
backgroundColor: statusBgColor!,
|
|
491
|
-
color: statusTextColor!,
|
|
492
|
-
}}>
|
|
493
|
-
{statusLabel}
|
|
494
|
-
</span>
|
|
495
|
-
</div>
|
|
496
|
-
)}
|
|
497
|
-
</div>
|
|
498
|
-
</div>
|
|
499
|
-
);
|
|
500
|
-
};
|
|
792
|
+
const nominatorLabels = (consentDetail?.nominators || [])
|
|
793
|
+
.map((n) => n.email?.trim() || n.org_user_id?.trim() || n.uuid?.trim() || '')
|
|
794
|
+
.filter(Boolean);
|
|
795
|
+
const summaryText = t('productsAndConsentsSummary', {
|
|
796
|
+
products: visibleProductCount,
|
|
797
|
+
consents: visibleConsentCount,
|
|
798
|
+
});
|
|
501
799
|
|
|
502
800
|
return (
|
|
503
|
-
<div style={{ width: '100%' }}>
|
|
504
|
-
{/* User Relationship Strip */}
|
|
505
|
-
{renderUserRelationshipStrip()}
|
|
506
|
-
|
|
801
|
+
<div className="pc-consent-manager-surface" style={{ width: '100%' }}>
|
|
507
802
|
<div
|
|
803
|
+
className="pc-consent-shell-card"
|
|
508
804
|
style={{
|
|
509
|
-
|
|
510
|
-
border: `1px solid ${getVar('--pc-border-color')}`,
|
|
511
|
-
backgroundColor: getVar('--pc-bg-primary'),
|
|
512
|
-
boxShadow: getVar('--pc-shadow-sm'),
|
|
513
|
-
padding: getVar('--pc-spacing-md'),
|
|
514
|
-
marginBottom: getVar('--pc-spacing-lg'),
|
|
805
|
+
backgroundColor: 'transparent',
|
|
515
806
|
}}
|
|
516
807
|
>
|
|
517
|
-
{/* Tabs: My Consents / Managing */}
|
|
518
|
-
<div
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
808
|
+
{/* Tabs: My Consents / Managing + Product/Purpose toggle */}
|
|
809
|
+
<div
|
|
810
|
+
className="pc-consent-tabs-row"
|
|
811
|
+
style={{
|
|
812
|
+
display: 'flex',
|
|
813
|
+
alignItems: 'center',
|
|
814
|
+
justifyContent: 'space-between',
|
|
815
|
+
gap: getVar('--pc-spacing-sm'),
|
|
816
|
+
borderBottom: `1px solid ${getVar('--pc-border-color')}`,
|
|
817
|
+
padding: '0.125rem 0 0.375rem',
|
|
818
|
+
}}
|
|
819
|
+
>
|
|
820
|
+
<div className="pc-consent-tabs-left" style={{ display: 'flex', minWidth: 0 }}>
|
|
821
|
+
<button
|
|
822
|
+
type="button"
|
|
823
|
+
onClick={() => handleTabChange('direct')}
|
|
824
|
+
style={{
|
|
825
|
+
display: 'flex',
|
|
826
|
+
alignItems: 'center',
|
|
827
|
+
gap: getVar('--pc-spacing-sm'),
|
|
828
|
+
padding: `0.5rem ${getVar('--pc-spacing-sm')}`,
|
|
829
|
+
fontSize: '0.75rem',
|
|
830
|
+
fontWeight: 600,
|
|
831
|
+
background: 'none',
|
|
832
|
+
border: 'none',
|
|
833
|
+
cursor: 'pointer',
|
|
834
|
+
borderBottom: activeTab === 'direct' ? `2px solid ${getVar('--pc-primary')}` : '2px solid transparent',
|
|
835
|
+
color: activeTab === 'direct' ? getVar('--pc-primary') : getVar('--pc-text-secondary'),
|
|
836
|
+
transition: 'color 0.2s, border-color 0.2s',
|
|
837
|
+
}}
|
|
838
|
+
>
|
|
839
|
+
{t('myConsents')}
|
|
840
|
+
<span style={{
|
|
841
|
+
borderRadius: '9999px',
|
|
842
|
+
backgroundColor: getVar('--pc-bg-tertiary'),
|
|
843
|
+
padding: '0.0625rem 0.375rem',
|
|
844
|
+
fontSize: '0.6875rem',
|
|
845
|
+
color: getVar('--pc-text-secondary'),
|
|
846
|
+
}}>
|
|
847
|
+
{directBadge}
|
|
848
|
+
</span>
|
|
849
|
+
</button>
|
|
850
|
+
<button
|
|
851
|
+
type="button"
|
|
852
|
+
onClick={() => handleTabChange('nominated')}
|
|
853
|
+
style={{
|
|
854
|
+
display: 'flex',
|
|
855
|
+
alignItems: 'center',
|
|
856
|
+
gap: getVar('--pc-spacing-sm'),
|
|
857
|
+
padding: `0.5rem ${getVar('--pc-spacing-sm')}`,
|
|
858
|
+
fontSize: '0.75rem',
|
|
859
|
+
fontWeight: 600,
|
|
860
|
+
background: 'none',
|
|
861
|
+
border: 'none',
|
|
862
|
+
cursor: 'pointer',
|
|
863
|
+
borderBottom: activeTab === 'nominated' ? `2px solid ${getVar('--pc-primary')}` : '2px solid transparent',
|
|
864
|
+
color: activeTab === 'nominated' ? getVar('--pc-primary') : getVar('--pc-text-secondary'),
|
|
865
|
+
transition: 'color 0.2s, border-color 0.2s',
|
|
866
|
+
}}
|
|
867
|
+
>
|
|
868
|
+
{t('managing')}
|
|
869
|
+
<span style={{
|
|
870
|
+
borderRadius: '9999px',
|
|
871
|
+
backgroundColor: getVar('--pc-bg-tertiary'),
|
|
872
|
+
padding: '0.0625rem 0.375rem',
|
|
873
|
+
fontSize: '0.6875rem',
|
|
874
|
+
color: getVar('--pc-text-secondary'),
|
|
875
|
+
}}>
|
|
876
|
+
{nominatedBadge}
|
|
877
|
+
</span>
|
|
878
|
+
</button>
|
|
879
|
+
</div>
|
|
880
|
+
|
|
881
|
+
<div
|
|
882
|
+
className="pc-consent-view-toggle"
|
|
527
883
|
style={{
|
|
884
|
+
marginLeft: 'auto',
|
|
528
885
|
display: 'flex',
|
|
529
886
|
alignItems: 'center',
|
|
530
887
|
gap: getVar('--pc-spacing-sm'),
|
|
531
|
-
|
|
532
|
-
fontSize: getVar('--pc-font-size-sm'),
|
|
533
|
-
fontWeight: 600,
|
|
534
|
-
background: 'none',
|
|
535
|
-
border: 'none',
|
|
536
|
-
cursor: 'pointer',
|
|
537
|
-
borderBottom: activeTab === 'direct' ? `2px solid ${getVar('--pc-primary')}` : '2px solid transparent',
|
|
538
|
-
color: activeTab === 'direct' ? getVar('--pc-primary') : getVar('--pc-text-secondary'),
|
|
539
|
-
transition: 'color 0.2s, border-color 0.2s',
|
|
888
|
+
paddingRight: getVar('--pc-spacing-sm'),
|
|
540
889
|
}}
|
|
541
890
|
>
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
891
|
+
<PCText
|
|
892
|
+
size="sm"
|
|
893
|
+
color="secondary"
|
|
894
|
+
className="pc-consent-view-toggle-label"
|
|
895
|
+
style={{ margin: 0, fontSize: '0.75rem', fontWeight: 500 }}
|
|
896
|
+
>
|
|
897
|
+
{t('consentVisibility')}
|
|
898
|
+
</PCText>
|
|
899
|
+
<div
|
|
900
|
+
className="pc-consent-view-toggle-group"
|
|
901
|
+
style={{
|
|
902
|
+
display: 'inline-flex',
|
|
903
|
+
borderRadius: '9999px',
|
|
904
|
+
backgroundColor: getVar('--pc-bg-secondary'),
|
|
905
|
+
padding: '0.125rem',
|
|
906
|
+
}}
|
|
907
|
+
>
|
|
908
|
+
<button
|
|
909
|
+
type="button"
|
|
910
|
+
className={`pc-consent-view-toggle-btn ${viewMode === 'product' ? 'is-active' : ''}`}
|
|
911
|
+
onClick={() => handleViewModeChange('product')}
|
|
912
|
+
style={{
|
|
913
|
+
minWidth: '5.25rem',
|
|
914
|
+
minHeight: '1.75rem',
|
|
915
|
+
padding: '0.3125rem 0.75rem',
|
|
916
|
+
border: 'none',
|
|
917
|
+
backgroundColor:
|
|
918
|
+
viewMode === 'product' ? getVar('--pc-primary') : 'transparent',
|
|
919
|
+
color:
|
|
920
|
+
viewMode === 'product'
|
|
921
|
+
? getVar('--pc-text-inverse')
|
|
922
|
+
: getVar('--pc-text-secondary'),
|
|
923
|
+
fontSize: '0.6875rem',
|
|
924
|
+
fontWeight: 600,
|
|
925
|
+
cursor: 'pointer',
|
|
926
|
+
borderRadius: '9999px',
|
|
927
|
+
transition: 'background-color 0.2s ease, color 0.2s ease',
|
|
928
|
+
}}
|
|
929
|
+
>
|
|
930
|
+
{t('product')}
|
|
931
|
+
</button>
|
|
932
|
+
<button
|
|
933
|
+
type="button"
|
|
934
|
+
className={`pc-consent-view-toggle-btn ${viewMode === 'purpose' ? 'is-active' : ''}`}
|
|
935
|
+
onClick={() => handleViewModeChange('purpose')}
|
|
936
|
+
style={{
|
|
937
|
+
minWidth: '5.25rem',
|
|
938
|
+
minHeight: '1.75rem',
|
|
939
|
+
padding: '0.3125rem 0.75rem',
|
|
940
|
+
border: 'none',
|
|
941
|
+
backgroundColor:
|
|
942
|
+
viewMode === 'purpose' ? getVar('--pc-primary') : 'transparent',
|
|
943
|
+
color:
|
|
944
|
+
viewMode === 'purpose'
|
|
945
|
+
? getVar('--pc-text-inverse')
|
|
946
|
+
: getVar('--pc-text-secondary'),
|
|
947
|
+
fontSize: '0.6875rem',
|
|
948
|
+
fontWeight: 600,
|
|
949
|
+
cursor: 'pointer',
|
|
950
|
+
borderRadius: '9999px',
|
|
951
|
+
transition: 'background-color 0.2s ease, color 0.2s ease',
|
|
952
|
+
}}
|
|
953
|
+
>
|
|
954
|
+
{t('purpose')}
|
|
955
|
+
</button>
|
|
956
|
+
</div>
|
|
957
|
+
</div>
|
|
958
|
+
</div>
|
|
959
|
+
|
|
960
|
+
{activeTab === 'nominated' && nominatorLabels.length > 0 && (
|
|
961
|
+
<PCText
|
|
962
|
+
size="sm"
|
|
963
|
+
color="secondary"
|
|
556
964
|
style={{
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
fontSize: getVar('--pc-font-size-sm'),
|
|
562
|
-
fontWeight: 600,
|
|
563
|
-
background: 'none',
|
|
564
|
-
border: 'none',
|
|
565
|
-
cursor: 'pointer',
|
|
566
|
-
borderBottom: activeTab === 'nominated' ? `2px solid ${getVar('--pc-primary')}` : '2px solid transparent',
|
|
567
|
-
color: activeTab === 'nominated' ? getVar('--pc-primary') : getVar('--pc-text-secondary'),
|
|
568
|
-
transition: 'color 0.2s, border-color 0.2s',
|
|
965
|
+
margin: 0,
|
|
966
|
+
marginBottom: getVar('--pc-spacing-sm'),
|
|
967
|
+
paddingLeft: getVar('--pc-spacing-lg'),
|
|
968
|
+
paddingRight: getVar('--pc-spacing-lg'),
|
|
569
969
|
}}
|
|
570
970
|
>
|
|
571
|
-
{t('
|
|
572
|
-
<span style={{
|
|
573
|
-
|
|
574
|
-
backgroundColor: getVar('--pc-bg-tertiary'),
|
|
575
|
-
padding: '0.125rem 0.5rem',
|
|
576
|
-
fontSize: getVar('--pc-font-size-xs'),
|
|
577
|
-
color: getVar('--pc-text-secondary'),
|
|
578
|
-
}}>
|
|
579
|
-
{nominatedBadge}
|
|
971
|
+
{t('managingConsentsFor')}{' '}
|
|
972
|
+
<span style={{ fontWeight: 600, color: getVar('--pc-text-primary') }}>
|
|
973
|
+
{nominatorLabels.join(', ')}
|
|
580
974
|
</span>
|
|
581
|
-
</
|
|
582
|
-
|
|
975
|
+
</PCText>
|
|
976
|
+
)}
|
|
583
977
|
|
|
584
978
|
{/* Filters Section: Search on left, filters and export on the right */}
|
|
585
979
|
<div style={{
|
|
586
980
|
display: 'flex',
|
|
587
981
|
flexDirection: 'column',
|
|
588
|
-
gap: getVar('--pc-spacing-
|
|
982
|
+
gap: getVar('--pc-spacing-sm'),
|
|
589
983
|
width: '100%',
|
|
984
|
+
padding: '0.75rem 0 0.625rem',
|
|
590
985
|
}} className="pc-filters-responsive">
|
|
591
986
|
<div style={{ width: '100%' }} className="pc-search-responsive">
|
|
592
987
|
<PCInput
|
|
593
988
|
type="search"
|
|
989
|
+
size="sm"
|
|
594
990
|
clearable
|
|
595
991
|
clearAriaLabel={t('clearInput')}
|
|
596
|
-
placeholder={
|
|
992
|
+
placeholder={composedSearchPlaceholder}
|
|
597
993
|
value={searchQuery}
|
|
598
994
|
onClear={() => {
|
|
599
995
|
setSearchQuery('');
|
|
600
996
|
setCurrentPage(DEFAULT_PAGE);
|
|
601
997
|
}}
|
|
602
|
-
prefix={<Search size={
|
|
998
|
+
prefix={<Search size={14} strokeWidth={1.8} />}
|
|
603
999
|
onChange={(e) => {
|
|
604
1000
|
setSearchQuery(e.target.value);
|
|
605
1001
|
setCurrentPage(DEFAULT_PAGE);
|
|
@@ -613,57 +1009,127 @@ export default function ConsentManager({ variant }: ConsentManagerProps) {
|
|
|
613
1009
|
<div style={{
|
|
614
1010
|
display: 'flex',
|
|
615
1011
|
flexDirection: 'column',
|
|
616
|
-
gap: getVar('--pc-spacing-
|
|
1012
|
+
gap: getVar('--pc-spacing-sm'),
|
|
617
1013
|
width: '100%',
|
|
618
1014
|
}} className="pc-filters-row-responsive">
|
|
619
1015
|
<PCSelect
|
|
620
1016
|
className="pc-filter-select"
|
|
1017
|
+
size="sm"
|
|
621
1018
|
placeholder={t('status')}
|
|
622
|
-
|
|
623
|
-
|
|
1019
|
+
prefix={<Filter size={12} strokeWidth={1.8} />}
|
|
1020
|
+
options={statusFilterOptions}
|
|
1021
|
+
value={statusSelectValue}
|
|
624
1022
|
onChange={handleStatusFilterChange}
|
|
625
|
-
|
|
626
|
-
clearable={statusFilter !== null}
|
|
627
|
-
clearAriaLabel={t('clearSelection')}
|
|
628
|
-
dropdownMatchTriggerWidth
|
|
1023
|
+
showSelectedCheck
|
|
629
1024
|
style={{
|
|
630
1025
|
width: '100%',
|
|
631
1026
|
}}
|
|
632
1027
|
/>
|
|
633
1028
|
<PCSelect
|
|
634
1029
|
className="pc-filter-select"
|
|
1030
|
+
size="sm"
|
|
635
1031
|
placeholder={t('product')}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
options={productOptions}
|
|
640
|
-
value={productFilter}
|
|
1032
|
+
prefix={<Circle size={10} strokeWidth={2} />}
|
|
1033
|
+
options={productFilterOptions}
|
|
1034
|
+
value={productSelectValue}
|
|
641
1035
|
onChange={handleProductFilterChange}
|
|
642
|
-
|
|
643
|
-
clearable={productFilter !== null}
|
|
644
|
-
clearAriaLabel={t('clearSelection')}
|
|
645
|
-
dropdownMatchTriggerWidth
|
|
1036
|
+
showSelectedCheck
|
|
646
1037
|
style={{
|
|
647
1038
|
width: '100%',
|
|
648
1039
|
}}
|
|
649
1040
|
/>
|
|
1041
|
+
<PCText
|
|
1042
|
+
size="sm"
|
|
1043
|
+
color="secondary"
|
|
1044
|
+
className="pc-filters-summary"
|
|
1045
|
+
style={{
|
|
1046
|
+
margin: 0,
|
|
1047
|
+
whiteSpace: 'nowrap',
|
|
1048
|
+
alignSelf: 'center',
|
|
1049
|
+
}}
|
|
1050
|
+
>
|
|
1051
|
+
{summaryText}
|
|
1052
|
+
</PCText>
|
|
650
1053
|
<div className="pc-export-button-responsive">
|
|
651
1054
|
<PCButton
|
|
1055
|
+
size="sm"
|
|
652
1056
|
variant="outline"
|
|
653
1057
|
onClick={() => setExportModal(true)}
|
|
654
1058
|
style={{
|
|
655
1059
|
whiteSpace: 'nowrap',
|
|
656
1060
|
width: '100%',
|
|
657
|
-
minHeight: '2.25rem',
|
|
658
|
-
padding: '0.4375rem 0.875rem',
|
|
659
1061
|
}}
|
|
660
1062
|
>
|
|
661
|
-
<ArrowUpToLine size={
|
|
1063
|
+
<ArrowUpToLine size={13} />
|
|
662
1064
|
<span>{t('export')}</span>
|
|
663
1065
|
</PCButton>
|
|
664
1066
|
</div>
|
|
665
1067
|
</div>
|
|
666
1068
|
</div>
|
|
1069
|
+
{isLoading ? (
|
|
1070
|
+
<ConsentManagerTableSkeleton />
|
|
1071
|
+
) : (
|
|
1072
|
+
<>
|
|
1073
|
+
<div className="pc-consent-desktop-view">
|
|
1074
|
+
{viewMode === 'product' ? (
|
|
1075
|
+
<AdvancedTable
|
|
1076
|
+
table={productTable}
|
|
1077
|
+
variant={variant}
|
|
1078
|
+
classNames={groupedTableClassNames}
|
|
1079
|
+
isLoading={isLoading}
|
|
1080
|
+
expandedComponent={(row) => {
|
|
1081
|
+
const cardKey = getCardKey(row.original);
|
|
1082
|
+
return (
|
|
1083
|
+
<ConsentManagerExpandedRow
|
|
1084
|
+
group={row.original}
|
|
1085
|
+
onModify={handleOpenModal}
|
|
1086
|
+
extraPurposes={extraPurposesByKey[cardKey]}
|
|
1087
|
+
hasMore={hasMoreByKey[cardKey] ?? row.original.has_more_purposes}
|
|
1088
|
+
isLoadingMore={loadingMoreByKey[cardKey]}
|
|
1089
|
+
onLoadMore={handleLoadMorePurposes}
|
|
1090
|
+
/>
|
|
1091
|
+
);
|
|
1092
|
+
}}
|
|
1093
|
+
/>
|
|
1094
|
+
) : (
|
|
1095
|
+
<AdvancedTable
|
|
1096
|
+
table={purposeTable}
|
|
1097
|
+
variant={variant}
|
|
1098
|
+
classNames={purposeTableClassNames}
|
|
1099
|
+
isLoading={isLoading}
|
|
1100
|
+
/>
|
|
1101
|
+
)}
|
|
1102
|
+
</div>
|
|
1103
|
+
|
|
1104
|
+
<div className="pc-consent-mobile-view">
|
|
1105
|
+
{viewMode === 'product' ? (
|
|
1106
|
+
<ConsentManagerProductCards
|
|
1107
|
+
groups={consentGroups}
|
|
1108
|
+
onModify={handleOpenModal}
|
|
1109
|
+
extraPurposesByKey={extraPurposesByKey}
|
|
1110
|
+
hasMoreByKey={hasMoreByKey}
|
|
1111
|
+
loadingMoreByKey={loadingMoreByKey}
|
|
1112
|
+
onLoadMore={handleLoadMorePurposes}
|
|
1113
|
+
cardKeyFor={getCardKey}
|
|
1114
|
+
/>
|
|
1115
|
+
) : (
|
|
1116
|
+
<ConsentManagerPurposeCards
|
|
1117
|
+
rows={consentRows}
|
|
1118
|
+
onModify={handleOpenModal}
|
|
1119
|
+
/>
|
|
1120
|
+
)}
|
|
1121
|
+
</div>
|
|
1122
|
+
</>
|
|
1123
|
+
)}
|
|
1124
|
+
|
|
1125
|
+
{/* Always show pagination, even during loading */}
|
|
1126
|
+
<div className="pc-consent-pagination-wrap">
|
|
1127
|
+
{viewMode === 'product' ? (
|
|
1128
|
+
<AdvancedTablePagination table={productTable} />
|
|
1129
|
+
) : (
|
|
1130
|
+
<AdvancedTablePagination table={purposeTable} />
|
|
1131
|
+
)}
|
|
1132
|
+
</div>
|
|
667
1133
|
</div>
|
|
668
1134
|
|
|
669
1135
|
<PCModal
|
|
@@ -703,23 +1169,6 @@ export default function ConsentManager({ variant }: ConsentManagerProps) {
|
|
|
703
1169
|
</div>
|
|
704
1170
|
</PCModal>
|
|
705
1171
|
|
|
706
|
-
{isLoading ? (
|
|
707
|
-
<ConsentManagerTableSkeleton />
|
|
708
|
-
) : (
|
|
709
|
-
<AdvancedTable table={table} variant={variant} isLoading={isLoading} />
|
|
710
|
-
)}
|
|
711
|
-
|
|
712
|
-
{/* Always show pagination, even during loading */}
|
|
713
|
-
<div style={{
|
|
714
|
-
...pcStyles.flex,
|
|
715
|
-
...pcStyles.flexCol,
|
|
716
|
-
...pcStyles.itemsCenter,
|
|
717
|
-
...pcStyles.justifyBetween,
|
|
718
|
-
...pcStyles.gapSm,
|
|
719
|
-
}} className="pc-flex-responsive">
|
|
720
|
-
<AdvancedTablePagination table={table} />
|
|
721
|
-
</div>
|
|
722
|
-
|
|
723
1172
|
<ModifyConsentModal
|
|
724
1173
|
isOpen={isModalOpen}
|
|
725
1174
|
onClose={handleCloseModal}
|