@tantainnovative/ndpr-toolkit 1.0.1 → 1.0.3
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/.claude/settings.local.json +20 -0
- package/.eslintrc.json +10 -0
- package/.github/workflows/ci.yml +36 -0
- package/.github/workflows/nextjs.yml +104 -0
- package/.husky/commit-msg +4 -0
- package/.husky/pre-commit +4 -0
- package/.lintstagedrc.js +4 -0
- package/.nvmrc +1 -0
- package/.versionrc +17 -0
- package/CHANGELOG.md +16 -0
- package/CLAUDE.md +90 -0
- package/CNAME +1 -0
- package/CONTRIBUTING.md +87 -0
- package/README.md +84 -431
- package/RELEASE-NOTES-v1.0.0.md +140 -0
- package/RELEASE-NOTES-v1.0.1.md +69 -0
- package/SECURITY.md +21 -0
- package/commitlint.config.js +36 -0
- package/components.json +21 -0
- package/eslint.config.mjs +16 -0
- package/jest.config.js +31 -0
- package/jest.setup.js +15 -0
- package/next.config.js +15 -0
- package/next.config.ts +62 -0
- package/package.json +70 -52
- package/packages/ndpr-toolkit/README.md +467 -0
- package/packages/ndpr-toolkit/jest.config.js +23 -0
- package/packages/ndpr-toolkit/package-lock.json +8197 -0
- package/packages/ndpr-toolkit/package.json +71 -0
- package/packages/ndpr-toolkit/rollup.config.js +34 -0
- package/packages/ndpr-toolkit/src/__tests__/components/consent/ConsentBanner.test.tsx +119 -0
- package/packages/ndpr-toolkit/src/__tests__/components/consent/ConsentManager.test.tsx +122 -0
- package/packages/ndpr-toolkit/src/__tests__/components/consent/ConsentStorage.test.tsx +270 -0
- package/packages/ndpr-toolkit/src/__tests__/components/dsr/DSRDashboard.test.tsx +199 -0
- package/packages/ndpr-toolkit/src/__tests__/components/dsr/DSRRequestForm.test.tsx +224 -0
- package/packages/ndpr-toolkit/src/__tests__/components/dsr/DSRTracker.test.tsx +104 -0
- package/packages/ndpr-toolkit/src/__tests__/hooks/useConsent.test.tsx +161 -0
- package/packages/ndpr-toolkit/src/__tests__/hooks/useDSR.test.tsx +330 -0
- package/packages/ndpr-toolkit/src/__tests__/utils/breach.test.ts +149 -0
- package/packages/ndpr-toolkit/src/__tests__/utils/consent.test.ts +88 -0
- package/packages/ndpr-toolkit/src/__tests__/utils/dpia.test.ts +160 -0
- package/packages/ndpr-toolkit/src/__tests__/utils/dsr.test.ts +110 -0
- package/packages/ndpr-toolkit/src/__tests__/utils/privacy.test.ts +97 -0
- package/packages/ndpr-toolkit/src/components/breach/BreachNotificationManager.tsx +701 -0
- package/packages/ndpr-toolkit/src/components/breach/BreachReportForm.tsx +631 -0
- package/packages/ndpr-toolkit/src/components/breach/BreachRiskAssessment.tsx +569 -0
- package/packages/ndpr-toolkit/src/components/breach/RegulatoryReportGenerator.tsx +496 -0
- package/packages/ndpr-toolkit/src/components/consent/ConsentBanner.tsx +270 -0
- package/packages/ndpr-toolkit/src/components/consent/ConsentManager.tsx +217 -0
- package/packages/ndpr-toolkit/src/components/consent/ConsentStorage.tsx +206 -0
- package/packages/ndpr-toolkit/src/components/dpia/DPIAQuestionnaire.tsx +342 -0
- package/packages/ndpr-toolkit/src/components/dpia/DPIAReport.tsx +373 -0
- package/packages/ndpr-toolkit/src/components/dpia/StepIndicator.tsx +174 -0
- package/packages/ndpr-toolkit/src/components/dsr/DSRDashboard.tsx +717 -0
- package/packages/ndpr-toolkit/src/components/dsr/DSRRequestForm.tsx +476 -0
- package/packages/ndpr-toolkit/src/components/dsr/DSRTracker.tsx +620 -0
- package/packages/ndpr-toolkit/src/components/policy/PolicyExporter.tsx +541 -0
- package/packages/ndpr-toolkit/src/components/policy/PolicyGenerator.tsx +454 -0
- package/packages/ndpr-toolkit/src/components/policy/PolicyPreview.tsx +333 -0
- package/packages/ndpr-toolkit/src/hooks/useBreach.ts +409 -0
- package/packages/ndpr-toolkit/src/hooks/useConsent.ts +263 -0
- package/packages/ndpr-toolkit/src/hooks/useDPIA.ts +457 -0
- package/packages/ndpr-toolkit/src/hooks/useDSR.ts +236 -0
- package/packages/ndpr-toolkit/src/hooks/usePrivacyPolicy.ts +428 -0
- package/{dist/index.d.ts → packages/ndpr-toolkit/src/index.ts} +14 -1
- package/packages/ndpr-toolkit/src/setupTests.ts +5 -0
- package/packages/ndpr-toolkit/src/types/breach.ts +283 -0
- package/packages/ndpr-toolkit/src/types/consent.ts +111 -0
- package/packages/ndpr-toolkit/src/types/dpia.ts +236 -0
- package/packages/ndpr-toolkit/src/types/dsr.ts +192 -0
- package/packages/ndpr-toolkit/src/types/index.ts +42 -0
- package/packages/ndpr-toolkit/src/types/privacy.ts +246 -0
- package/packages/ndpr-toolkit/src/utils/breach.ts +122 -0
- package/packages/ndpr-toolkit/src/utils/consent.ts +51 -0
- package/packages/ndpr-toolkit/src/utils/dpia.ts +104 -0
- package/packages/ndpr-toolkit/src/utils/dsr.ts +77 -0
- package/packages/ndpr-toolkit/src/utils/privacy.ts +100 -0
- package/packages/ndpr-toolkit/tsconfig.json +23 -0
- package/postcss.config.mjs +5 -0
- package/public/NDPR TOOLKIT.svg +1 -0
- package/public/favicon/android-chrome-192x192.png +0 -0
- package/public/favicon/android-chrome-512x512.png +0 -0
- package/public/favicon/apple-touch-icon.png +0 -0
- package/public/favicon/favicon-16x16.png +0 -0
- package/public/favicon/favicon-32x32.png +0 -0
- package/public/favicon/site.webmanifest +1 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/ndpr-toolkit-logo.svg +108 -0
- package/public/next.svg +1 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/src/__tests__/example.test.ts +13 -0
- package/src/__tests__/requestService.test.ts +57 -0
- package/src/app/accessibility.css +70 -0
- package/src/app/docs/components/DocLayout.tsx +267 -0
- package/src/app/docs/components/breach-notification/page.tsx +797 -0
- package/src/app/docs/components/consent-management/page.tsx +576 -0
- package/src/app/docs/components/data-subject-rights/page.tsx +511 -0
- package/src/app/docs/components/dpia-questionnaire/layout.tsx +15 -0
- package/src/app/docs/components/dpia-questionnaire/metadata.ts +31 -0
- package/src/app/docs/components/dpia-questionnaire/page.tsx +666 -0
- package/src/app/docs/components/hooks/page.tsx +305 -0
- package/src/app/docs/components/page.tsx +84 -0
- package/src/app/docs/components/privacy-policy-generator/page.tsx +634 -0
- package/src/app/docs/guides/breach-notification-process/components/BestPractices.tsx +123 -0
- package/src/app/docs/guides/breach-notification-process/components/ImplementationSteps.tsx +328 -0
- package/src/app/docs/guides/breach-notification-process/components/Introduction.tsx +28 -0
- package/src/app/docs/guides/breach-notification-process/components/NotificationTimeline.tsx +91 -0
- package/src/app/docs/guides/breach-notification-process/components/Resources.tsx +118 -0
- package/src/app/docs/guides/breach-notification-process/page.tsx +39 -0
- package/src/app/docs/guides/conducting-dpia/page.tsx +593 -0
- package/src/app/docs/guides/data-subject-requests/page.tsx +666 -0
- package/src/app/docs/guides/managing-consent/page.tsx +738 -0
- package/src/app/docs/guides/ndpr-compliance-checklist/components/ComplianceChecklist.tsx +296 -0
- package/src/app/docs/guides/ndpr-compliance-checklist/components/ImplementationTools.tsx +145 -0
- package/src/app/docs/guides/ndpr-compliance-checklist/components/Introduction.tsx +33 -0
- package/src/app/docs/guides/ndpr-compliance-checklist/components/KeyRequirements.tsx +99 -0
- package/src/app/docs/guides/ndpr-compliance-checklist/components/Resources.tsx +159 -0
- package/src/app/docs/guides/ndpr-compliance-checklist/page.tsx +38 -0
- package/src/app/docs/guides/page.tsx +67 -0
- package/src/app/docs/layout.tsx +15 -0
- package/src/app/docs/metadata.ts +31 -0
- package/src/app/docs/page.tsx +572 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +123 -0
- package/src/app/layout.tsx +37 -0
- package/src/app/ndpr-demos/breach/page.tsx +354 -0
- package/src/app/ndpr-demos/consent/page.tsx +366 -0
- package/src/app/ndpr-demos/dpia/page.tsx +495 -0
- package/src/app/ndpr-demos/dsr/page.tsx +280 -0
- package/src/app/ndpr-demos/page.tsx +73 -0
- package/src/app/ndpr-demos/policy/page.tsx +771 -0
- package/src/app/page.tsx +452 -0
- package/src/components/ErrorBoundary.tsx +90 -0
- package/src/components/breach-notification/BreachNotificationForm.tsx +479 -0
- package/src/components/consent/ConsentBanner.tsx +159 -0
- package/src/components/data-subject-rights/DataSubjectRequestForm.tsx +419 -0
- package/src/components/docs/DocLayout.tsx +289 -0
- package/src/components/docs/index.ts +2 -0
- package/src/components/dpia/DPIAQuestionnaire.tsx +483 -0
- package/src/components/privacy-policy/PolicyGenerator.tsx +1062 -0
- package/src/components/privacy-policy/data.ts +98 -0
- package/src/components/privacy-policy/shared/CheckboxField.tsx +38 -0
- package/src/components/privacy-policy/shared/CheckboxGroup.tsx +85 -0
- package/src/components/privacy-policy/shared/FormField.tsx +79 -0
- package/src/components/privacy-policy/shared/StepIndicator.tsx +86 -0
- package/src/components/privacy-policy/steps/CustomSectionsStep.tsx +335 -0
- package/src/components/privacy-policy/steps/DataCollectionStep.tsx +231 -0
- package/src/components/privacy-policy/steps/DataSharingStep.tsx +418 -0
- package/src/components/privacy-policy/steps/OrganizationInfoStep.tsx +202 -0
- package/src/components/privacy-policy/steps/PolicyPreviewStep.tsx +172 -0
- package/src/components/ui/Badge.tsx +46 -0
- package/src/components/ui/Button.tsx +59 -0
- package/src/components/ui/Card.tsx +92 -0
- package/src/components/ui/Checkbox.tsx +57 -0
- package/src/components/ui/FormField.tsx +50 -0
- package/src/components/ui/Input.tsx +38 -0
- package/src/components/ui/Loading.tsx +201 -0
- package/src/components/ui/Select.tsx +42 -0
- package/src/components/ui/TextArea.tsx +38 -0
- package/src/components/ui/label.tsx +24 -0
- package/src/components/ui/switch.tsx +31 -0
- package/src/components/ui/tabs.tsx +66 -0
- package/src/hooks/useConsent.ts +64 -0
- package/src/hooks/useLoadingState.ts +85 -0
- package/src/lib/consentService.ts +137 -0
- package/src/lib/dpiaQuestions.ts +148 -0
- package/src/lib/requestService.ts +75 -0
- package/src/lib/sanitize.ts +108 -0
- package/src/lib/storage.ts +222 -0
- package/src/lib/utils.ts +6 -0
- package/src/types/html-to-docx.d.ts +30 -0
- package/src/types/index.ts +72 -0
- package/tailwind.config.ts +65 -0
- package/tsconfig.json +41 -0
- package/dist/components/breach/BreachNotificationManager.d.ts +0 -62
- package/dist/components/breach/BreachReportForm.d.ts +0 -66
- package/dist/components/breach/BreachRiskAssessment.d.ts +0 -50
- package/dist/components/breach/RegulatoryReportGenerator.d.ts +0 -94
- package/dist/components/consent/ConsentBanner.d.ts +0 -79
- package/dist/components/consent/ConsentManager.d.ts +0 -73
- package/dist/components/consent/ConsentStorage.d.ts +0 -41
- package/dist/components/dpia/DPIAQuestionnaire.d.ts +0 -70
- package/dist/components/dpia/DPIAReport.d.ts +0 -40
- package/dist/components/dpia/StepIndicator.d.ts +0 -64
- package/dist/components/dsr/DSRDashboard.d.ts +0 -58
- package/dist/components/dsr/DSRRequestForm.d.ts +0 -74
- package/dist/components/dsr/DSRTracker.d.ts +0 -56
- package/dist/components/policy/PolicyExporter.d.ts +0 -65
- package/dist/components/policy/PolicyGenerator.d.ts +0 -54
- package/dist/components/policy/PolicyPreview.d.ts +0 -71
- package/dist/hooks/useBreach.d.ts +0 -97
- package/dist/hooks/useConsent.d.ts +0 -63
- package/dist/hooks/useDPIA.d.ts +0 -92
- package/dist/hooks/useDSR.d.ts +0 -72
- package/dist/hooks/usePrivacyPolicy.d.ts +0 -87
- package/dist/index.esm.js +0 -2
- package/dist/index.esm.js.map +0 -1
- package/dist/index.js +0 -2
- package/dist/index.js.map +0 -1
- package/dist/setupTests.d.ts +0 -2
- package/dist/types/breach.d.ts +0 -239
- package/dist/types/consent.d.ts +0 -95
- package/dist/types/dpia.d.ts +0 -196
- package/dist/types/dsr.d.ts +0 -162
- package/dist/types/privacy.d.ts +0 -204
- package/dist/utils/breach.d.ts +0 -14
- package/dist/utils/consent.d.ts +0 -10
- package/dist/utils/dpia.d.ts +0 -12
- package/dist/utils/dsr.d.ts +0 -11
- package/dist/utils/privacy.d.ts +0 -12
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { DSRRequest, DSRStatus, DSRType } from '../../types/dsr';
|
|
3
|
+
|
|
4
|
+
export interface DSRTrackerProps {
|
|
5
|
+
/**
|
|
6
|
+
* List of DSR requests to track
|
|
7
|
+
*/
|
|
8
|
+
requests: DSRRequest[];
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Callback function called when a request is selected
|
|
12
|
+
*/
|
|
13
|
+
onSelectRequest?: (requestId: string) => void;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Title displayed on the tracker
|
|
17
|
+
* @default "DSR Request Tracker"
|
|
18
|
+
*/
|
|
19
|
+
title?: string;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Description text displayed on the tracker
|
|
23
|
+
* @default "Track the status and progress of data subject requests."
|
|
24
|
+
*/
|
|
25
|
+
description?: string;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Custom CSS class for the tracker
|
|
29
|
+
*/
|
|
30
|
+
className?: string;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Custom CSS class for the buttons
|
|
34
|
+
*/
|
|
35
|
+
buttonClassName?: string;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Whether to show the summary statistics
|
|
39
|
+
* @default true
|
|
40
|
+
*/
|
|
41
|
+
showSummaryStats?: boolean;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Whether to show the request type breakdown
|
|
45
|
+
* @default true
|
|
46
|
+
*/
|
|
47
|
+
showTypeBreakdown?: boolean;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Whether to show the status breakdown
|
|
51
|
+
* @default true
|
|
52
|
+
*/
|
|
53
|
+
showStatusBreakdown?: boolean;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Whether to show the timeline chart
|
|
57
|
+
* @default true
|
|
58
|
+
*/
|
|
59
|
+
showTimelineChart?: boolean;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Whether to show the overdue requests
|
|
63
|
+
* @default true
|
|
64
|
+
*/
|
|
65
|
+
showOverdueRequests?: boolean;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const DSRTracker: React.FC<DSRTrackerProps> = ({
|
|
69
|
+
requests,
|
|
70
|
+
onSelectRequest,
|
|
71
|
+
title = "DSR Request Tracker",
|
|
72
|
+
description = "Track the status and progress of data subject requests.",
|
|
73
|
+
className = "",
|
|
74
|
+
buttonClassName = "",
|
|
75
|
+
showSummaryStats = true,
|
|
76
|
+
showTypeBreakdown = true,
|
|
77
|
+
showStatusBreakdown = true,
|
|
78
|
+
showTimelineChart = true,
|
|
79
|
+
showOverdueRequests = true
|
|
80
|
+
}) => {
|
|
81
|
+
const [selectedTimeframe, setSelectedTimeframe] = useState<'7days' | '30days' | '90days' | 'all'>('30days');
|
|
82
|
+
const [filteredRequests, setFilteredRequests] = useState<DSRRequest[]>(requests);
|
|
83
|
+
const [overdueRequests, setOverdueRequests] = useState<DSRRequest[]>([]);
|
|
84
|
+
const [upcomingDeadlines, setUpcomingDeadlines] = useState<DSRRequest[]>([]);
|
|
85
|
+
|
|
86
|
+
// Filter requests based on selected timeframe
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
const now = Date.now();
|
|
89
|
+
let filtered: DSRRequest[];
|
|
90
|
+
|
|
91
|
+
switch (selectedTimeframe) {
|
|
92
|
+
case '7days':
|
|
93
|
+
filtered = requests.filter(request => (now - request.createdAt) <= 7 * 24 * 60 * 60 * 1000);
|
|
94
|
+
break;
|
|
95
|
+
case '30days':
|
|
96
|
+
filtered = requests.filter(request => (now - request.createdAt) <= 30 * 24 * 60 * 60 * 1000);
|
|
97
|
+
break;
|
|
98
|
+
case '90days':
|
|
99
|
+
filtered = requests.filter(request => (now - request.createdAt) <= 90 * 24 * 60 * 60 * 1000);
|
|
100
|
+
break;
|
|
101
|
+
default:
|
|
102
|
+
filtered = [...requests];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
setFilteredRequests(filtered);
|
|
106
|
+
|
|
107
|
+
// Find overdue requests
|
|
108
|
+
const overdue = filtered.filter(request =>
|
|
109
|
+
request.dueDate &&
|
|
110
|
+
now > request.dueDate &&
|
|
111
|
+
request.status !== 'completed' &&
|
|
112
|
+
request.status !== 'rejected'
|
|
113
|
+
);
|
|
114
|
+
setOverdueRequests(overdue);
|
|
115
|
+
|
|
116
|
+
// Find upcoming deadlines (due in the next 7 days)
|
|
117
|
+
const upcoming = filtered.filter(request =>
|
|
118
|
+
request.dueDate &&
|
|
119
|
+
now < request.dueDate &&
|
|
120
|
+
(request.dueDate - now) <= 7 * 24 * 60 * 60 * 1000 &&
|
|
121
|
+
request.status !== 'completed' &&
|
|
122
|
+
request.status !== 'rejected'
|
|
123
|
+
);
|
|
124
|
+
setUpcomingDeadlines(upcoming);
|
|
125
|
+
}, [requests, selectedTimeframe]);
|
|
126
|
+
|
|
127
|
+
// Handle request selection
|
|
128
|
+
const handleSelectRequest = (requestId: string) => {
|
|
129
|
+
if (onSelectRequest) {
|
|
130
|
+
onSelectRequest(requestId);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// Format a date from timestamp
|
|
135
|
+
const formatDate = (timestamp: number): string => {
|
|
136
|
+
return new Date(timestamp).toLocaleDateString();
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// Calculate days remaining until deadline
|
|
140
|
+
const calculateDaysRemaining = (dueDate: number): number => {
|
|
141
|
+
const now = Date.now();
|
|
142
|
+
const remaining = (dueDate - now) / (24 * 60 * 60 * 1000);
|
|
143
|
+
return Math.ceil(remaining);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// Calculate average response time in days
|
|
147
|
+
const calculateAverageResponseTime = (): number | null => {
|
|
148
|
+
const completedRequests = filteredRequests.filter(request =>
|
|
149
|
+
request.status === 'completed' && request.completedAt && request.createdAt
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
if (completedRequests.length === 0) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const totalDays = completedRequests.reduce((sum, request) => {
|
|
157
|
+
const responseTime = ((request.completedAt || 0) - request.createdAt) / (24 * 60 * 60 * 1000);
|
|
158
|
+
return sum + responseTime;
|
|
159
|
+
}, 0);
|
|
160
|
+
|
|
161
|
+
return Number((totalDays / completedRequests.length).toFixed(1));
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// Calculate compliance rate (percentage of requests completed within deadline)
|
|
165
|
+
const calculateComplianceRate = (): number | null => {
|
|
166
|
+
const completedRequests = filteredRequests.filter(request =>
|
|
167
|
+
(request.status === 'completed' || request.status === 'rejected') &&
|
|
168
|
+
request.completedAt &&
|
|
169
|
+
request.dueDate
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
if (completedRequests.length === 0) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const compliantRequests = completedRequests.filter(request =>
|
|
177
|
+
(request.completedAt || 0) <= (request.dueDate || 0)
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
return Math.round((compliantRequests.length / completedRequests.length) * 100);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
// Count requests by type
|
|
184
|
+
const countRequestsByType = (): Record<DSRType, number> => {
|
|
185
|
+
const counts: Partial<Record<DSRType, number>> = {};
|
|
186
|
+
|
|
187
|
+
filteredRequests.forEach(request => {
|
|
188
|
+
counts[request.type] = (counts[request.type] || 0) + 1;
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
access: counts.access || 0,
|
|
193
|
+
rectification: counts.rectification || 0,
|
|
194
|
+
erasure: counts.erasure || 0,
|
|
195
|
+
restriction: counts.restriction || 0,
|
|
196
|
+
portability: counts.portability || 0,
|
|
197
|
+
objection: counts.objection || 0
|
|
198
|
+
};
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
// Count requests by status
|
|
202
|
+
const countRequestsByStatus = (): Record<DSRStatus, number> => {
|
|
203
|
+
const counts: Partial<Record<DSRStatus, number>> = {};
|
|
204
|
+
|
|
205
|
+
filteredRequests.forEach(request => {
|
|
206
|
+
counts[request.status] = (counts[request.status] || 0) + 1;
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
pending: counts.pending || 0,
|
|
211
|
+
awaitingVerification: counts.awaitingVerification || 0,
|
|
212
|
+
inProgress: counts.inProgress || 0,
|
|
213
|
+
completed: counts.completed || 0,
|
|
214
|
+
rejected: counts.rejected || 0
|
|
215
|
+
};
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
// Render type badge
|
|
219
|
+
const renderTypeBadge = (type: DSRType) => {
|
|
220
|
+
const colorClasses = {
|
|
221
|
+
access: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200',
|
|
222
|
+
rectification: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
|
|
223
|
+
erasure: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
|
|
224
|
+
restriction: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200',
|
|
225
|
+
portability: 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200',
|
|
226
|
+
objection: 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200'
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
return (
|
|
230
|
+
<span className={`px-2 py-1 rounded text-xs font-medium ${colorClasses[type]}`}>
|
|
231
|
+
{type.charAt(0).toUpperCase() + type.slice(1)}
|
|
232
|
+
</span>
|
|
233
|
+
);
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
// Render status badge
|
|
237
|
+
const renderStatusBadge = (status: DSRStatus) => {
|
|
238
|
+
const colorClasses = {
|
|
239
|
+
pending: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200',
|
|
240
|
+
inProgress: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200',
|
|
241
|
+
completed: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
|
|
242
|
+
rejected: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
|
|
243
|
+
awaitingVerification: 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200'
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
return (
|
|
247
|
+
<span className={`px-2 py-1 rounded text-xs font-medium ${colorClasses[status]}`}>
|
|
248
|
+
{status === 'inProgress' ? 'In Progress' :
|
|
249
|
+
status === 'awaitingVerification' ? 'Awaiting Verification' :
|
|
250
|
+
status.charAt(0).toUpperCase() + status.slice(1)}
|
|
251
|
+
</span>
|
|
252
|
+
);
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
// Render the summary statistics
|
|
256
|
+
const renderSummaryStats = () => {
|
|
257
|
+
const totalRequests = filteredRequests.length;
|
|
258
|
+
const pendingRequests = filteredRequests.filter(request =>
|
|
259
|
+
request.status !== 'completed' && request.status !== 'rejected'
|
|
260
|
+
).length;
|
|
261
|
+
const completedRequests = filteredRequests.filter(request =>
|
|
262
|
+
request.status === 'completed' || request.status === 'rejected'
|
|
263
|
+
).length;
|
|
264
|
+
const averageResponseTime = calculateAverageResponseTime();
|
|
265
|
+
const complianceRate = calculateComplianceRate();
|
|
266
|
+
|
|
267
|
+
return (
|
|
268
|
+
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
|
269
|
+
<div className="bg-white dark:bg-gray-700 p-4 rounded-lg shadow">
|
|
270
|
+
<h4 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-1">Total Requests</h4>
|
|
271
|
+
<p className="text-2xl font-bold">{totalRequests}</p>
|
|
272
|
+
</div>
|
|
273
|
+
|
|
274
|
+
<div className="bg-white dark:bg-gray-700 p-4 rounded-lg shadow">
|
|
275
|
+
<h4 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-1">Pending Requests</h4>
|
|
276
|
+
<p className="text-2xl font-bold">{pendingRequests}</p>
|
|
277
|
+
</div>
|
|
278
|
+
|
|
279
|
+
<div className="bg-white dark:bg-gray-700 p-4 rounded-lg shadow">
|
|
280
|
+
<h4 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-1">Avg. Response Time</h4>
|
|
281
|
+
<p className="text-2xl font-bold">{averageResponseTime !== null ? `${averageResponseTime} days` : 'N/A'}</p>
|
|
282
|
+
</div>
|
|
283
|
+
|
|
284
|
+
<div className="bg-white dark:bg-gray-700 p-4 rounded-lg shadow">
|
|
285
|
+
<h4 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-1">Compliance Rate</h4>
|
|
286
|
+
<p className="text-2xl font-bold">{complianceRate !== null ? `${complianceRate}%` : 'N/A'}</p>
|
|
287
|
+
</div>
|
|
288
|
+
</div>
|
|
289
|
+
);
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
// Render the type breakdown
|
|
293
|
+
const renderTypeBreakdown = () => {
|
|
294
|
+
const typeCounts = countRequestsByType();
|
|
295
|
+
const totalRequests = filteredRequests.length;
|
|
296
|
+
|
|
297
|
+
// Sort types by count (descending)
|
|
298
|
+
const sortedTypes = Object.entries(typeCounts)
|
|
299
|
+
.sort(([, countA], [, countB]) => countB - countA)
|
|
300
|
+
.map(([type]) => type as DSRType);
|
|
301
|
+
|
|
302
|
+
return (
|
|
303
|
+
<div className="bg-white dark:bg-gray-700 p-4 rounded-lg shadow mb-6">
|
|
304
|
+
<h3 className="text-lg font-medium mb-4">Request Types</h3>
|
|
305
|
+
|
|
306
|
+
{totalRequests === 0 ? (
|
|
307
|
+
<p className="text-gray-500 dark:text-gray-400 text-sm">
|
|
308
|
+
No data available for the selected timeframe.
|
|
309
|
+
</p>
|
|
310
|
+
) : (
|
|
311
|
+
<div className="space-y-4">
|
|
312
|
+
{sortedTypes.map(type => {
|
|
313
|
+
const count = typeCounts[type];
|
|
314
|
+
const percentage = Math.round((count / totalRequests) * 100);
|
|
315
|
+
|
|
316
|
+
return (
|
|
317
|
+
<div key={type}>
|
|
318
|
+
<div className="flex justify-between items-center mb-1">
|
|
319
|
+
<div className="flex items-center">
|
|
320
|
+
{renderTypeBadge(type)}
|
|
321
|
+
<span className="ml-2 text-sm">{count} requests</span>
|
|
322
|
+
</div>
|
|
323
|
+
<span className="text-sm font-medium">{percentage}%</span>
|
|
324
|
+
</div>
|
|
325
|
+
<div className="w-full bg-gray-200 dark:bg-gray-600 rounded-full h-2.5">
|
|
326
|
+
<div
|
|
327
|
+
className="bg-blue-600 h-2.5 rounded-full"
|
|
328
|
+
style={{ width: `${percentage}%` }}
|
|
329
|
+
></div>
|
|
330
|
+
</div>
|
|
331
|
+
</div>
|
|
332
|
+
);
|
|
333
|
+
})}
|
|
334
|
+
</div>
|
|
335
|
+
)}
|
|
336
|
+
</div>
|
|
337
|
+
);
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
// Render the status breakdown
|
|
341
|
+
const renderStatusBreakdown = () => {
|
|
342
|
+
const statusCounts = countRequestsByStatus();
|
|
343
|
+
const totalRequests = filteredRequests.length;
|
|
344
|
+
|
|
345
|
+
// Define the order of statuses
|
|
346
|
+
const statusOrder: DSRStatus[] = ['pending', 'awaitingVerification', 'inProgress', 'completed', 'rejected'];
|
|
347
|
+
|
|
348
|
+
// Define colors for each status
|
|
349
|
+
const statusColors: Record<DSRStatus, string> = {
|
|
350
|
+
pending: 'bg-yellow-500',
|
|
351
|
+
awaitingVerification: 'bg-purple-500',
|
|
352
|
+
inProgress: 'bg-blue-500',
|
|
353
|
+
completed: 'bg-green-500',
|
|
354
|
+
rejected: 'bg-red-500'
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
return (
|
|
358
|
+
<div className="bg-white dark:bg-gray-700 p-4 rounded-lg shadow mb-6">
|
|
359
|
+
<h3 className="text-lg font-medium mb-4">Request Status</h3>
|
|
360
|
+
|
|
361
|
+
{totalRequests === 0 ? (
|
|
362
|
+
<p className="text-gray-500 dark:text-gray-400 text-sm">
|
|
363
|
+
No data available for the selected timeframe.
|
|
364
|
+
</p>
|
|
365
|
+
) : (
|
|
366
|
+
<div>
|
|
367
|
+
<div className="flex h-4 mb-2">
|
|
368
|
+
{statusOrder.map(status => {
|
|
369
|
+
const count = statusCounts[status];
|
|
370
|
+
const percentage = (count / totalRequests) * 100;
|
|
371
|
+
|
|
372
|
+
return percentage > 0 ? (
|
|
373
|
+
<div
|
|
374
|
+
key={status}
|
|
375
|
+
className={`${statusColors[status]} first:rounded-l-full last:rounded-r-full`}
|
|
376
|
+
style={{ width: `${percentage}%` }}
|
|
377
|
+
title={`${status}: ${count} (${Math.round(percentage)}%)`}
|
|
378
|
+
></div>
|
|
379
|
+
) : null;
|
|
380
|
+
})}
|
|
381
|
+
</div>
|
|
382
|
+
|
|
383
|
+
<div className="grid grid-cols-2 md:grid-cols-5 gap-2 mt-4">
|
|
384
|
+
{statusOrder.map(status => {
|
|
385
|
+
const count = statusCounts[status];
|
|
386
|
+
const percentage = Math.round((count / totalRequests) * 100);
|
|
387
|
+
|
|
388
|
+
return (
|
|
389
|
+
<div key={status} className="flex items-center">
|
|
390
|
+
<div className={`w-3 h-3 rounded-full ${statusColors[status]} mr-2`}></div>
|
|
391
|
+
<div>
|
|
392
|
+
<p className="text-xs font-medium">
|
|
393
|
+
{status === 'inProgress' ? 'In Progress' :
|
|
394
|
+
status === 'awaitingVerification' ? 'Awaiting Verification' :
|
|
395
|
+
status.charAt(0).toUpperCase() + status.slice(1)}
|
|
396
|
+
</p>
|
|
397
|
+
<p className="text-xs text-gray-500 dark:text-gray-400">{count} ({percentage}%)</p>
|
|
398
|
+
</div>
|
|
399
|
+
</div>
|
|
400
|
+
);
|
|
401
|
+
})}
|
|
402
|
+
</div>
|
|
403
|
+
</div>
|
|
404
|
+
)}
|
|
405
|
+
</div>
|
|
406
|
+
);
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
// Render the timeline chart (simplified version)
|
|
410
|
+
const renderTimelineChart = () => {
|
|
411
|
+
// Group requests by month
|
|
412
|
+
const requestsByMonth: Record<string, number> = {};
|
|
413
|
+
|
|
414
|
+
filteredRequests.forEach(request => {
|
|
415
|
+
const date = new Date(request.createdAt);
|
|
416
|
+
const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
|
|
417
|
+
|
|
418
|
+
requestsByMonth[monthKey] = (requestsByMonth[monthKey] || 0) + 1;
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// Sort months chronologically
|
|
422
|
+
const sortedMonths = Object.keys(requestsByMonth).sort();
|
|
423
|
+
|
|
424
|
+
// Get the last 6 months (or all if less than 6)
|
|
425
|
+
const displayMonths = sortedMonths.slice(-6);
|
|
426
|
+
|
|
427
|
+
// Find the maximum count for scaling
|
|
428
|
+
const maxCount = Math.max(...Object.values(requestsByMonth).filter(count => count > 0), 1);
|
|
429
|
+
|
|
430
|
+
// Format month for display
|
|
431
|
+
const formatMonth = (monthKey: string): string => {
|
|
432
|
+
const [year, month] = monthKey.split('-');
|
|
433
|
+
return `${month}/${year.slice(2)}`;
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
return (
|
|
437
|
+
<div className="bg-white dark:bg-gray-700 p-4 rounded-lg shadow mb-6">
|
|
438
|
+
<h3 className="text-lg font-medium mb-4">Request Timeline</h3>
|
|
439
|
+
|
|
440
|
+
{displayMonths.length === 0 ? (
|
|
441
|
+
<p className="text-gray-500 dark:text-gray-400 text-sm">
|
|
442
|
+
No data available for the selected timeframe.
|
|
443
|
+
</p>
|
|
444
|
+
) : (
|
|
445
|
+
<div className="h-40">
|
|
446
|
+
<div className="flex h-32 items-end justify-between space-x-2">
|
|
447
|
+
{displayMonths.map(month => {
|
|
448
|
+
const count = requestsByMonth[month];
|
|
449
|
+
const height = `${(count / maxCount) * 100}%`;
|
|
450
|
+
|
|
451
|
+
return (
|
|
452
|
+
<div key={month} className="flex flex-col items-center flex-1">
|
|
453
|
+
<div
|
|
454
|
+
className="w-full bg-blue-500 rounded-t"
|
|
455
|
+
style={{ height }}
|
|
456
|
+
title={`${formatMonth(month)}: ${count} requests`}
|
|
457
|
+
></div>
|
|
458
|
+
</div>
|
|
459
|
+
);
|
|
460
|
+
})}
|
|
461
|
+
</div>
|
|
462
|
+
|
|
463
|
+
<div className="flex justify-between mt-2">
|
|
464
|
+
{displayMonths.map(month => (
|
|
465
|
+
<div key={month} className="text-xs text-center">
|
|
466
|
+
{formatMonth(month)}
|
|
467
|
+
</div>
|
|
468
|
+
))}
|
|
469
|
+
</div>
|
|
470
|
+
</div>
|
|
471
|
+
)}
|
|
472
|
+
</div>
|
|
473
|
+
);
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
// Render overdue requests
|
|
477
|
+
const renderOverdueRequests = () => {
|
|
478
|
+
return (
|
|
479
|
+
<div className="bg-white dark:bg-gray-700 p-4 rounded-lg shadow mb-6">
|
|
480
|
+
<h3 className="text-lg font-medium mb-4">Overdue Requests</h3>
|
|
481
|
+
|
|
482
|
+
{overdueRequests.length === 0 ? (
|
|
483
|
+
<p className="text-gray-500 dark:text-gray-400 text-sm">
|
|
484
|
+
No overdue requests.
|
|
485
|
+
</p>
|
|
486
|
+
) : (
|
|
487
|
+
<div className="space-y-2 max-h-60 overflow-y-auto">
|
|
488
|
+
{overdueRequests.map(request => (
|
|
489
|
+
<div
|
|
490
|
+
key={request.id}
|
|
491
|
+
className="p-3 bg-red-50 dark:bg-red-900/20 rounded-md cursor-pointer hover:bg-red-100 dark:hover:bg-red-900/30"
|
|
492
|
+
onClick={() => handleSelectRequest(request.id)}
|
|
493
|
+
>
|
|
494
|
+
<div className="flex justify-between items-start mb-1">
|
|
495
|
+
<h4 className="font-medium text-sm">{request.subject.name}</h4>
|
|
496
|
+
{renderTypeBadge(request.type)}
|
|
497
|
+
</div>
|
|
498
|
+
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">
|
|
499
|
+
{request.subject.email}
|
|
500
|
+
</p>
|
|
501
|
+
<div className="flex justify-between items-center">
|
|
502
|
+
<p className="text-xs text-gray-500 dark:text-gray-400">
|
|
503
|
+
Due: {formatDate(request.dueDate || 0)}
|
|
504
|
+
</p>
|
|
505
|
+
<p className="text-xs font-bold text-red-600 dark:text-red-400">
|
|
506
|
+
Overdue by {Math.abs(calculateDaysRemaining(request.dueDate || 0))} days
|
|
507
|
+
</p>
|
|
508
|
+
</div>
|
|
509
|
+
</div>
|
|
510
|
+
))}
|
|
511
|
+
</div>
|
|
512
|
+
)}
|
|
513
|
+
</div>
|
|
514
|
+
);
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
// Render upcoming deadlines
|
|
518
|
+
const renderUpcomingDeadlines = () => {
|
|
519
|
+
return (
|
|
520
|
+
<div className="bg-white dark:bg-gray-700 p-4 rounded-lg shadow mb-6">
|
|
521
|
+
<h3 className="text-lg font-medium mb-4">Upcoming Deadlines</h3>
|
|
522
|
+
|
|
523
|
+
{upcomingDeadlines.length === 0 ? (
|
|
524
|
+
<p className="text-gray-500 dark:text-gray-400 text-sm">
|
|
525
|
+
No upcoming deadlines in the next 7 days.
|
|
526
|
+
</p>
|
|
527
|
+
) : (
|
|
528
|
+
<div className="space-y-2 max-h-60 overflow-y-auto">
|
|
529
|
+
{upcomingDeadlines.map(request => {
|
|
530
|
+
const daysRemaining = calculateDaysRemaining(request.dueDate || 0);
|
|
531
|
+
|
|
532
|
+
return (
|
|
533
|
+
<div
|
|
534
|
+
key={request.id}
|
|
535
|
+
className="p-3 bg-yellow-50 dark:bg-yellow-900/20 rounded-md cursor-pointer hover:bg-yellow-100 dark:hover:bg-yellow-900/30"
|
|
536
|
+
onClick={() => handleSelectRequest(request.id)}
|
|
537
|
+
>
|
|
538
|
+
<div className="flex justify-between items-start mb-1">
|
|
539
|
+
<h4 className="font-medium text-sm">{request.subject.name}</h4>
|
|
540
|
+
{renderTypeBadge(request.type)}
|
|
541
|
+
</div>
|
|
542
|
+
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">
|
|
543
|
+
{request.subject.email}
|
|
544
|
+
</p>
|
|
545
|
+
<div className="flex justify-between items-center">
|
|
546
|
+
<p className="text-xs text-gray-500 dark:text-gray-400">
|
|
547
|
+
Due: {formatDate(request.dueDate || 0)}
|
|
548
|
+
</p>
|
|
549
|
+
<p className={`text-xs font-bold ${
|
|
550
|
+
daysRemaining <= 3
|
|
551
|
+
? 'text-red-600 dark:text-red-400'
|
|
552
|
+
: 'text-yellow-600 dark:text-yellow-400'
|
|
553
|
+
}`}>
|
|
554
|
+
Due in {daysRemaining} days
|
|
555
|
+
</p>
|
|
556
|
+
</div>
|
|
557
|
+
</div>
|
|
558
|
+
);
|
|
559
|
+
})}
|
|
560
|
+
</div>
|
|
561
|
+
)}
|
|
562
|
+
</div>
|
|
563
|
+
);
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
return (
|
|
567
|
+
<div className={`bg-white dark:bg-gray-800 p-6 rounded-lg shadow-md ${className}`}>
|
|
568
|
+
<div className="flex flex-col md:flex-row md:justify-between md:items-center mb-6">
|
|
569
|
+
<div>
|
|
570
|
+
<h2 className="text-xl font-bold mb-2">{title}</h2>
|
|
571
|
+
<p className="text-gray-600 dark:text-gray-300">{description}</p>
|
|
572
|
+
</div>
|
|
573
|
+
|
|
574
|
+
<div className="mt-4 md:mt-0">
|
|
575
|
+
<label htmlFor="timeframe" className="block text-sm font-medium mb-1">
|
|
576
|
+
Timeframe
|
|
577
|
+
</label>
|
|
578
|
+
<select
|
|
579
|
+
id="timeframe"
|
|
580
|
+
value={selectedTimeframe}
|
|
581
|
+
onChange={e => setSelectedTimeframe(e.target.value as '7days' | '30days' | '90days' | 'all')}
|
|
582
|
+
className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
583
|
+
>
|
|
584
|
+
<option value="7days">Last 7 Days</option>
|
|
585
|
+
<option value="30days">Last 30 Days</option>
|
|
586
|
+
<option value="90days">Last 90 Days</option>
|
|
587
|
+
<option value="all">All Time</option>
|
|
588
|
+
</select>
|
|
589
|
+
</div>
|
|
590
|
+
</div>
|
|
591
|
+
|
|
592
|
+
{/* Summary Statistics */}
|
|
593
|
+
{showSummaryStats && renderSummaryStats()}
|
|
594
|
+
|
|
595
|
+
{/* Main Content */}
|
|
596
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
597
|
+
{/* Left Column */}
|
|
598
|
+
<div>
|
|
599
|
+
{/* Type Breakdown */}
|
|
600
|
+
{showTypeBreakdown && renderTypeBreakdown()}
|
|
601
|
+
|
|
602
|
+
{/* Status Breakdown */}
|
|
603
|
+
{showStatusBreakdown && renderStatusBreakdown()}
|
|
604
|
+
</div>
|
|
605
|
+
|
|
606
|
+
{/* Right Column */}
|
|
607
|
+
<div>
|
|
608
|
+
{/* Timeline Chart */}
|
|
609
|
+
{showTimelineChart && renderTimelineChart()}
|
|
610
|
+
|
|
611
|
+
{/* Overdue Requests */}
|
|
612
|
+
{showOverdueRequests && renderOverdueRequests()}
|
|
613
|
+
|
|
614
|
+
{/* Upcoming Deadlines */}
|
|
615
|
+
{showOverdueRequests && renderUpcomingDeadlines()}
|
|
616
|
+
</div>
|
|
617
|
+
</div>
|
|
618
|
+
</div>
|
|
619
|
+
);
|
|
620
|
+
};
|