@redacto.io/consent-sdk-react 5.2.0 → 5.3.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/CHANGELOG.md +6 -0
- package/dist/index.d.mts +24 -1
- package/dist/index.d.ts +24 -1
- package/dist/index.js +149 -206
- package/dist/index.mjs +148 -206
- package/dist/privacy-center.d.mts +81 -2
- package/dist/privacy-center.d.ts +81 -2
- package/dist/privacy-center.js +2823 -1272
- package/dist/privacy-center.mjs +2813 -1263
- package/package.json +1 -1
- package/src/RedactoNoticeConsent/RedactoNoticeConsent.tsx +16 -20
- package/src/RedactoNoticeConsent/api/index.ts +21 -103
- package/src/RedactoNoticeConsentInline/RedactoNoticeConsentInline.tsx +5 -4
- package/src/RedactoNoticeConsentInline/api/index.ts +64 -117
- package/src/RedactoPrivacyCenter/NotificationsPage.tsx +174 -0
- package/src/RedactoPrivacyCenter/PrivacyCenterHomePage.tsx +3 -0
- package/src/RedactoPrivacyCenter/api/actions.ts +84 -0
- package/src/RedactoPrivacyCenter/api/client.ts +118 -0
- package/src/RedactoPrivacyCenter/components/DataRequest/PurposeSelectionSection.tsx +15 -0
- package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationBell.tsx +160 -0
- package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationCTAButton.tsx +18 -0
- package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationDropdown.tsx +54 -0
- package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationDropdownItem.tsx +57 -0
- package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationIcon.tsx +19 -0
- package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationListPanel.tsx +92 -0
- package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationRefreshButton.tsx +20 -0
- package/src/RedactoPrivacyCenter/components/NotificationHub/NotificationRow.tsx +58 -0
- package/src/RedactoPrivacyCenter/components/NotificationHub/StatCard.tsx +10 -0
- package/src/RedactoPrivacyCenter/components/NotificationHub/useNotificationActions.ts +112 -0
- package/src/RedactoPrivacyCenter/components/NotificationHub/utils.ts +27 -0
- package/src/RedactoPrivacyCenter/components/PrivacyCenterLayout.tsx +3 -2
- package/src/RedactoPrivacyCenter/context/NavigationContext.tsx +2 -2
- package/src/RedactoPrivacyCenter/lib/api.ts +77 -0
- package/src/RedactoPrivacyCenter/lib/constants.ts +78 -1
- package/src/RedactoPrivacyCenter/lib/types.ts +148 -0
- package/src/RedactoPrivacyCenter/lib/utils.ts +3 -0
- package/src/RedactoPrivacyCenter/locales/en/translation.json +27 -1
- package/src/RedactoPrivacyCenter/styles/injectStyles.ts +505 -0
- package/src/RedactoPrivacyCenter/ui/PCPopover.tsx +23 -1
- package/src/index.ts +5 -0
- package/src/privacy-center.ts +1 -0
- package/src/shared/api-errors.ts +171 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { useTranslation } from 'react-i18next';
|
|
2
|
+
import type { NotificationRowProps } from '../../lib/types';
|
|
3
|
+
import { NotificationIcon } from './NotificationIcon';
|
|
4
|
+
import { NotificationCTAButton } from './NotificationCTAButton';
|
|
5
|
+
import { formatTimeAgo } from './utils';
|
|
6
|
+
|
|
7
|
+
export const NotificationRow = ({ notification, onClick, onCTAClick }: NotificationRowProps) => {
|
|
8
|
+
const { t } = useTranslation();
|
|
9
|
+
const statusLabel = notification.status_label_display || notification.status_label;
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<div
|
|
13
|
+
onClick={onClick}
|
|
14
|
+
onKeyDown={(e) => {
|
|
15
|
+
if (e.currentTarget !== e.target) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
19
|
+
e.preventDefault();
|
|
20
|
+
onClick();
|
|
21
|
+
}
|
|
22
|
+
}}
|
|
23
|
+
role="button"
|
|
24
|
+
tabIndex={0}
|
|
25
|
+
className="pc-notification-row"
|
|
26
|
+
data-read={notification.is_read ? 'true' : 'false'}
|
|
27
|
+
>
|
|
28
|
+
<NotificationIcon notificationType={notification.notification_type} size="lg" />
|
|
29
|
+
|
|
30
|
+
<div className="pc-notification-row-content">
|
|
31
|
+
<div className="pc-notification-row-title-line">
|
|
32
|
+
<span className="pc-notification-row-title">{notification.title}</span>
|
|
33
|
+
{!notification.is_read && <span className="pc-notification-unread-dot-lg" />}
|
|
34
|
+
{statusLabel && (
|
|
35
|
+
<span className="pc-notification-status-badge">{statusLabel}</span>
|
|
36
|
+
)}
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<p className="pc-notification-row-description">{notification.description}</p>
|
|
40
|
+
|
|
41
|
+
{notification.ctas && notification.ctas.length > 0 && (
|
|
42
|
+
<div className="pc-notification-row-ctas">
|
|
43
|
+
{notification.ctas.map((cta) => (
|
|
44
|
+
<NotificationCTAButton
|
|
45
|
+
key={`${cta.action_type}-${cta.label}`}
|
|
46
|
+
cta={cta}
|
|
47
|
+
size="lg"
|
|
48
|
+
onClick={(e) => { e.stopPropagation(); onCTAClick(cta); }}
|
|
49
|
+
/>
|
|
50
|
+
))}
|
|
51
|
+
</div>
|
|
52
|
+
)}
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<span className="pc-notification-row-time">{formatTimeAgo(notification.created_at, t)}</span>
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { StatCardProps } from '../../lib/types';
|
|
2
|
+
|
|
3
|
+
export const StatCard = ({ value, label }: StatCardProps) => {
|
|
4
|
+
return (
|
|
5
|
+
<div className="pc-notification-stat-card">
|
|
6
|
+
<span className="pc-notification-stat-value">{value}</span>
|
|
7
|
+
<span className="pc-notification-stat-label">{label}</span>
|
|
8
|
+
</div>
|
|
9
|
+
);
|
|
10
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { useCallback, useMemo } from 'react';
|
|
2
|
+
import { useInternalNavigation } from '../../context/NavigationContext';
|
|
3
|
+
import { useOrganization } from '../../context/OrganizationContext';
|
|
4
|
+
import { useAuth } from '../../context/AuthContext';
|
|
5
|
+
import { useBaseUrl } from '../../context/BaseUrlContext';
|
|
6
|
+
import { markNotificationRead, markAllRead, acknowledgeNotification, fetchCaseHistory } from '../../lib/api';
|
|
7
|
+
import type { AuthContext } from '../../lib/api';
|
|
8
|
+
import { useCurrentLanguage } from '../../lib/useCurrentLanguage';
|
|
9
|
+
import type { Notification, NotificationCTA } from '../../lib/types';
|
|
10
|
+
import { CASE_DETAILS_PREFIX } from '../../lib/constants';
|
|
11
|
+
|
|
12
|
+
export const useNotificationActions = () => {
|
|
13
|
+
const { navigateTo, setSelectedCase } = useInternalNavigation();
|
|
14
|
+
const { organization } = useOrganization();
|
|
15
|
+
const { accessToken, refreshToken, onError, updateTokens } = useAuth();
|
|
16
|
+
const { consentClient } = useBaseUrl();
|
|
17
|
+
const { langParam } = useCurrentLanguage();
|
|
18
|
+
|
|
19
|
+
const organizationId = organization?.uuid;
|
|
20
|
+
const workspaceId = organization?.workspaces[0]?.uuid;
|
|
21
|
+
|
|
22
|
+
const authContext: AuthContext = useMemo(() => ({
|
|
23
|
+
accessToken, refreshToken, onError, updateTokens,
|
|
24
|
+
}), [accessToken, refreshToken, onError, updateTokens]);
|
|
25
|
+
|
|
26
|
+
const hasAcknowledgeCTA = useCallback((notification: Notification) => {
|
|
27
|
+
return notification.ctas?.some((cta) => cta.action_type === 'acknowledge') ?? false;
|
|
28
|
+
}, []);
|
|
29
|
+
|
|
30
|
+
const handleMarkRead = useCallback(async (notificationUuid: string): Promise<boolean> => {
|
|
31
|
+
if (!organizationId || !workspaceId) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
const result = await markNotificationRead(organizationId, workspaceId, notificationUuid, authContext, consentClient);
|
|
35
|
+
return result !== null;
|
|
36
|
+
}, [organizationId, workspaceId, authContext, consentClient]);
|
|
37
|
+
|
|
38
|
+
const handleMarkAllRead = useCallback(async (): Promise<boolean> => {
|
|
39
|
+
if (!organizationId || !workspaceId) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
const result = await markAllRead(organizationId, workspaceId, authContext, consentClient);
|
|
43
|
+
return result !== null;
|
|
44
|
+
}, [organizationId, workspaceId, authContext, consentClient]);
|
|
45
|
+
|
|
46
|
+
const handleAcknowledge = useCallback(async (notificationUuid: string): Promise<boolean> => {
|
|
47
|
+
if (!organizationId || !workspaceId) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
const result = await acknowledgeNotification(organizationId, workspaceId, notificationUuid, authContext, consentClient);
|
|
51
|
+
return result !== null;
|
|
52
|
+
}, [organizationId, workspaceId, authContext, consentClient]);
|
|
53
|
+
|
|
54
|
+
const handleCTAClick = useCallback(async (notification: Notification, cta: NotificationCTA) => {
|
|
55
|
+
if (cta.action_type === 'acknowledge') {
|
|
56
|
+
const ok = await handleAcknowledge(notification.uuid);
|
|
57
|
+
if (!ok) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
return 'acknowledged' as const;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (cta.action_type !== 'link' || !cta.url) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!notification.is_read) {
|
|
68
|
+
await handleMarkRead(notification.uuid);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
const caseUuid = cta.url.startsWith(CASE_DETAILS_PREFIX)
|
|
73
|
+
? cta.url.slice(CASE_DETAILS_PREFIX.length)
|
|
74
|
+
: '';
|
|
75
|
+
if (caseUuid && organizationId && workspaceId) {
|
|
76
|
+
const contact = typeof window !== 'undefined' ? sessionStorage.getItem('email') || '' : '';
|
|
77
|
+
const caseHistory = await fetchCaseHistory(organizationId, workspaceId, contact, undefined, undefined, authContext, consentClient, langParam);
|
|
78
|
+
const fullCase = caseHistory?.data?.find((c) => c.uuid === caseUuid);
|
|
79
|
+
if (fullCase) {
|
|
80
|
+
setSelectedCase(fullCase);
|
|
81
|
+
navigateTo('case-details');
|
|
82
|
+
return 'navigated' as const;
|
|
83
|
+
}
|
|
84
|
+
return 'not_found' as const;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let parsed: URL;
|
|
88
|
+
try {
|
|
89
|
+
parsed = new URL(cta.url);
|
|
90
|
+
} catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
window.open(parsed.href, '_blank', 'noopener,noreferrer');
|
|
97
|
+
return 'opened' as const;
|
|
98
|
+
}, [handleAcknowledge, handleMarkRead, navigateTo, setSelectedCase, organizationId, workspaceId, authContext, consentClient, langParam]);
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
organizationId,
|
|
102
|
+
workspaceId,
|
|
103
|
+
authContext,
|
|
104
|
+
consentClient,
|
|
105
|
+
navigateTo,
|
|
106
|
+
hasAcknowledgeCTA,
|
|
107
|
+
handleMarkRead,
|
|
108
|
+
handleMarkAllRead,
|
|
109
|
+
handleAcknowledge,
|
|
110
|
+
handleCTAClick,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { TFunction } from 'i18next';
|
|
2
|
+
|
|
3
|
+
export const formatTimeAgo = (dateString: string, t: TFunction): string => {
|
|
4
|
+
const now = new Date();
|
|
5
|
+
const date = new Date(dateString);
|
|
6
|
+
if (Number.isNaN(date.getTime())) {
|
|
7
|
+
return t('justNow')
|
|
8
|
+
};
|
|
9
|
+
const diffMs = now.getTime() - date.getTime();
|
|
10
|
+
const diffMin = Math.floor(diffMs / 60_000);
|
|
11
|
+
if (diffMin < 1) {
|
|
12
|
+
return t('justNow')
|
|
13
|
+
};
|
|
14
|
+
if (diffMin < 60) {
|
|
15
|
+
return t('minutesAgo', { count: diffMin })
|
|
16
|
+
};
|
|
17
|
+
const diffHrs = Math.floor(diffMin / 60);
|
|
18
|
+
if (diffHrs < 24) {
|
|
19
|
+
return t('hoursAgo', { count: diffHrs })
|
|
20
|
+
};
|
|
21
|
+
const diffDays = Math.floor(diffHrs / 24);
|
|
22
|
+
if (diffDays < 7) {
|
|
23
|
+
return t('daysAgo', { count: diffDays })
|
|
24
|
+
};
|
|
25
|
+
const diffWeeks = Math.floor(diffDays / 7);
|
|
26
|
+
return t('weeksAgo', { count: diffWeeks });
|
|
27
|
+
};
|
|
@@ -12,8 +12,8 @@ import { usePageChrome } from '../context/PageChromeContext';
|
|
|
12
12
|
import i18n from '../lib/i18n';
|
|
13
13
|
import { pcStyles } from '../styles/pcStyles';
|
|
14
14
|
import type { PrivacyCenterLayoutProps } from './types';
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
import { NotificationBell } from './NotificationHub/NotificationBell';
|
|
16
|
+
import { getVar } from '../lib/utils';
|
|
17
17
|
|
|
18
18
|
export default function PrivacyCenterLayout({
|
|
19
19
|
children,
|
|
@@ -141,6 +141,7 @@ export default function PrivacyCenterLayout({
|
|
|
141
141
|
value={languageOptions.find((opt) => opt.value === locale) || null}
|
|
142
142
|
onChange={handleLanguageChange}
|
|
143
143
|
/>
|
|
144
|
+
<NotificationBell />
|
|
144
145
|
<ThemeToggle />
|
|
145
146
|
</div>
|
|
146
147
|
</nav>
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { createContext, useContext, useState, useCallback, type ReactNode } from 'react';
|
|
2
2
|
import type { CaseRequest } from '../lib/types';
|
|
3
3
|
|
|
4
|
-
export type InternalPage = 'consent-manager' | 'form' | 'activity' | 'case-details';
|
|
4
|
+
export type InternalPage = 'consent-manager' | 'form' | 'activity' | 'case-details' | 'notifications';
|
|
5
5
|
|
|
6
6
|
const NAV_STORAGE_KEY = 'pc_current_page';
|
|
7
7
|
const CASE_UUID_STORAGE_KEY = 'pc_selected_case_uuid';
|
|
8
8
|
|
|
9
9
|
const VALID_PAGES: ReadonlySet<string> = new Set<InternalPage>([
|
|
10
|
-
'consent-manager', 'form', 'activity', 'case-details',
|
|
10
|
+
'consent-manager', 'form', 'activity', 'case-details', 'notifications',
|
|
11
11
|
]);
|
|
12
12
|
|
|
13
13
|
function getStoredPage(fallback: InternalPage): InternalPage {
|
|
@@ -11,6 +11,12 @@ import {
|
|
|
11
11
|
uploadCaseDocumentServer,
|
|
12
12
|
submitDocumentForRequestServer,
|
|
13
13
|
getCaseDocumentServer,
|
|
14
|
+
getNotificationsServer,
|
|
15
|
+
getNotificationSummaryServer,
|
|
16
|
+
getUnreadCountServer,
|
|
17
|
+
markNotificationReadServer,
|
|
18
|
+
markAllNotificationsReadServer,
|
|
19
|
+
acknowledgeNotificationServer,
|
|
14
20
|
} from '../api/actions';
|
|
15
21
|
import type { BaseUrlContextValue } from '../context/BaseUrlContext';
|
|
16
22
|
import type {
|
|
@@ -20,6 +26,11 @@ import type {
|
|
|
20
26
|
CreateCaseResponse,
|
|
21
27
|
FormData,
|
|
22
28
|
MessageDocument,
|
|
29
|
+
NotificationActionResponse,
|
|
30
|
+
NotificationListResponse,
|
|
31
|
+
NotificationQueryParams,
|
|
32
|
+
NotificationSummary,
|
|
33
|
+
UnreadCountResponse,
|
|
23
34
|
UploadDocumentResponse,
|
|
24
35
|
UserConsentDetail,
|
|
25
36
|
UserDataRequest,
|
|
@@ -564,3 +575,69 @@ export const getCaseDocument = async (
|
|
|
564
575
|
withAuthRetry(organizationId, workspaceId, authContext, consentClient, 'Error fetching case document:', (token) =>
|
|
565
576
|
getCaseDocumentServer({ organizationId, workspaceId, caseUuid, documentUuid, authToken: token, consentClient })
|
|
566
577
|
);
|
|
578
|
+
|
|
579
|
+
// ─── Notification Hub ───────────────────────────────────────────────────────
|
|
580
|
+
|
|
581
|
+
export const fetchNotifications = async (
|
|
582
|
+
organizationId: string,
|
|
583
|
+
workspaceId: string,
|
|
584
|
+
params?: NotificationQueryParams,
|
|
585
|
+
authContext?: AuthContext,
|
|
586
|
+
consentClient?: BaseUrlContextValue['consentClient'],
|
|
587
|
+
language?: string,
|
|
588
|
+
): Promise<NotificationListResponse | null> =>
|
|
589
|
+
withAuthRetry(organizationId, workspaceId, authContext, consentClient, 'Error fetching notifications:', (token) =>
|
|
590
|
+
getNotificationsServer({ organizationId, workspaceId, ...params, language, authToken: token, consentClient })
|
|
591
|
+
);
|
|
592
|
+
|
|
593
|
+
export const fetchNotificationSummary = async (
|
|
594
|
+
organizationId: string,
|
|
595
|
+
workspaceId: string,
|
|
596
|
+
authContext?: AuthContext,
|
|
597
|
+
consentClient?: BaseUrlContextValue['consentClient']
|
|
598
|
+
): Promise<NotificationSummary | null> =>
|
|
599
|
+
withAuthRetry(organizationId, workspaceId, authContext, consentClient, 'Error fetching notification summary:', (token) =>
|
|
600
|
+
getNotificationSummaryServer({ organizationId, workspaceId, authToken: token, consentClient })
|
|
601
|
+
);
|
|
602
|
+
|
|
603
|
+
export const fetchUnreadCount = async (
|
|
604
|
+
organizationId: string,
|
|
605
|
+
workspaceId: string,
|
|
606
|
+
authContext?: AuthContext,
|
|
607
|
+
consentClient?: BaseUrlContextValue['consentClient']
|
|
608
|
+
): Promise<UnreadCountResponse | null> =>
|
|
609
|
+
withAuthRetry(organizationId, workspaceId, authContext, consentClient, 'Error fetching unread count:', (token) =>
|
|
610
|
+
getUnreadCountServer({ organizationId, workspaceId, authToken: token, consentClient })
|
|
611
|
+
);
|
|
612
|
+
|
|
613
|
+
export const markNotificationRead = async (
|
|
614
|
+
organizationId: string,
|
|
615
|
+
workspaceId: string,
|
|
616
|
+
notificationUuid: string,
|
|
617
|
+
authContext?: AuthContext,
|
|
618
|
+
consentClient?: BaseUrlContextValue['consentClient']
|
|
619
|
+
): Promise<NotificationActionResponse | null> =>
|
|
620
|
+
withAuthRetry(organizationId, workspaceId, authContext, consentClient, 'Error marking notification as read:', (token) =>
|
|
621
|
+
markNotificationReadServer({ organizationId, workspaceId, notificationUuid, authToken: token, consentClient })
|
|
622
|
+
);
|
|
623
|
+
|
|
624
|
+
export const markAllRead = async (
|
|
625
|
+
organizationId: string,
|
|
626
|
+
workspaceId: string,
|
|
627
|
+
authContext?: AuthContext,
|
|
628
|
+
consentClient?: BaseUrlContextValue['consentClient']
|
|
629
|
+
): Promise<NotificationActionResponse | null> =>
|
|
630
|
+
withAuthRetry(organizationId, workspaceId, authContext, consentClient, 'Error marking all notifications as read:', (token) =>
|
|
631
|
+
markAllNotificationsReadServer({ organizationId, workspaceId, authToken: token, consentClient })
|
|
632
|
+
);
|
|
633
|
+
|
|
634
|
+
export const acknowledgeNotification = async (
|
|
635
|
+
organizationId: string,
|
|
636
|
+
workspaceId: string,
|
|
637
|
+
notificationUuid: string,
|
|
638
|
+
authContext?: AuthContext,
|
|
639
|
+
consentClient?: BaseUrlContextValue['consentClient']
|
|
640
|
+
): Promise<NotificationActionResponse | null> =>
|
|
641
|
+
withAuthRetry(organizationId, workspaceId, authContext, consentClient, 'Error acknowledging notification:', (token) =>
|
|
642
|
+
acknowledgeNotificationServer({ organizationId, workspaceId, notificationUuid, authToken: token, consentClient })
|
|
643
|
+
);
|
|
@@ -4,7 +4,8 @@ import { GRIEVANCE_TAB_IDS } from '../components/constant';
|
|
|
4
4
|
import { menuItemsPC } from '../components/SideMenuBarPC/constants';
|
|
5
5
|
import type { MenuItem } from '../components/SideMenuBarPC/types';
|
|
6
6
|
import type { InternalPage } from '../context/NavigationContext';
|
|
7
|
-
import type { DocumentRequestStatus, DocumentRequestStatusInfo } from './types';
|
|
7
|
+
import type { DocumentRequestStatus, DocumentRequestStatusInfo, NotificationCTA, NotificationType, NotificationCategory, IconCategory, FilterTab } from './types';
|
|
8
|
+
import { IoPersonOutline, IoTimeOutline, IoDocumentOutline, IoShieldCheckmarkOutline, IoAlertCircleOutline, IoSettingsOutline } from 'react-icons/io5';
|
|
8
9
|
|
|
9
10
|
export const useRequestTypeLabels = () => {
|
|
10
11
|
const { t } = useTranslation();
|
|
@@ -89,6 +90,10 @@ export const PAGE_CHROME_BY_PAGE: Record<InternalPage, PageChromeConfig> = {
|
|
|
89
90
|
titleKey: 'caseDetails',
|
|
90
91
|
descriptionKey: 'yourPrivacyCenter',
|
|
91
92
|
},
|
|
93
|
+
notifications: {
|
|
94
|
+
titleKey: 'notificationHub',
|
|
95
|
+
descriptionKey: 'notificationHubSubtitle',
|
|
96
|
+
},
|
|
92
97
|
};
|
|
93
98
|
|
|
94
99
|
const HREF_TO_NAME_KEY: Record<string, string> = {
|
|
@@ -159,3 +164,75 @@ export const ACTIVITY_DATE_FORMATS = {
|
|
|
159
164
|
DISPLAY: 'MMM D, YYYY [at] h:mm A',
|
|
160
165
|
TOOLTIP: 'MMMM D, YYYY [at] h:mm:ss A',
|
|
161
166
|
} as const;
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
// ─── Notification Hub ──────────────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
export const POLL_INTERVAL = 30_000;
|
|
172
|
+
export const NOTIFICATION_PAGE_SIZE = 10;
|
|
173
|
+
export const NOTIFICATION_DROPDOWN_LIMIT = 5;
|
|
174
|
+
|
|
175
|
+
export const TAB_TO_CATEGORY: Record<FilterTab, NotificationCategory | undefined> = {
|
|
176
|
+
all: undefined,
|
|
177
|
+
consent: 'consent',
|
|
178
|
+
request: 'request',
|
|
179
|
+
system: 'system',
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
export const TYPE_TO_ICON_CATEGORY: Record<NotificationType, IconCategory> = {
|
|
183
|
+
guardian_verification_request: 'guardian',
|
|
184
|
+
consent_expiry_warning: 'consent',
|
|
185
|
+
new_consent_notice: 'system',
|
|
186
|
+
consent_withdrawal_confirmation: 'withdrawal',
|
|
187
|
+
data_request_status_update: 'access',
|
|
188
|
+
case_message: 'access',
|
|
189
|
+
grievance_status_update: 'grievance',
|
|
190
|
+
consent_regrant_prompt: 'consent',
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
export const ICON_MAP: Record<IconCategory, typeof IoPersonOutline> = {
|
|
194
|
+
guardian: IoPersonOutline,
|
|
195
|
+
consent: IoTimeOutline,
|
|
196
|
+
access: IoDocumentOutline,
|
|
197
|
+
withdrawal: IoShieldCheckmarkOutline,
|
|
198
|
+
grievance: IoAlertCircleOutline,
|
|
199
|
+
system: IoSettingsOutline,
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
export const ICON_COLORS: Record<IconCategory, string> = {
|
|
203
|
+
guardian: '#7c3aed',
|
|
204
|
+
consent: '#ea580c',
|
|
205
|
+
access: '#16a34a',
|
|
206
|
+
withdrawal: '#2563eb',
|
|
207
|
+
grievance: '#dc2626',
|
|
208
|
+
system: '#6b7280',
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
export const CTA_VARIANT_CLASS: Record<NotificationCTA['style'], string> = {
|
|
212
|
+
primary: 'pc-notification-cta--primary',
|
|
213
|
+
danger: 'pc-notification-cta--danger',
|
|
214
|
+
secondary: 'pc-notification-cta--secondary',
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
export const useNotificationFilterTabs = () => {
|
|
218
|
+
const { t } = useTranslation();
|
|
219
|
+
return [
|
|
220
|
+
{ key: 'all' as FilterTab, label: t('all') },
|
|
221
|
+
{ key: 'consent' as FilterTab, label: t('consent') },
|
|
222
|
+
{ key: 'request' as FilterTab, label: t('requests') },
|
|
223
|
+
{ key: 'system' as FilterTab, label: t('system') },
|
|
224
|
+
];
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
export const useNotificationStatLabels = () => {
|
|
228
|
+
const { t } = useTranslation();
|
|
229
|
+
return {
|
|
230
|
+
total: t('total'),
|
|
231
|
+
unread: t('unread'),
|
|
232
|
+
action_needed: t('actionNeeded'),
|
|
233
|
+
acknowledged: t('acknowledged'),
|
|
234
|
+
};
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
export const CASE_DETAILS_PREFIX = '/case-details/';
|
|
@@ -529,3 +529,151 @@ export type SubmitDocumentForRequestInput = Readonly<{
|
|
|
529
529
|
export type DocumentRequestStatusInfo = Readonly<{ labelKey: string; color: string }>;
|
|
530
530
|
|
|
531
531
|
export type CaseDetailsTab = Readonly<{ id: TabId; label: string }>;
|
|
532
|
+
|
|
533
|
+
// ─── Notification Hub ──────────────────────────────────────────────────────
|
|
534
|
+
|
|
535
|
+
export type NotificationType =
|
|
536
|
+
| 'guardian_verification_request'
|
|
537
|
+
| 'consent_expiry_warning'
|
|
538
|
+
| 'new_consent_notice'
|
|
539
|
+
| 'consent_withdrawal_confirmation'
|
|
540
|
+
| 'data_request_status_update'
|
|
541
|
+
| 'case_message'
|
|
542
|
+
| 'grievance_status_update'
|
|
543
|
+
| 'consent_regrant_prompt';
|
|
544
|
+
|
|
545
|
+
export type NotificationCategory = 'consent' | 'request' | 'system';
|
|
546
|
+
|
|
547
|
+
export type NotificationCTA = {
|
|
548
|
+
label: string;
|
|
549
|
+
style: 'primary' | 'secondary' | 'danger';
|
|
550
|
+
action_type: 'link' | 'acknowledge';
|
|
551
|
+
url?: string;
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
export type Notification = {
|
|
555
|
+
uuid: string;
|
|
556
|
+
notification_type: NotificationType;
|
|
557
|
+
category: NotificationCategory;
|
|
558
|
+
title: string;
|
|
559
|
+
description: string;
|
|
560
|
+
is_read: boolean;
|
|
561
|
+
is_acknowledged: boolean;
|
|
562
|
+
source_type: string;
|
|
563
|
+
source_uuid: string | null;
|
|
564
|
+
request_type: string;
|
|
565
|
+
request_type_display?: string | null;
|
|
566
|
+
status_label: string;
|
|
567
|
+
status_label_display?: string | null;
|
|
568
|
+
ctas: NotificationCTA[];
|
|
569
|
+
created_at: string;
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
export type NotificationSummary = {
|
|
573
|
+
total: number;
|
|
574
|
+
unread: number;
|
|
575
|
+
action_needed: number;
|
|
576
|
+
acknowledged: number;
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
export type NotificationListResponse = {
|
|
580
|
+
data: Notification[];
|
|
581
|
+
total: number;
|
|
582
|
+
skip: number;
|
|
583
|
+
limit: number;
|
|
584
|
+
summary: NotificationSummary | null;
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
export type UnreadCountResponse = {
|
|
588
|
+
count: number;
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
export type NotificationActionResponse = {
|
|
592
|
+
message: string;
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
export type NotificationActionBaseParams = {
|
|
596
|
+
organizationId: string;
|
|
597
|
+
workspaceId: string;
|
|
598
|
+
authToken?: string;
|
|
599
|
+
consentClient?: ReturnType<typeof import('../api/client').createConsentClient>;
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
export type StatCardProps = Readonly<{
|
|
603
|
+
value: number; label: string;
|
|
604
|
+
}>
|
|
605
|
+
|
|
606
|
+
export type NotificationDropdownProps = Readonly<{
|
|
607
|
+
notifications: Notification[];
|
|
608
|
+
onMarkAllRead: () => void;
|
|
609
|
+
onRefresh: () => void;
|
|
610
|
+
isRefreshing: boolean;
|
|
611
|
+
onViewAll: () => void;
|
|
612
|
+
onNotificationClick: (notification: Notification) => void;
|
|
613
|
+
onCTAClick: (notification: Notification, cta: NotificationCTA) => void;
|
|
614
|
+
}>;
|
|
615
|
+
|
|
616
|
+
export type NotificationListPanelProps = Readonly<{
|
|
617
|
+
notifications: Notification[];
|
|
618
|
+
totalCount: number;
|
|
619
|
+
activeTab: FilterTab;
|
|
620
|
+
onTabChange: (tab: FilterTab) => void;
|
|
621
|
+
skip: number;
|
|
622
|
+
onSkipChange: (skip: number) => void;
|
|
623
|
+
isLoading: boolean;
|
|
624
|
+
isRefreshing: boolean;
|
|
625
|
+
onRefresh: () => void;
|
|
626
|
+
onMarkAllRead: () => void;
|
|
627
|
+
onNotificationClick: (notification: Notification) => void;
|
|
628
|
+
onCTAClick: (notification: Notification, cta: NotificationCTA) => void;
|
|
629
|
+
}>;
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
export type NotificationRowProps = Readonly<{
|
|
633
|
+
notification: Notification;
|
|
634
|
+
onClick: () => void;
|
|
635
|
+
onCTAClick: (cta: NotificationCTA) => void;
|
|
636
|
+
}>;
|
|
637
|
+
|
|
638
|
+
export type NotificationIconProps = Readonly<{
|
|
639
|
+
notificationType: NotificationType;
|
|
640
|
+
size?: 'sm' | 'lg';
|
|
641
|
+
}>;
|
|
642
|
+
|
|
643
|
+
export type NotificationCTAButtonProps = Readonly<{
|
|
644
|
+
cta: NotificationCTA;
|
|
645
|
+
onClick: (e: React.MouseEvent) => void;
|
|
646
|
+
size?: 'sm' | 'lg';
|
|
647
|
+
}>;
|
|
648
|
+
|
|
649
|
+
export type FilterTab = 'all' | 'consent' | 'request' | 'system';
|
|
650
|
+
|
|
651
|
+
export type IconCategory = 'guardian' | 'consent' | 'access' | 'withdrawal' | 'grievance' | 'system';
|
|
652
|
+
|
|
653
|
+
export type NotificationDropdownItemProps = Readonly<{
|
|
654
|
+
notification: Notification;
|
|
655
|
+
onNotificationClick: (notification: Notification) => void;
|
|
656
|
+
onCTAClick: (notification: Notification, cta: NotificationCTA) => void;
|
|
657
|
+
}>
|
|
658
|
+
|
|
659
|
+
export type NotificationQueryParams = {
|
|
660
|
+
category?: NotificationCategory;
|
|
661
|
+
notification_type?: NotificationType;
|
|
662
|
+
is_read?: boolean;
|
|
663
|
+
skip?: number;
|
|
664
|
+
limit?: number;
|
|
665
|
+
include_summary?: boolean;
|
|
666
|
+
language?: string;
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
export type NotificationClient = NonNullable<NotificationActionBaseParams['consentClient']>;
|
|
670
|
+
|
|
671
|
+
export type GetNotificationsServerParams = NotificationActionBaseParams & NotificationQueryParams;
|
|
672
|
+
|
|
673
|
+
export type NotificationByUuidServerParams = NotificationActionBaseParams & { notificationUuid: string };
|
|
674
|
+
|
|
675
|
+
export type NotificationRefreshButtonProps = Readonly<{
|
|
676
|
+
isRefreshing: boolean;
|
|
677
|
+
onRefresh: () => void;
|
|
678
|
+
iconSize?: number;
|
|
679
|
+
}>;
|
|
@@ -251,5 +251,31 @@
|
|
|
251
251
|
"document": "Document",
|
|
252
252
|
"downloadAttachment": "Download {{fileName}}",
|
|
253
253
|
"sortByColumn": "Sort by column",
|
|
254
|
-
"failedToFetchActivityData": "Failed to fetch activity data"
|
|
254
|
+
"failedToFetchActivityData": "Failed to fetch activity data",
|
|
255
|
+
"acknowledged": "Acknowledged",
|
|
256
|
+
"actionNeeded": "Action needed",
|
|
257
|
+
"all": "All",
|
|
258
|
+
"backToManageConsent": "Back to manage consent",
|
|
259
|
+
"consent": "Consent",
|
|
260
|
+
"daysAgo": "{{count}}d ago",
|
|
261
|
+
"hoursAgo": "{{count}}h ago",
|
|
262
|
+
"justNow": "Just now",
|
|
263
|
+
"loadingNotifications": "Loading notifications...",
|
|
264
|
+
"markAllAsRead": "Mark all as read",
|
|
265
|
+
"minutesAgo": "{{count}}m ago",
|
|
266
|
+
"next": "Next",
|
|
267
|
+
"noNotificationsInCategory": "No notifications in this category.",
|
|
268
|
+
"noNotificationsYet": "No notifications yet.",
|
|
269
|
+
"notificationHub": "Notification Hub",
|
|
270
|
+
"notificationHubSubtitle": "All your privacy-related notifications in one place",
|
|
271
|
+
"notifications": "Notifications",
|
|
272
|
+
"paginationRange": "{{start}}–{{end}} of {{total}}",
|
|
273
|
+
"previous": "Previous",
|
|
274
|
+
"refreshNotifications": "Refresh notifications",
|
|
275
|
+
"requests": "Requests",
|
|
276
|
+
"system": "System",
|
|
277
|
+
"total": "Total",
|
|
278
|
+
"unread": "Unread",
|
|
279
|
+
"viewAllNotifications": "View all notifications",
|
|
280
|
+
"weeksAgo": "{{count}}w ago"
|
|
255
281
|
}
|