@redacto.io/consent-sdk-react 9.2.0 → 9.3.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +25 -26
- package/.turbo/turbo-test.log +203 -0
- package/.turbo/turbo-verify-pack.log +6 -0
- package/CHANGELOG.md +12 -0
- package/dist/privacy-center.js +3242 -2956
- package/dist/privacy-center.mjs +3068 -2774
- package/package.json +3 -3
- package/src/RedactoPrivacyCenter/PrivacyCenterLayout.tsx +6 -3
- package/src/RedactoPrivacyCenter/components/AppShell/AppShell.tsx +36 -5
- package/src/RedactoPrivacyCenter/components/AppShell/TopAppBar.tsx +3 -2
- package/src/RedactoPrivacyCenter/components/Form.tsx +127 -8
- package/src/RedactoPrivacyCenter/components/PrivacyCenterEmptyState.tsx +76 -0
- package/src/RedactoPrivacyCenter/components/ReceiptsList.tsx +3 -41
- package/src/RedactoPrivacyCenter/context/DataAvailabilityContext.tsx +169 -0
- package/src/RedactoPrivacyCenter/locales/as/translation.json +2 -0
- package/src/RedactoPrivacyCenter/locales/bho/translation.json +2 -0
- package/src/RedactoPrivacyCenter/locales/bn/translation.json +2 -0
- package/src/RedactoPrivacyCenter/locales/brx/translation.json +2 -0
- package/src/RedactoPrivacyCenter/locales/doi/translation.json +2 -0
- package/src/RedactoPrivacyCenter/locales/en/translation.json +2 -0
- package/src/RedactoPrivacyCenter/locales/gom/translation.json +2 -0
- package/src/RedactoPrivacyCenter/locales/gu/translation.json +2 -0
- package/src/RedactoPrivacyCenter/locales/hi/translation.json +2 -0
- package/src/RedactoPrivacyCenter/locales/kn/translation.json +2 -0
- package/src/RedactoPrivacyCenter/locales/ks/translation.json +2 -0
- package/src/RedactoPrivacyCenter/locales/mai/translation.json +2 -0
- package/src/RedactoPrivacyCenter/locales/ml/translation.json +2 -0
- package/src/RedactoPrivacyCenter/locales/mni-Mtei/translation.json +2 -0
- package/src/RedactoPrivacyCenter/locales/mr/translation.json +2 -0
- package/src/RedactoPrivacyCenter/locales/ne/translation.json +2 -0
- package/src/RedactoPrivacyCenter/locales/or/translation.json +2 -0
- package/src/RedactoPrivacyCenter/locales/pa/translation.json +2 -0
- package/src/RedactoPrivacyCenter/locales/sa/translation.json +2 -0
- package/src/RedactoPrivacyCenter/locales/sat/translation.json +2 -0
- package/src/RedactoPrivacyCenter/locales/sd/translation.json +2 -0
- package/src/RedactoPrivacyCenter/locales/ta/translation.json +2 -0
- package/src/RedactoPrivacyCenter/locales/te/translation.json +2 -0
- package/src/RedactoPrivacyCenter/locales/ur/translation.json +2 -0
- package/src/RedactoPrivacyCenter/styles/injectStyles.ts +1 -1
- package/tests/DataAvailabilityContext.test.tsx +123 -0
- package/tests/Form.test.tsx +93 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@redacto.io/consent-sdk-react",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.3.0-beta.1",
|
|
4
4
|
"main": "dist/index.cjs",
|
|
5
5
|
"module": "dist/index.mjs",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -32,11 +32,11 @@
|
|
|
32
32
|
"@types/lodash": "^4.17.0",
|
|
33
33
|
"@types/react": "^19.1.3",
|
|
34
34
|
"@types/react-dom": "^19.1.3",
|
|
35
|
-
"@vitest/coverage-v8": "
|
|
35
|
+
"@vitest/coverage-v8": "^4.1.8",
|
|
36
36
|
"jsdom": "^26.1.0",
|
|
37
37
|
"tsup": "^8.4.0",
|
|
38
38
|
"typescript": "5.8.2",
|
|
39
|
-
"vitest": "^
|
|
39
|
+
"vitest": "^4.1.8"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
42
|
"react": "^16.8.0 || ^17 || ^18 || ^19",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useEffect, useState, useRef, type ReactNode } from 'react';
|
|
2
2
|
import { OrganizationProvider } from './context/OrganizationContext';
|
|
3
|
+
import { DataAvailabilityProvider } from './context/DataAvailabilityContext';
|
|
3
4
|
import type { OrganizationDetail } from './lib/types';
|
|
4
5
|
import {
|
|
5
6
|
checkAndRefreshAuth,
|
|
@@ -161,9 +162,11 @@ export function PrivacyCenterLayout({ children, slug, onBack }: PrivacyCenterLay
|
|
|
161
162
|
|
|
162
163
|
return (
|
|
163
164
|
<OrganizationProvider organization={organization}>
|
|
164
|
-
<
|
|
165
|
-
{
|
|
166
|
-
|
|
165
|
+
<DataAvailabilityProvider>
|
|
166
|
+
<AppShell onBack={onBack}>
|
|
167
|
+
{children}
|
|
168
|
+
</AppShell>
|
|
169
|
+
</DataAvailabilityProvider>
|
|
167
170
|
</OrganizationProvider>
|
|
168
171
|
);
|
|
169
172
|
}
|
|
@@ -19,6 +19,9 @@ import { TopAppBar } from './TopAppBar';
|
|
|
19
19
|
import { SidebarNav } from './SidebarNav';
|
|
20
20
|
import { MobileNavDrawer } from './MobileNavDrawer';
|
|
21
21
|
import { PageHeader } from './PageHeader';
|
|
22
|
+
import { PCLoader } from '../../ui';
|
|
23
|
+
import { useDataAvailability } from '../../context/DataAvailabilityContext';
|
|
24
|
+
import { PrivacyCenterEmptyState } from '../PrivacyCenterEmptyState';
|
|
22
25
|
import type { PrivacyCenterLayoutProps } from '../types';
|
|
23
26
|
|
|
24
27
|
export default function AppShell({
|
|
@@ -35,6 +38,13 @@ export default function AppShell({
|
|
|
35
38
|
|
|
36
39
|
const isMobile = useMediaQuery('(max-width: 639px)');
|
|
37
40
|
|
|
41
|
+
// Keep the shell minimal — top bar + a content spinner — until we've CONFIRMED
|
|
42
|
+
// the user has data. Hiding the sidebar/tabs/bell during 'checking' (not just
|
|
43
|
+
// 'empty') stops the nav from flashing in on refresh only to disappear when
|
|
44
|
+
// the API resolves to "no data". The branded top bar stays visible throughout.
|
|
45
|
+
const { status } = useDataAvailability();
|
|
46
|
+
const hideNav = status !== 'has-data';
|
|
47
|
+
|
|
38
48
|
// Close mobile drawer when crossing the breakpoint
|
|
39
49
|
useEffect(() => {
|
|
40
50
|
if (!isMobile && isMobileNavOpen) setMobileNavOpen(false);
|
|
@@ -110,14 +120,15 @@ export default function AppShell({
|
|
|
110
120
|
<main className="pc-root pc-shell-root" style={rootStyle}>
|
|
111
121
|
<TopAppBar
|
|
112
122
|
ref={hamburgerRef}
|
|
113
|
-
showHamburger={isMobile}
|
|
123
|
+
showHamburger={isMobile && !hideNav}
|
|
114
124
|
onOpenMobileNav={() => setMobileNavOpen(true)}
|
|
115
125
|
locale={locale}
|
|
116
126
|
onLocaleChange={setLocale}
|
|
117
127
|
onBack={onBack}
|
|
128
|
+
showNotifications={!hideNav}
|
|
118
129
|
/>
|
|
119
130
|
<div className="pc-shell-body" style={bodyStyle}>
|
|
120
|
-
{!isMobile && (
|
|
131
|
+
{!isMobile && !hideNav && (
|
|
121
132
|
<SidebarNav
|
|
122
133
|
className={sidebarClassName}
|
|
123
134
|
isExpanded={sidebar.isExpanded}
|
|
@@ -126,8 +137,28 @@ export default function AppShell({
|
|
|
126
137
|
)}
|
|
127
138
|
<section className="pc-shell-content" style={mainStyle}>
|
|
128
139
|
<div className={contentClassName} style={contentWrapperStyle}>
|
|
129
|
-
|
|
130
|
-
|
|
140
|
+
{status === 'checking' ? (
|
|
141
|
+
<div
|
|
142
|
+
style={{
|
|
143
|
+
...pcStyles.flex,
|
|
144
|
+
...pcStyles.itemsCenter,
|
|
145
|
+
...pcStyles.justifyCenter,
|
|
146
|
+
flex: 1,
|
|
147
|
+
minHeight: 240,
|
|
148
|
+
}}
|
|
149
|
+
>
|
|
150
|
+
<PCLoader variant="spinner" color="info" size="lg" />
|
|
151
|
+
</div>
|
|
152
|
+
) : status === 'empty' ? (
|
|
153
|
+
<div style={{ flex: 1, minWidth: 0, width: '100%' }}>
|
|
154
|
+
<PrivacyCenterEmptyState />
|
|
155
|
+
</div>
|
|
156
|
+
) : (
|
|
157
|
+
<>
|
|
158
|
+
<PageHeader />
|
|
159
|
+
<div style={{ flex: 1, minWidth: 0, width: '100%' }}>{children}</div>
|
|
160
|
+
</>
|
|
161
|
+
)}
|
|
131
162
|
<div
|
|
132
163
|
style={{
|
|
133
164
|
...pcStyles.flex,
|
|
@@ -143,7 +174,7 @@ export default function AppShell({
|
|
|
143
174
|
</div>
|
|
144
175
|
</section>
|
|
145
176
|
</div>
|
|
146
|
-
{isMobile && (
|
|
177
|
+
{isMobile && !hideNav && (
|
|
147
178
|
<MobileNavDrawer
|
|
148
179
|
isOpen={isMobileNavOpen}
|
|
149
180
|
onClose={() => setMobileNavOpen(false)}
|
|
@@ -26,10 +26,11 @@ type TopAppBarProps = {
|
|
|
26
26
|
locale: string;
|
|
27
27
|
onLocaleChange: (next: string) => void;
|
|
28
28
|
onBack?: 'back' | 'signout';
|
|
29
|
+
showNotifications?: boolean;
|
|
29
30
|
};
|
|
30
31
|
|
|
31
32
|
export const TopAppBar = forwardRef<HTMLButtonElement, TopAppBarProps>(function TopAppBar(
|
|
32
|
-
{ onOpenMobileNav, showHamburger, locale, onLocaleChange, onBack },
|
|
33
|
+
{ onOpenMobileNav, showHamburger, locale, onLocaleChange, onBack, showNotifications = true },
|
|
33
34
|
hamburgerRef,
|
|
34
35
|
) {
|
|
35
36
|
const { t } = useTranslation();
|
|
@@ -170,7 +171,7 @@ export const TopAppBar = forwardRef<HTMLButtonElement, TopAppBarProps>(function
|
|
|
170
171
|
onChange={handleLanguageChange}
|
|
171
172
|
/>
|
|
172
173
|
)}
|
|
173
|
-
<NotificationBell />
|
|
174
|
+
{showNotifications && <NotificationBell />}
|
|
174
175
|
<UserMenu onBack={onBack} />
|
|
175
176
|
</div>
|
|
176
177
|
</header>
|
|
@@ -9,6 +9,7 @@ import { AiOutlineClose } from 'react-icons/ai';
|
|
|
9
9
|
import { PiUploadBold } from 'react-icons/pi';
|
|
10
10
|
import { toast } from '../utils/toast';
|
|
11
11
|
import { PCButton, PCCheckbox, PCInput, PCLoader, PCSelect, PCText, PCTextarea } from '../ui';
|
|
12
|
+
import { PCModal } from '../ui/PCModal';
|
|
12
13
|
import { buildGroupDataElements, buildGroupPurposes } from './groupHelpers';
|
|
13
14
|
import { pcStyles } from '../styles/pcStyles';
|
|
14
15
|
import { createCaseRequest, getFormData } from '../lib/api';
|
|
@@ -46,7 +47,8 @@ import {
|
|
|
46
47
|
import PurposeSelectionSection from './DataRequest/PurposeSelectionSection';
|
|
47
48
|
import RequestSubmit from './RequestSubmit';
|
|
48
49
|
import type { CaseHistoryProps } from './types';
|
|
49
|
-
import { FiChevronDown, FiChevronUp, FiX } from 'react-icons/fi';
|
|
50
|
+
import { FiAlertCircle, FiChevronDown, FiChevronUp, FiX } from 'react-icons/fi';
|
|
51
|
+
import { MAX_DISPLAYED_PURPOSE_NAMES_IN_ERASURE_MODAL } from './constant';
|
|
50
52
|
|
|
51
53
|
const getVar = (varName: string): string => `var(${varName})`;
|
|
52
54
|
|
|
@@ -219,6 +221,10 @@ export default function Form({
|
|
|
219
221
|
const [isTranslating, setIsTranslating] = useState(false);
|
|
220
222
|
const [isRequestDetailsExpanded, setIsRequestDetailsExpanded] =
|
|
221
223
|
useState(true);
|
|
224
|
+
const [isErasureConsentModalOpen, setIsErasureConsentModalOpen] =
|
|
225
|
+
useState(false);
|
|
226
|
+
const [revokeConsentOnFulfilment, setRevokeConsentOnFulfilment] =
|
|
227
|
+
useState(false);
|
|
222
228
|
|
|
223
229
|
const resetFileInput = () => {
|
|
224
230
|
if (fileInputRef.current) {
|
|
@@ -537,17 +543,52 @@ export default function Form({
|
|
|
537
543
|
return translatedName;
|
|
538
544
|
}, [data]);
|
|
539
545
|
|
|
546
|
+
// Purpose names selected in an ERASURE request — surfaced in the
|
|
547
|
+
// consent-revoke modal and the post-submit notice. When the list is
|
|
548
|
+
// empty or too long to enumerate, fall back to a generic phrase.
|
|
549
|
+
const selectedErasurePurposeList = useMemo(
|
|
550
|
+
() =>
|
|
551
|
+
selectedPurposes
|
|
552
|
+
.filter((p) => p.selected || p.data_elements.some((de) => de.selected))
|
|
553
|
+
.map((p) => p.name)
|
|
554
|
+
.filter(Boolean),
|
|
555
|
+
[selectedPurposes]
|
|
556
|
+
);
|
|
557
|
+
|
|
558
|
+
const selectedErasurePurposeNames = useMemo(() => {
|
|
559
|
+
if (
|
|
560
|
+
selectedErasurePurposeList.length === 0 ||
|
|
561
|
+
selectedErasurePurposeList.length > MAX_DISPLAYED_PURPOSE_NAMES_IN_ERASURE_MODAL
|
|
562
|
+
) {
|
|
563
|
+
return t('revokeConsentModalYourPurposes');
|
|
564
|
+
}
|
|
565
|
+
return selectedErasurePurposeList.join(', ');
|
|
566
|
+
}, [selectedErasurePurposeList, t]);
|
|
567
|
+
|
|
540
568
|
const handleSubmitClick = () => {
|
|
541
569
|
if (!isFormValid) {
|
|
542
570
|
return;
|
|
543
571
|
}
|
|
544
|
-
//
|
|
545
|
-
//
|
|
546
|
-
//
|
|
547
|
-
//
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
572
|
+
// ERASURE asks the data principal whether to also revoke consent on
|
|
573
|
+
// fulfilment — the choice is made in the modal and threaded into the
|
|
574
|
+
// payload via handleErasureModalChoice. Other request types
|
|
575
|
+
// (ACCESS / CORRECTION / NOMINATION / GRIEVANCE) submit directly.
|
|
576
|
+
if (requestType === RequestTypeEnum.ERASURE) {
|
|
577
|
+
setIsErasureConsentModalOpen(true);
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
setRevokeConsentOnFulfilment(false);
|
|
581
|
+
void handleSubmit(false);
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
const handleErasureModalChoice = (revoke: boolean) => {
|
|
585
|
+
setRevokeConsentOnFulfilment(revoke);
|
|
586
|
+
setIsErasureConsentModalOpen(false);
|
|
587
|
+
void handleSubmit(revoke);
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
const handleErasureModalClose = () => {
|
|
591
|
+
setIsErasureConsentModalOpen(false);
|
|
551
592
|
};
|
|
552
593
|
|
|
553
594
|
/**
|
|
@@ -959,6 +1000,8 @@ export default function Form({
|
|
|
959
1000
|
result={submittedCase}
|
|
960
1001
|
contact={formData?.contact}
|
|
961
1002
|
onCreateRequest={onCreateRequest}
|
|
1003
|
+
revokeConsentOnFulfilment={revokeConsentOnFulfilment}
|
|
1004
|
+
revokedPurposeNames={selectedErasurePurposeNames}
|
|
962
1005
|
/>
|
|
963
1006
|
) : (
|
|
964
1007
|
<div>
|
|
@@ -1616,6 +1659,82 @@ export default function Form({
|
|
|
1616
1659
|
</div>
|
|
1617
1660
|
</div>
|
|
1618
1661
|
)}
|
|
1662
|
+
<PCModal
|
|
1663
|
+
isOpen={isErasureConsentModalOpen}
|
|
1664
|
+
onClose={handleErasureModalClose}
|
|
1665
|
+
size="md"
|
|
1666
|
+
showCloseButton={false}
|
|
1667
|
+
footer={
|
|
1668
|
+
<div className="pc-erasure-modal-footer">
|
|
1669
|
+
<PCButton
|
|
1670
|
+
variant="secondary"
|
|
1671
|
+
onClick={() => handleErasureModalChoice(false)}
|
|
1672
|
+
disabled={isLoading}
|
|
1673
|
+
>
|
|
1674
|
+
{t('revokeConsentNo')}
|
|
1675
|
+
</PCButton>
|
|
1676
|
+
<PCButton
|
|
1677
|
+
variant="primary"
|
|
1678
|
+
onClick={() => handleErasureModalChoice(true)}
|
|
1679
|
+
disabled={isLoading}
|
|
1680
|
+
isLoading={isLoading}
|
|
1681
|
+
>
|
|
1682
|
+
{t('revokeConsentYes')}
|
|
1683
|
+
</PCButton>
|
|
1684
|
+
</div>
|
|
1685
|
+
}
|
|
1686
|
+
>
|
|
1687
|
+
<div className="pc-erasure-modal-content">
|
|
1688
|
+
<div className="pc-erasure-modal-header">
|
|
1689
|
+
<div className="pc-erasure-modal-icon">
|
|
1690
|
+
<FiAlertCircle size={24} />
|
|
1691
|
+
</div>
|
|
1692
|
+
<p className="pc-erasure-modal-title">
|
|
1693
|
+
{t('revokeConsentModalTitle')}
|
|
1694
|
+
</p>
|
|
1695
|
+
<p className="pc-erasure-modal-question">
|
|
1696
|
+
{t('revokeConsentModalQuestion')}
|
|
1697
|
+
</p>
|
|
1698
|
+
</div>
|
|
1699
|
+
|
|
1700
|
+
<div className="pc-erasure-modal-purposes">
|
|
1701
|
+
<p className="pc-erasure-modal-purposes-label">
|
|
1702
|
+
{t('revokeConsentModalPurposesLabel')}
|
|
1703
|
+
</p>
|
|
1704
|
+
{selectedErasurePurposeList.length > 0 ? (
|
|
1705
|
+
<>
|
|
1706
|
+
{selectedErasurePurposeList
|
|
1707
|
+
.slice(0, MAX_DISPLAYED_PURPOSE_NAMES_IN_ERASURE_MODAL)
|
|
1708
|
+
.map((name) => (
|
|
1709
|
+
<div key={name} className="pc-erasure-modal-purpose-item">
|
|
1710
|
+
{name}
|
|
1711
|
+
</div>
|
|
1712
|
+
))}
|
|
1713
|
+
{selectedErasurePurposeList.length > MAX_DISPLAYED_PURPOSE_NAMES_IN_ERASURE_MODAL && (
|
|
1714
|
+
<span className="pc-erasure-modal-purposes-placeholder">
|
|
1715
|
+
{t('revokeConsentModalAndMore', {
|
|
1716
|
+
count: selectedErasurePurposeList.length - MAX_DISPLAYED_PURPOSE_NAMES_IN_ERASURE_MODAL,
|
|
1717
|
+
})}
|
|
1718
|
+
</span>
|
|
1719
|
+
)}
|
|
1720
|
+
</>
|
|
1721
|
+
) : (
|
|
1722
|
+
<span className="pc-erasure-modal-purposes-placeholder">
|
|
1723
|
+
{t('revokeConsentModalYourPurposes')}
|
|
1724
|
+
</span>
|
|
1725
|
+
)}
|
|
1726
|
+
</div>
|
|
1727
|
+
|
|
1728
|
+
<div className="pc-erasure-modal-warning">
|
|
1729
|
+
<p className="pc-erasure-modal-warning-text">
|
|
1730
|
+
<span className="pc-erasure-modal-warning-label">
|
|
1731
|
+
{t('revokeConsentModalIfYesLabel')}
|
|
1732
|
+
</span>{' '}
|
|
1733
|
+
{t('revokeConsentModalIfYesBody')}
|
|
1734
|
+
</p>
|
|
1735
|
+
</div>
|
|
1736
|
+
</div>
|
|
1737
|
+
</PCModal>
|
|
1619
1738
|
</>
|
|
1620
1739
|
);
|
|
1621
1740
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { useTranslation } from 'react-i18next';
|
|
2
|
+
import character from '../assets/character.svg';
|
|
3
|
+
import { PCText, PCTitle } from '../ui';
|
|
4
|
+
import { pcStyles } from '../styles/pcStyles';
|
|
5
|
+
import { getVar } from '../lib/utils';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Full-screen content shown (in place of the consent manager / tabs) when the
|
|
9
|
+
* user has no data anywhere — no consents, no data-request cases, no activity.
|
|
10
|
+
* Purely presentational: a friendly illustration, heading, and subtitle.
|
|
11
|
+
*/
|
|
12
|
+
export function PrivacyCenterEmptyState() {
|
|
13
|
+
const { t } = useTranslation();
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div
|
|
17
|
+
style={{
|
|
18
|
+
...pcStyles.flex,
|
|
19
|
+
...pcStyles.flexCol,
|
|
20
|
+
...pcStyles.itemsCenter,
|
|
21
|
+
...pcStyles.justifyCenter,
|
|
22
|
+
...pcStyles.textCenter,
|
|
23
|
+
width: '100%',
|
|
24
|
+
maxWidth: 460,
|
|
25
|
+
margin: '0 auto',
|
|
26
|
+
minHeight: 'min(62vh, 540px)',
|
|
27
|
+
paddingTop: getVar('--pc-spacing-2xl'),
|
|
28
|
+
paddingBottom: getVar('--pc-spacing-2xl'),
|
|
29
|
+
gap: getVar('--pc-spacing-xl'),
|
|
30
|
+
}}
|
|
31
|
+
>
|
|
32
|
+
<div
|
|
33
|
+
aria-hidden="true"
|
|
34
|
+
style={{
|
|
35
|
+
display: 'flex',
|
|
36
|
+
alignItems: 'center',
|
|
37
|
+
justifyContent: 'center',
|
|
38
|
+
width: 168,
|
|
39
|
+
height: 168,
|
|
40
|
+
borderRadius: '50%',
|
|
41
|
+
background:
|
|
42
|
+
'color-mix(in srgb, var(--pc-primary) 8%, var(--pc-bg-shell))',
|
|
43
|
+
flexShrink: 0,
|
|
44
|
+
}}
|
|
45
|
+
>
|
|
46
|
+
<img
|
|
47
|
+
src={character}
|
|
48
|
+
alt=""
|
|
49
|
+
style={{ width: 104, height: 'auto', display: 'block' }}
|
|
50
|
+
/>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div
|
|
54
|
+
style={{
|
|
55
|
+
...pcStyles.flex,
|
|
56
|
+
...pcStyles.flexCol,
|
|
57
|
+
...pcStyles.itemsCenter,
|
|
58
|
+
gap: getVar('--pc-spacing-sm'),
|
|
59
|
+
}}
|
|
60
|
+
>
|
|
61
|
+
<PCTitle as="h2" style={{ margin: 0 }}>
|
|
62
|
+
{t('noDataTitle')}
|
|
63
|
+
</PCTitle>
|
|
64
|
+
<PCText
|
|
65
|
+
as="p"
|
|
66
|
+
size="base"
|
|
67
|
+
color="secondary"
|
|
68
|
+
align="center"
|
|
69
|
+
style={{ margin: 0, maxWidth: 400 }}
|
|
70
|
+
>
|
|
71
|
+
{t('noDataDescription')}
|
|
72
|
+
</PCText>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
@@ -13,7 +13,7 @@ import { useAuth } from '../context/AuthContext';
|
|
|
13
13
|
import { useBaseUrl } from '../context/BaseUrlContext';
|
|
14
14
|
import { useCurrentLanguage } from '../lib/useCurrentLanguage';
|
|
15
15
|
import { toast } from '../utils/toast';
|
|
16
|
-
import type { Receipt, ReceiptEventType
|
|
16
|
+
import type { Receipt, ReceiptEventType } from '../lib/types';
|
|
17
17
|
import { useReceiptColumns } from './receiptColumns';
|
|
18
18
|
|
|
19
19
|
/**
|
|
@@ -89,7 +89,6 @@ export default function ReceiptsList() {
|
|
|
89
89
|
|
|
90
90
|
// ─── Filters ────────────────────────────────────────────────────────────────
|
|
91
91
|
const [eventType, setEventType] = useState<ReceiptEventType | ''>('');
|
|
92
|
-
const [status, setStatus] = useState<ReceiptPurposeStatus | ''>('');
|
|
93
92
|
const [datePreset, setDatePreset] = useState<string>(''); // '' | '1' | '7' | '30' | '90' | 'custom'
|
|
94
93
|
const [customFrom, setCustomFrom] = useState<string>('');
|
|
95
94
|
const [customTo, setCustomTo] = useState<string>('');
|
|
@@ -104,15 +103,6 @@ export default function ReceiptsList() {
|
|
|
104
103
|
],
|
|
105
104
|
[t]
|
|
106
105
|
);
|
|
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
106
|
const dateOptions = useMemo<SelectOption[]>(
|
|
117
107
|
() => [
|
|
118
108
|
{ value: '', label: t('allTime') },
|
|
@@ -126,9 +116,8 @@ export default function ReceiptsList() {
|
|
|
126
116
|
);
|
|
127
117
|
|
|
128
118
|
const eventValue = eventTypeOptions.find((o) => o.value === eventType) ?? null;
|
|
129
|
-
const statusValue = statusOptions.find((o) => o.value === status) ?? null;
|
|
130
119
|
const dateValue = dateOptions.find((o) => o.value === datePreset) ?? null;
|
|
131
|
-
const hasActiveFilters = Boolean(eventType ||
|
|
120
|
+
const hasActiveFilters = Boolean(eventType || datePreset);
|
|
132
121
|
const isCustomRangeInvalid = useMemo(
|
|
133
122
|
() =>
|
|
134
123
|
datePreset === 'custom' &&
|
|
@@ -142,9 +131,6 @@ export default function ReceiptsList() {
|
|
|
142
131
|
if (eventType && eventValue?.label) {
|
|
143
132
|
filters.push({ id: 'event', label: t('event'), value: eventValue.label });
|
|
144
133
|
}
|
|
145
|
-
if (status && statusValue?.label) {
|
|
146
|
-
filters.push({ id: 'status', label: t('status'), value: statusValue.label });
|
|
147
|
-
}
|
|
148
134
|
if (datePreset && dateValue?.label) {
|
|
149
135
|
let dateLabel = dateValue.label;
|
|
150
136
|
if (datePreset === 'custom') {
|
|
@@ -156,16 +142,12 @@ export default function ReceiptsList() {
|
|
|
156
142
|
}
|
|
157
143
|
|
|
158
144
|
return filters;
|
|
159
|
-
}, [eventType, eventValue,
|
|
145
|
+
}, [eventType, eventValue, datePreset, dateValue, customFrom, customTo, t]);
|
|
160
146
|
|
|
161
147
|
const handleEventChange = (opt: SelectOption | null) => {
|
|
162
148
|
setEventType((opt ? String(opt.value) : '') as ReceiptEventType | '');
|
|
163
149
|
setCurrentPage(1);
|
|
164
150
|
};
|
|
165
|
-
const handleStatusChange = (opt: SelectOption | null) => {
|
|
166
|
-
setStatus((opt ? String(opt.value) : '') as ReceiptPurposeStatus | '');
|
|
167
|
-
setCurrentPage(1);
|
|
168
|
-
};
|
|
169
151
|
const handleDateChange = (opt: SelectOption | null) => {
|
|
170
152
|
const nextDatePreset = opt ? String(opt.value) : '';
|
|
171
153
|
setDatePreset(nextDatePreset);
|
|
@@ -177,7 +159,6 @@ export default function ReceiptsList() {
|
|
|
177
159
|
};
|
|
178
160
|
const handleClearFilters = () => {
|
|
179
161
|
setEventType('');
|
|
180
|
-
setStatus('');
|
|
181
162
|
setDatePreset('');
|
|
182
163
|
setCustomFrom('');
|
|
183
164
|
setCustomTo('');
|
|
@@ -199,7 +180,6 @@ export default function ReceiptsList() {
|
|
|
199
180
|
skip,
|
|
200
181
|
limit: size,
|
|
201
182
|
event_type: eventType || undefined,
|
|
202
|
-
status: status || undefined,
|
|
203
183
|
created_after,
|
|
204
184
|
created_before,
|
|
205
185
|
language: langParam,
|
|
@@ -229,7 +209,6 @@ export default function ReceiptsList() {
|
|
|
229
209
|
currentPage,
|
|
230
210
|
pageSize,
|
|
231
211
|
eventType,
|
|
232
|
-
status,
|
|
233
212
|
datePreset,
|
|
234
213
|
customFrom,
|
|
235
214
|
customTo,
|
|
@@ -375,23 +354,6 @@ export default function ReceiptsList() {
|
|
|
375
354
|
dropdownMatchTriggerWidth
|
|
376
355
|
style={{ width: '100%' }}
|
|
377
356
|
/>
|
|
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
357
|
<PCSelect
|
|
396
358
|
className="pc-filter-select pc-receipts-filter-control"
|
|
397
359
|
size="sm"
|