@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.
Files changed (51) hide show
  1. package/.turbo/turbo-build.log +15 -10
  2. package/CHANGELOG.md +12 -0
  3. package/dist/privacy-center.d.mts +51 -1
  4. package/dist/privacy-center.d.ts +51 -1
  5. package/dist/privacy-center.js +3463 -1148
  6. package/dist/privacy-center.mjs +3399 -1084
  7. package/package.json +1 -1
  8. package/src/RedactoPrivacyCenter/PrivacyCenterHomePage.tsx +3 -0
  9. package/src/RedactoPrivacyCenter/PrivacyCenterMyReceiptPage.tsx +26 -0
  10. package/src/RedactoPrivacyCenter/api/actions.ts +66 -0
  11. package/src/RedactoPrivacyCenter/api/client.ts +79 -0
  12. package/src/RedactoPrivacyCenter/components/DataRequest/ProductSelectionSection.tsx +222 -0
  13. package/src/RedactoPrivacyCenter/components/Form.tsx +402 -259
  14. package/src/RedactoPrivacyCenter/components/ReceiptsList.tsx +492 -0
  15. package/src/RedactoPrivacyCenter/components/RequestSubmit.tsx +95 -57
  16. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/constants.tsx +6 -1
  17. package/src/RedactoPrivacyCenter/components/groupHelpers.ts +56 -0
  18. package/src/RedactoPrivacyCenter/components/receiptColumns.tsx +203 -0
  19. package/src/RedactoPrivacyCenter/components/types.ts +20 -2
  20. package/src/RedactoPrivacyCenter/context/NavigationContext.tsx +2 -2
  21. package/src/RedactoPrivacyCenter/lib/api.ts +67 -8
  22. package/src/RedactoPrivacyCenter/lib/constants.ts +5 -0
  23. package/src/RedactoPrivacyCenter/lib/types.ts +74 -0
  24. package/src/RedactoPrivacyCenter/locales/as/translation.json +39 -1
  25. package/src/RedactoPrivacyCenter/locales/bho/translation.json +39 -1
  26. package/src/RedactoPrivacyCenter/locales/bn/translation.json +39 -1
  27. package/src/RedactoPrivacyCenter/locales/brx/translation.json +39 -1
  28. package/src/RedactoPrivacyCenter/locales/doi/translation.json +39 -1
  29. package/src/RedactoPrivacyCenter/locales/en/translation.json +44 -1
  30. package/src/RedactoPrivacyCenter/locales/gom/translation.json +39 -1
  31. package/src/RedactoPrivacyCenter/locales/gu/translation.json +39 -1
  32. package/src/RedactoPrivacyCenter/locales/hi/translation.json +39 -1
  33. package/src/RedactoPrivacyCenter/locales/kn/translation.json +39 -1
  34. package/src/RedactoPrivacyCenter/locales/ks/translation.json +39 -1
  35. package/src/RedactoPrivacyCenter/locales/mai/translation.json +39 -1
  36. package/src/RedactoPrivacyCenter/locales/ml/translation.json +39 -1
  37. package/src/RedactoPrivacyCenter/locales/mni-Mtei/translation.json +39 -1
  38. package/src/RedactoPrivacyCenter/locales/mr/translation.json +39 -1
  39. package/src/RedactoPrivacyCenter/locales/ne/translation.json +39 -1
  40. package/src/RedactoPrivacyCenter/locales/or/translation.json +39 -1
  41. package/src/RedactoPrivacyCenter/locales/pa/translation.json +39 -1
  42. package/src/RedactoPrivacyCenter/locales/sa/translation.json +39 -1
  43. package/src/RedactoPrivacyCenter/locales/sat/translation.json +39 -1
  44. package/src/RedactoPrivacyCenter/locales/sd/translation.json +39 -1
  45. package/src/RedactoPrivacyCenter/locales/ta/translation.json +39 -1
  46. package/src/RedactoPrivacyCenter/locales/te/translation.json +39 -1
  47. package/src/RedactoPrivacyCenter/locales/ur/translation.json +39 -1
  48. package/src/RedactoPrivacyCenter/styles/injectStyles.ts +301 -0
  49. package/tests/Form.test.tsx +1144 -0
  50. package/tests/ProductSelectionSection.test.tsx +328 -0
  51. 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
- caseId,
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 style={{
37
- ...pcStyles.card,
38
- borderRadius: pcStyles.borderRadiusXl,
39
- padding: pcStyles.pLg.padding,
40
- position: 'relative',
41
- ...pcStyles.bgPrimary,
42
- border: `1px solid ${pcStyles.borderColor}`,
43
- }} className="pc-padding-responsive">
44
- <div style={{
45
- ...pcStyles.flex,
46
- ...pcStyles.flexCol,
47
- ...pcStyles.itemsStart,
48
- ...pcStyles.gapMd,
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
- width: '100%',
55
- }}>
56
- <div style={{
57
- height: '2.5rem',
58
- width: '2.5rem',
59
- flexShrink: 0,
60
- display: 'flex',
61
- alignItems: 'center',
62
- justifyContent: 'center',
63
- borderRadius: '50%',
64
- backgroundColor: `${getVar('--pc-success')}33`,
65
- padding: '0.375rem',
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 style={{
98
- marginTop: pcStyles.mtMd.marginTop,
99
- padding: pcStyles.pSm.padding,
100
- ...pcStyles.bgSecondary,
101
- borderRadius: pcStyles.borderRadiusMd,
102
- border: `1px solid ${pcStyles.borderColor}`,
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')}: {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 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
- }}>
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 style={{
191
- marginTop: pcStyles.mtMd.marginTop,
192
- ...pcStyles.flex,
193
- ...pcStyles.justifyCenter
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 className="pc-block-xs pc-hidden-md" style={{ marginTop: pcStyles.mtMd.marginTop }}>
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"