@redacto.io/consent-sdk-react 9.2.0-beta.0 → 9.2.0-beta.2
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 +12 -0
- package/dist/privacy-center.d.mts +51 -1
- package/dist/privacy-center.d.ts +51 -1
- package/dist/privacy-center.js +3463 -1148
- package/dist/privacy-center.mjs +3399 -1084
- 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 +402 -259
- package/src/RedactoPrivacyCenter/components/ReceiptsList.tsx +492 -0
- package/src/RedactoPrivacyCenter/components/RequestSubmit.tsx +95 -57
- 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 +20 -2
- package/src/RedactoPrivacyCenter/context/NavigationContext.tsx +2 -2
- package/src/RedactoPrivacyCenter/lib/api.ts +67 -8
- package/src/RedactoPrivacyCenter/lib/constants.ts +5 -0
- package/src/RedactoPrivacyCenter/lib/types.ts +74 -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 +1144 -0
- package/tests/ProductSelectionSection.test.tsx +328 -0
- package/tests/RequestSubmit.test.tsx +135 -0
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback, useMemo } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { getCoreRowModel, useReactTable } from '@tanstack/react-table';
|
|
4
|
+
import dayjs from 'dayjs';
|
|
5
|
+
import { CalendarDays, Filter } from 'lucide-react';
|
|
6
|
+
import { AdvancedTable, AdvancedTablePagination } from '../table';
|
|
7
|
+
import { PCSelect, PCInput, PCButton, PCText, type SelectOption } from '../ui';
|
|
8
|
+
import { pcStyles } from '../styles/pcStyles';
|
|
9
|
+
import { getVar } from '../lib/utils';
|
|
10
|
+
import { fetchReceipts, fetchReceiptPdf } from '../lib/api';
|
|
11
|
+
import { useOrganization } from '../context/OrganizationContext';
|
|
12
|
+
import { useAuth } from '../context/AuthContext';
|
|
13
|
+
import { useBaseUrl } from '../context/BaseUrlContext';
|
|
14
|
+
import { useCurrentLanguage } from '../lib/useCurrentLanguage';
|
|
15
|
+
import { toast } from '../utils/toast';
|
|
16
|
+
import type { Receipt, ReceiptEventType, ReceiptPurposeStatus } from '../lib/types';
|
|
17
|
+
import { useReceiptColumns } from './receiptColumns';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Normalize the (still-evolving) receipts response into rows + total.
|
|
21
|
+
* Defensive about the wrapper shape ({data} | {items} | array) and the total
|
|
22
|
+
* key so the table works regardless of the exact envelope the API returns.
|
|
23
|
+
*/
|
|
24
|
+
function normalizeReceipts(resp: unknown): { rows: Receipt[]; total: number } {
|
|
25
|
+
if (!resp || typeof resp !== 'object') return { rows: [], total: 0 };
|
|
26
|
+
const r = resp as Record<string, unknown>;
|
|
27
|
+
const arr = (Array.isArray(resp)
|
|
28
|
+
? resp
|
|
29
|
+
: [r.data, r.items, r.receipts, r.results].find(Array.isArray)) as unknown[] | undefined;
|
|
30
|
+
const rows: Receipt[] = (arr ?? []).map((row) => {
|
|
31
|
+
const rec = (row ?? {}) as Record<string, unknown>;
|
|
32
|
+
// The receipt id field name may vary — coerce it so downloads always work.
|
|
33
|
+
return { ...rec, uuid: (rec.uuid ?? rec.receipt_uuid ?? rec.id ?? '') as string } as Receipt;
|
|
34
|
+
});
|
|
35
|
+
const pag = (r.pagination ?? r.meta ?? {}) as Record<string, unknown>;
|
|
36
|
+
const totalRaw = pag.total_count ?? pag.total ?? r.total_count ?? r.total ?? r.count ?? rows.length;
|
|
37
|
+
const total = Number(totalRaw);
|
|
38
|
+
return { rows, total: Number.isFinite(total) ? total : rows.length };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Map the date filter (preset or custom range) to the API's inclusive
|
|
43
|
+
* created_after / created_before ISO datetimes.
|
|
44
|
+
*/
|
|
45
|
+
function dateRangeParams(
|
|
46
|
+
preset: string,
|
|
47
|
+
from: string,
|
|
48
|
+
to: string
|
|
49
|
+
): { created_after?: string; created_before?: string } {
|
|
50
|
+
if (preset === 'custom') {
|
|
51
|
+
return {
|
|
52
|
+
created_after: from ? dayjs(from).startOf('day').toISOString() : undefined,
|
|
53
|
+
created_before: to ? dayjs(to).endOf('day').toISOString() : undefined,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
if (preset === '1') {
|
|
57
|
+
return { created_after: dayjs().startOf('day').toISOString() };
|
|
58
|
+
}
|
|
59
|
+
if (preset === '7' || preset === '30' || preset === '90') {
|
|
60
|
+
return { created_after: dayjs().subtract(Number(preset), 'day').startOf('day').toISOString() };
|
|
61
|
+
}
|
|
62
|
+
return {};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export default function ReceiptsList() {
|
|
66
|
+
const { t } = useTranslation();
|
|
67
|
+
const { organization } = useOrganization();
|
|
68
|
+
const { accessToken, refreshToken, onError, updateTokens } = useAuth();
|
|
69
|
+
const { consentClient } = useBaseUrl();
|
|
70
|
+
const { langParam } = useCurrentLanguage();
|
|
71
|
+
|
|
72
|
+
const organizationId = organization?.uuid;
|
|
73
|
+
const workspaceId = organization?.workspaces[0]?.uuid;
|
|
74
|
+
|
|
75
|
+
const authContext = useMemo(
|
|
76
|
+
() => ({ accessToken, refreshToken, onError, updateTokens }),
|
|
77
|
+
[accessToken, refreshToken, onError, updateTokens]
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const [receipts, setReceipts] = useState<Receipt[]>([]);
|
|
81
|
+
const [totalCount, setTotalCount] = useState(0);
|
|
82
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
83
|
+
const [currentPage, setCurrentPage] = useState(1);
|
|
84
|
+
const [pageSize, setPageSize] = useState(10);
|
|
85
|
+
const [downloadingUuid, setDownloadingUuid] = useState<string | null>(null);
|
|
86
|
+
// Keep showing prior rows while a new page loads (smooth pagination).
|
|
87
|
+
const [previousData, setPreviousData] = useState<Receipt[]>([]);
|
|
88
|
+
const [previousTotalCount, setPreviousTotalCount] = useState(0);
|
|
89
|
+
|
|
90
|
+
// ─── Filters ────────────────────────────────────────────────────────────────
|
|
91
|
+
const [eventType, setEventType] = useState<ReceiptEventType | ''>('');
|
|
92
|
+
const [status, setStatus] = useState<ReceiptPurposeStatus | ''>('');
|
|
93
|
+
const [datePreset, setDatePreset] = useState<string>(''); // '' | '1' | '7' | '30' | '90' | 'custom'
|
|
94
|
+
const [customFrom, setCustomFrom] = useState<string>('');
|
|
95
|
+
const [customTo, setCustomTo] = useState<string>('');
|
|
96
|
+
|
|
97
|
+
const eventTypeOptions = useMemo<SelectOption[]>(
|
|
98
|
+
() => [
|
|
99
|
+
{ value: '', label: t('all') },
|
|
100
|
+
{ value: 'GRANTED', label: t('consentGranted') },
|
|
101
|
+
{ value: 'WITHDRAWN', label: t('consentRevoked') },
|
|
102
|
+
{ value: 'DECLINED', label: t('consentDeclined') },
|
|
103
|
+
{ value: 'EXPIRED', label: t('consentExpired') },
|
|
104
|
+
],
|
|
105
|
+
[t]
|
|
106
|
+
);
|
|
107
|
+
const statusOptions = useMemo<SelectOption[]>(
|
|
108
|
+
() => [
|
|
109
|
+
{ value: '', label: t('all') },
|
|
110
|
+
{ value: 'CONSENTED', label: t('consented') },
|
|
111
|
+
{ value: 'DECLINED', label: t('declined') },
|
|
112
|
+
{ value: 'WITHDRAWN', label: t('withdrawn') },
|
|
113
|
+
],
|
|
114
|
+
[t]
|
|
115
|
+
);
|
|
116
|
+
const dateOptions = useMemo<SelectOption[]>(
|
|
117
|
+
() => [
|
|
118
|
+
{ value: '', label: t('allTime') },
|
|
119
|
+
{ value: '1', label: t('today') },
|
|
120
|
+
{ value: '7', label: t('last7Days') },
|
|
121
|
+
{ value: '30', label: t('last30Days') },
|
|
122
|
+
{ value: '90', label: t('last3Months') },
|
|
123
|
+
{ value: 'custom', label: t('customRange') },
|
|
124
|
+
],
|
|
125
|
+
[t]
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const eventValue = eventTypeOptions.find((o) => o.value === eventType) ?? null;
|
|
129
|
+
const statusValue = statusOptions.find((o) => o.value === status) ?? null;
|
|
130
|
+
const dateValue = dateOptions.find((o) => o.value === datePreset) ?? null;
|
|
131
|
+
const hasActiveFilters = Boolean(eventType || status || datePreset);
|
|
132
|
+
const isCustomRangeInvalid = useMemo(
|
|
133
|
+
() =>
|
|
134
|
+
datePreset === 'custom' &&
|
|
135
|
+
Boolean(customFrom && customTo && dayjs(customFrom).isAfter(dayjs(customTo), 'day')),
|
|
136
|
+
[datePreset, customFrom, customTo]
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const activeFilterLabels = useMemo(() => {
|
|
140
|
+
const filters: { id: string; label: string; value: string }[] = [];
|
|
141
|
+
|
|
142
|
+
if (eventType && eventValue?.label) {
|
|
143
|
+
filters.push({ id: 'event', label: t('event'), value: eventValue.label });
|
|
144
|
+
}
|
|
145
|
+
if (status && statusValue?.label) {
|
|
146
|
+
filters.push({ id: 'status', label: t('status'), value: statusValue.label });
|
|
147
|
+
}
|
|
148
|
+
if (datePreset && dateValue?.label) {
|
|
149
|
+
let dateLabel = dateValue.label;
|
|
150
|
+
if (datePreset === 'custom') {
|
|
151
|
+
const fromLabel = customFrom ? dayjs(customFrom).format('DD MMM YYYY') : t('from');
|
|
152
|
+
const toLabel = customTo ? dayjs(customTo).format('DD MMM YYYY') : t('to');
|
|
153
|
+
dateLabel = `${fromLabel} - ${toLabel}`;
|
|
154
|
+
}
|
|
155
|
+
filters.push({ id: 'dateRange', label: t('dateRange'), value: dateLabel });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return filters;
|
|
159
|
+
}, [eventType, eventValue, status, statusValue, datePreset, dateValue, customFrom, customTo, t]);
|
|
160
|
+
|
|
161
|
+
const handleEventChange = (opt: SelectOption | null) => {
|
|
162
|
+
setEventType((opt ? String(opt.value) : '') as ReceiptEventType | '');
|
|
163
|
+
setCurrentPage(1);
|
|
164
|
+
};
|
|
165
|
+
const handleStatusChange = (opt: SelectOption | null) => {
|
|
166
|
+
setStatus((opt ? String(opt.value) : '') as ReceiptPurposeStatus | '');
|
|
167
|
+
setCurrentPage(1);
|
|
168
|
+
};
|
|
169
|
+
const handleDateChange = (opt: SelectOption | null) => {
|
|
170
|
+
const nextDatePreset = opt ? String(opt.value) : '';
|
|
171
|
+
setDatePreset(nextDatePreset);
|
|
172
|
+
if (nextDatePreset !== 'custom') {
|
|
173
|
+
setCustomFrom('');
|
|
174
|
+
setCustomTo('');
|
|
175
|
+
}
|
|
176
|
+
setCurrentPage(1);
|
|
177
|
+
};
|
|
178
|
+
const handleClearFilters = () => {
|
|
179
|
+
setEventType('');
|
|
180
|
+
setStatus('');
|
|
181
|
+
setDatePreset('');
|
|
182
|
+
setCustomFrom('');
|
|
183
|
+
setCustomTo('');
|
|
184
|
+
setCurrentPage(1);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const handleGetReceipts = useCallback(
|
|
188
|
+
async (page: number = currentPage, size: number = pageSize) => {
|
|
189
|
+
if (!organizationId || !workspaceId) return;
|
|
190
|
+
if (isCustomRangeInvalid) return;
|
|
191
|
+
setIsLoading(true);
|
|
192
|
+
try {
|
|
193
|
+
const skip = (page - 1) * size;
|
|
194
|
+
const { created_after, created_before } = dateRangeParams(datePreset, customFrom, customTo);
|
|
195
|
+
const response = await fetchReceipts(
|
|
196
|
+
organizationId,
|
|
197
|
+
workspaceId,
|
|
198
|
+
{
|
|
199
|
+
skip,
|
|
200
|
+
limit: size,
|
|
201
|
+
event_type: eventType || undefined,
|
|
202
|
+
status: status || undefined,
|
|
203
|
+
created_after,
|
|
204
|
+
created_before,
|
|
205
|
+
language: langParam,
|
|
206
|
+
},
|
|
207
|
+
authContext,
|
|
208
|
+
consentClient
|
|
209
|
+
);
|
|
210
|
+
const { rows, total } = normalizeReceipts(response);
|
|
211
|
+
setReceipts(rows);
|
|
212
|
+
setTotalCount(total);
|
|
213
|
+
if (page !== currentPage) setCurrentPage(page);
|
|
214
|
+
if (size !== pageSize) setPageSize(size);
|
|
215
|
+
} catch (error) {
|
|
216
|
+
console.error('[My Receipt] failed to fetch receipts:', error);
|
|
217
|
+
setReceipts([]);
|
|
218
|
+
setTotalCount(0);
|
|
219
|
+
setPreviousData([]);
|
|
220
|
+
setPreviousTotalCount(0);
|
|
221
|
+
toast.error(t('failedToFetchReceipts'));
|
|
222
|
+
} finally {
|
|
223
|
+
setIsLoading(false);
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
[
|
|
227
|
+
organizationId,
|
|
228
|
+
workspaceId,
|
|
229
|
+
currentPage,
|
|
230
|
+
pageSize,
|
|
231
|
+
eventType,
|
|
232
|
+
status,
|
|
233
|
+
datePreset,
|
|
234
|
+
customFrom,
|
|
235
|
+
customTo,
|
|
236
|
+
isCustomRangeInvalid,
|
|
237
|
+
authContext,
|
|
238
|
+
consentClient,
|
|
239
|
+
langParam,
|
|
240
|
+
t,
|
|
241
|
+
]
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
const handleDownload = useCallback(
|
|
245
|
+
async (receipt: Receipt) => {
|
|
246
|
+
if (!organizationId || !workspaceId || !receipt.uuid) return;
|
|
247
|
+
setDownloadingUuid(receipt.uuid);
|
|
248
|
+
try {
|
|
249
|
+
const blob = await fetchReceiptPdf(
|
|
250
|
+
organizationId,
|
|
251
|
+
workspaceId,
|
|
252
|
+
receipt.uuid,
|
|
253
|
+
authContext,
|
|
254
|
+
consentClient
|
|
255
|
+
);
|
|
256
|
+
if (!blob) throw new Error('No PDF returned');
|
|
257
|
+
const url = URL.createObjectURL(blob);
|
|
258
|
+
const link = document.createElement('a');
|
|
259
|
+
link.href = url;
|
|
260
|
+
link.download = `receipt-${receipt.uuid}.pdf`;
|
|
261
|
+
document.body.appendChild(link);
|
|
262
|
+
link.click();
|
|
263
|
+
link.remove();
|
|
264
|
+
URL.revokeObjectURL(url);
|
|
265
|
+
} catch (error) {
|
|
266
|
+
console.error('[My Receipt] failed to download receipt PDF:', error);
|
|
267
|
+
toast.error(t('failedToDownloadReceipt'));
|
|
268
|
+
} finally {
|
|
269
|
+
setDownloadingUuid(null);
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
[organizationId, workspaceId, authContext, consentClient, t]
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
const columns = useReceiptColumns({ onDownload: handleDownload, downloadingUuid });
|
|
276
|
+
|
|
277
|
+
// Preserve previous data when a new page starts loading.
|
|
278
|
+
useEffect(() => {
|
|
279
|
+
if (receipts.length > 0) {
|
|
280
|
+
setPreviousData(receipts);
|
|
281
|
+
setPreviousTotalCount(totalCount);
|
|
282
|
+
}
|
|
283
|
+
}, [receipts, totalCount]);
|
|
284
|
+
|
|
285
|
+
useEffect(() => {
|
|
286
|
+
void handleGetReceipts();
|
|
287
|
+
}, [organizationId, workspaceId, handleGetReceipts]);
|
|
288
|
+
|
|
289
|
+
const displayData = isLoading && receipts.length === 0 ? previousData : receipts;
|
|
290
|
+
const displayTotalCount = isLoading && totalCount === 0 ? previousTotalCount : totalCount;
|
|
291
|
+
|
|
292
|
+
const table = useReactTable<Receipt>({
|
|
293
|
+
data: displayData,
|
|
294
|
+
columns,
|
|
295
|
+
getCoreRowModel: getCoreRowModel(),
|
|
296
|
+
manualPagination: true,
|
|
297
|
+
pageCount: Math.ceil(displayTotalCount / pageSize) || 0,
|
|
298
|
+
state: {
|
|
299
|
+
pagination: {
|
|
300
|
+
pageIndex: currentPage - 1,
|
|
301
|
+
pageSize,
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
onPaginationChange: (updater) => {
|
|
305
|
+
const next =
|
|
306
|
+
typeof updater === 'function'
|
|
307
|
+
? updater({ pageIndex: currentPage - 1, pageSize })
|
|
308
|
+
: updater;
|
|
309
|
+
const nextPage = (next as { pageIndex: number }).pageIndex + 1;
|
|
310
|
+
const nextSize = (next as { pageSize: number }).pageSize;
|
|
311
|
+
if (nextPage === currentPage && nextSize === pageSize) return;
|
|
312
|
+
setCurrentPage(nextPage);
|
|
313
|
+
setPageSize(nextSize);
|
|
314
|
+
void handleGetReceipts(nextPage, nextSize);
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
return (
|
|
319
|
+
<div
|
|
320
|
+
style={{
|
|
321
|
+
width: '100%',
|
|
322
|
+
display: 'flex',
|
|
323
|
+
flexDirection: 'column',
|
|
324
|
+
gap: getVar('--pc-spacing-md'),
|
|
325
|
+
}}
|
|
326
|
+
>
|
|
327
|
+
{/* Filters */}
|
|
328
|
+
<section className="pc-receipts-filters-card">
|
|
329
|
+
<div className="pc-receipts-filters-header">
|
|
330
|
+
<div style={{ minWidth: 0 }}>
|
|
331
|
+
<PCText
|
|
332
|
+
as="p"
|
|
333
|
+
size="sm"
|
|
334
|
+
weight="semibold"
|
|
335
|
+
style={{
|
|
336
|
+
margin: 0,
|
|
337
|
+
display: 'inline-flex',
|
|
338
|
+
alignItems: 'center',
|
|
339
|
+
gap: '0.375rem',
|
|
340
|
+
}}
|
|
341
|
+
>
|
|
342
|
+
<Filter size={13} strokeWidth={1.9} />
|
|
343
|
+
{t('filterReceipts')}
|
|
344
|
+
</PCText>
|
|
345
|
+
<PCText as="p" size="xs" color="secondary" style={{ margin: '0.1875rem 0 0' }}>
|
|
346
|
+
{t('receiptFiltersHint')}
|
|
347
|
+
</PCText>
|
|
348
|
+
</div>
|
|
349
|
+
<PCButton
|
|
350
|
+
size="sm"
|
|
351
|
+
variant="text"
|
|
352
|
+
disabled={!hasActiveFilters}
|
|
353
|
+
onClick={handleClearFilters}
|
|
354
|
+
style={{ whiteSpace: 'nowrap' }}
|
|
355
|
+
>
|
|
356
|
+
{t('clearFilters')}
|
|
357
|
+
</PCButton>
|
|
358
|
+
</div>
|
|
359
|
+
|
|
360
|
+
<div className="pc-receipts-filters-grid">
|
|
361
|
+
<PCSelect
|
|
362
|
+
className="pc-filter-select pc-receipts-filter-control"
|
|
363
|
+
size="sm"
|
|
364
|
+
label={t('event')}
|
|
365
|
+
placeholder={t('all')}
|
|
366
|
+
options={eventTypeOptions}
|
|
367
|
+
value={eventValue}
|
|
368
|
+
onChange={handleEventChange}
|
|
369
|
+
onClear={() => {
|
|
370
|
+
setEventType('');
|
|
371
|
+
setCurrentPage(1);
|
|
372
|
+
}}
|
|
373
|
+
clearable={Boolean(eventType)}
|
|
374
|
+
showSelectedCheck
|
|
375
|
+
dropdownMatchTriggerWidth
|
|
376
|
+
style={{ width: '100%' }}
|
|
377
|
+
/>
|
|
378
|
+
<PCSelect
|
|
379
|
+
className="pc-filter-select pc-receipts-filter-control"
|
|
380
|
+
size="sm"
|
|
381
|
+
label={t('status')}
|
|
382
|
+
placeholder={t('all')}
|
|
383
|
+
options={statusOptions}
|
|
384
|
+
value={statusValue}
|
|
385
|
+
onChange={handleStatusChange}
|
|
386
|
+
onClear={() => {
|
|
387
|
+
setStatus('');
|
|
388
|
+
setCurrentPage(1);
|
|
389
|
+
}}
|
|
390
|
+
clearable={Boolean(status)}
|
|
391
|
+
showSelectedCheck
|
|
392
|
+
dropdownMatchTriggerWidth
|
|
393
|
+
style={{ width: '100%' }}
|
|
394
|
+
/>
|
|
395
|
+
<PCSelect
|
|
396
|
+
className="pc-filter-select pc-receipts-filter-control"
|
|
397
|
+
size="sm"
|
|
398
|
+
label={t('dateRange')}
|
|
399
|
+
placeholder={t('allTime')}
|
|
400
|
+
prefix={<CalendarDays size={12} strokeWidth={1.8} />}
|
|
401
|
+
options={dateOptions}
|
|
402
|
+
value={dateValue}
|
|
403
|
+
onChange={handleDateChange}
|
|
404
|
+
onClear={() => {
|
|
405
|
+
setDatePreset('');
|
|
406
|
+
setCustomFrom('');
|
|
407
|
+
setCustomTo('');
|
|
408
|
+
setCurrentPage(1);
|
|
409
|
+
}}
|
|
410
|
+
clearable={Boolean(datePreset)}
|
|
411
|
+
showSelectedCheck
|
|
412
|
+
dropdownMatchTriggerWidth
|
|
413
|
+
style={{ width: '100%' }}
|
|
414
|
+
/>
|
|
415
|
+
</div>
|
|
416
|
+
|
|
417
|
+
{datePreset === 'custom' && (
|
|
418
|
+
<div className="pc-receipt-date-range">
|
|
419
|
+
<div className="pc-receipt-date-field">
|
|
420
|
+
<PCInput
|
|
421
|
+
type="date"
|
|
422
|
+
size="sm"
|
|
423
|
+
label={t('from')}
|
|
424
|
+
aria-label={t('from')}
|
|
425
|
+
value={customFrom}
|
|
426
|
+
error={isCustomRangeInvalid}
|
|
427
|
+
max={customTo || undefined}
|
|
428
|
+
onChange={(e) => {
|
|
429
|
+
setCustomFrom(e.target.value);
|
|
430
|
+
setCurrentPage(1);
|
|
431
|
+
}}
|
|
432
|
+
/>
|
|
433
|
+
</div>
|
|
434
|
+
<div className="pc-receipt-date-field">
|
|
435
|
+
<PCInput
|
|
436
|
+
type="date"
|
|
437
|
+
size="sm"
|
|
438
|
+
label={t('to')}
|
|
439
|
+
aria-label={t('to')}
|
|
440
|
+
value={customTo}
|
|
441
|
+
error={isCustomRangeInvalid}
|
|
442
|
+
min={customFrom || undefined}
|
|
443
|
+
onChange={(e) => {
|
|
444
|
+
setCustomTo(e.target.value);
|
|
445
|
+
setCurrentPage(1);
|
|
446
|
+
}}
|
|
447
|
+
/>
|
|
448
|
+
</div>
|
|
449
|
+
</div>
|
|
450
|
+
)}
|
|
451
|
+
|
|
452
|
+
{datePreset === 'custom' && isCustomRangeInvalid && (
|
|
453
|
+
<PCText as="p" size="xs" className="pc-receipt-custom-hint">
|
|
454
|
+
{t('receiptDateRangeInvalid')}
|
|
455
|
+
</PCText>
|
|
456
|
+
)}
|
|
457
|
+
|
|
458
|
+
<div className="pc-receipts-summary-row">
|
|
459
|
+
<PCText as="p" size="xs" color="secondary" style={{ margin: 0 }}>
|
|
460
|
+
{t('totalRecords', { count: displayTotalCount })}
|
|
461
|
+
</PCText>
|
|
462
|
+
{activeFilterLabels.length > 0 && (
|
|
463
|
+
<div className="pc-receipts-active-chips" aria-label={t('filtersApplied')}>
|
|
464
|
+
{activeFilterLabels.map((item) => (
|
|
465
|
+
<span key={item.id} className="pc-receipts-filter-chip">
|
|
466
|
+
<span className="pc-receipts-filter-chip-label">{item.label}:</span>
|
|
467
|
+
<span>{item.value}</span>
|
|
468
|
+
</span>
|
|
469
|
+
))}
|
|
470
|
+
</div>
|
|
471
|
+
)}
|
|
472
|
+
</div>
|
|
473
|
+
</section>
|
|
474
|
+
|
|
475
|
+
<AdvancedTable table={table} isLoading={isLoading && displayData.length === 0} />
|
|
476
|
+
|
|
477
|
+
<div
|
|
478
|
+
style={{
|
|
479
|
+
...pcStyles.flex,
|
|
480
|
+
...pcStyles.flexCol,
|
|
481
|
+
...pcStyles.itemsCenter,
|
|
482
|
+
...pcStyles.justifyBetween,
|
|
483
|
+
...pcStyles.gapSm,
|
|
484
|
+
marginTop: '0.25rem',
|
|
485
|
+
}}
|
|
486
|
+
className="pc-flex-responsive"
|
|
487
|
+
>
|
|
488
|
+
<AdvancedTablePagination table={table} />
|
|
489
|
+
</div>
|
|
490
|
+
</div>
|
|
491
|
+
);
|
|
492
|
+
}
|
|
@@ -9,14 +9,25 @@ import { Mail } from 'lucide-react';
|
|
|
9
9
|
|
|
10
10
|
const getVar = (varName: string): string => `var(${varName})`;
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Success surface shown after a case is submitted. Always renders a
|
|
14
|
+
* single Case-ID card — the form's 1-form -> 1-case contract means
|
|
15
|
+
* there is no fan-out and no partial-failure surface to maintain.
|
|
16
|
+
*
|
|
17
|
+
* The product label above the case ID is rendered only when the
|
|
18
|
+
* result carries one (product-scoped request types: ACCESS / ERASURE
|
|
19
|
+
* / CORRECTION). Nomination and grievance leave the field empty and
|
|
20
|
+
* the label is omitted.
|
|
21
|
+
*/
|
|
12
22
|
export default function RequestSubmit({
|
|
13
|
-
|
|
23
|
+
result,
|
|
14
24
|
contact,
|
|
15
25
|
onCreateRequest,
|
|
16
26
|
revokeConsentOnFulfilment,
|
|
17
27
|
revokedPurposeNames,
|
|
18
28
|
}: RequestSubmitProps) {
|
|
19
29
|
const { t } = useTranslation();
|
|
30
|
+
|
|
20
31
|
return (
|
|
21
32
|
<div style={{ marginTop: pcStyles.mtMd.marginTop, position: 'relative' }}>
|
|
22
33
|
<img
|
|
@@ -33,37 +44,47 @@ export default function RequestSubmit({
|
|
|
33
44
|
filter: 'brightness(0) saturate(100%) invert(var(--pc-icon-filter, 0))',
|
|
34
45
|
}}
|
|
35
46
|
/>
|
|
36
|
-
<div
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}} className="pc-flex-responsive">
|
|
50
|
-
<div style={{
|
|
47
|
+
<div
|
|
48
|
+
style={{
|
|
49
|
+
...pcStyles.card,
|
|
50
|
+
borderRadius: pcStyles.borderRadiusXl,
|
|
51
|
+
padding: pcStyles.pLg.padding,
|
|
52
|
+
position: 'relative',
|
|
53
|
+
...pcStyles.bgPrimary,
|
|
54
|
+
border: `1px solid ${pcStyles.borderColor}`,
|
|
55
|
+
}}
|
|
56
|
+
className="pc-padding-responsive"
|
|
57
|
+
>
|
|
58
|
+
<div
|
|
59
|
+
style={{
|
|
51
60
|
...pcStyles.flex,
|
|
61
|
+
...pcStyles.flexCol,
|
|
52
62
|
...pcStyles.itemsStart,
|
|
53
63
|
...pcStyles.gapMd,
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
}}
|
|
65
|
+
className="pc-flex-responsive"
|
|
66
|
+
>
|
|
67
|
+
<div
|
|
68
|
+
style={{
|
|
69
|
+
...pcStyles.flex,
|
|
70
|
+
...pcStyles.itemsStart,
|
|
71
|
+
...pcStyles.gapMd,
|
|
72
|
+
width: '100%',
|
|
73
|
+
}}
|
|
74
|
+
>
|
|
75
|
+
<div
|
|
76
|
+
style={{
|
|
77
|
+
height: '2.5rem',
|
|
78
|
+
width: '2.5rem',
|
|
79
|
+
flexShrink: 0,
|
|
80
|
+
display: 'flex',
|
|
81
|
+
alignItems: 'center',
|
|
82
|
+
justifyContent: 'center',
|
|
83
|
+
borderRadius: '50%',
|
|
84
|
+
backgroundColor: `${getVar('--pc-success')}33`,
|
|
85
|
+
padding: '0.375rem',
|
|
86
|
+
}}
|
|
87
|
+
>
|
|
67
88
|
<img
|
|
68
89
|
src={greenTick}
|
|
69
90
|
alt="success"
|
|
@@ -94,13 +115,27 @@ export default function RequestSubmit({
|
|
|
94
115
|
>
|
|
95
116
|
{t('thankYouForSubmittingYourRequest')}.
|
|
96
117
|
</PCText>
|
|
97
|
-
<div
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
118
|
+
<div
|
|
119
|
+
style={{
|
|
120
|
+
marginTop: pcStyles.mtMd.marginTop,
|
|
121
|
+
padding: pcStyles.pSm.padding,
|
|
122
|
+
...pcStyles.bgSecondary,
|
|
123
|
+
borderRadius: pcStyles.borderRadiusMd,
|
|
124
|
+
border: `1px solid ${pcStyles.borderColor}`,
|
|
125
|
+
}}
|
|
126
|
+
>
|
|
127
|
+
{result.product_name !== '' && (
|
|
128
|
+
<PCText
|
|
129
|
+
size="sm"
|
|
130
|
+
weight="medium"
|
|
131
|
+
style={{
|
|
132
|
+
marginBottom: pcStyles.mbXs.marginBottom,
|
|
133
|
+
wordBreak: 'break-word',
|
|
134
|
+
}}
|
|
135
|
+
>
|
|
136
|
+
{result.product_name}
|
|
137
|
+
</PCText>
|
|
138
|
+
)}
|
|
104
139
|
<PCText
|
|
105
140
|
size="base"
|
|
106
141
|
weight="bold"
|
|
@@ -111,25 +146,23 @@ export default function RequestSubmit({
|
|
|
111
146
|
}}
|
|
112
147
|
className="pc-text-responsive"
|
|
113
148
|
>
|
|
114
|
-
{t('caseID')}: {
|
|
149
|
+
{t('caseID')}: {result.case_id}
|
|
115
150
|
</PCText>
|
|
116
|
-
<PCText
|
|
117
|
-
size="xs"
|
|
118
|
-
weight="medium"
|
|
119
|
-
color="secondary"
|
|
120
|
-
>
|
|
151
|
+
<PCText size="xs" weight="medium" color="secondary">
|
|
121
152
|
{t('saveThisID')}.
|
|
122
153
|
</PCText>
|
|
123
154
|
</div>
|
|
124
|
-
<div
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
155
|
+
<div
|
|
156
|
+
style={{
|
|
157
|
+
...pcStyles.flex,
|
|
158
|
+
...pcStyles.itemsStart,
|
|
159
|
+
...pcStyles.gapSm,
|
|
160
|
+
marginTop: pcStyles.mtMd.marginTop,
|
|
161
|
+
padding: '0.5rem 0.625rem',
|
|
162
|
+
...pcStyles.bgSecondary,
|
|
163
|
+
borderRadius: pcStyles.borderRadiusMd,
|
|
164
|
+
}}
|
|
165
|
+
>
|
|
133
166
|
<Mail
|
|
134
167
|
size={16}
|
|
135
168
|
style={{
|
|
@@ -187,11 +220,13 @@ export default function RequestSubmit({
|
|
|
187
220
|
opacity: 0.9,
|
|
188
221
|
}}
|
|
189
222
|
/>
|
|
190
|
-
<div
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
223
|
+
<div
|
|
224
|
+
style={{
|
|
225
|
+
marginTop: pcStyles.mtMd.marginTop,
|
|
226
|
+
...pcStyles.flex,
|
|
227
|
+
...pcStyles.justifyCenter,
|
|
228
|
+
}}
|
|
229
|
+
>
|
|
195
230
|
<PCButton
|
|
196
231
|
variant="primary"
|
|
197
232
|
size="sm"
|
|
@@ -203,7 +238,10 @@ export default function RequestSubmit({
|
|
|
203
238
|
</div>
|
|
204
239
|
</div>
|
|
205
240
|
</div>
|
|
206
|
-
<div
|
|
241
|
+
<div
|
|
242
|
+
className="pc-block-xs pc-hidden-md"
|
|
243
|
+
style={{ marginTop: pcStyles.mtMd.marginTop }}
|
|
244
|
+
>
|
|
207
245
|
<div style={{ ...pcStyles.flex, ...pcStyles.justifyCenter }}>
|
|
208
246
|
<PCButton
|
|
209
247
|
variant="primary"
|