@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.
Files changed (212) hide show
  1. package/.claude/settings.local.json +20 -0
  2. package/.eslintrc.json +10 -0
  3. package/.github/workflows/ci.yml +36 -0
  4. package/.github/workflows/nextjs.yml +104 -0
  5. package/.husky/commit-msg +4 -0
  6. package/.husky/pre-commit +4 -0
  7. package/.lintstagedrc.js +4 -0
  8. package/.nvmrc +1 -0
  9. package/.versionrc +17 -0
  10. package/CHANGELOG.md +16 -0
  11. package/CLAUDE.md +90 -0
  12. package/CNAME +1 -0
  13. package/CONTRIBUTING.md +87 -0
  14. package/README.md +84 -431
  15. package/RELEASE-NOTES-v1.0.0.md +140 -0
  16. package/RELEASE-NOTES-v1.0.1.md +69 -0
  17. package/SECURITY.md +21 -0
  18. package/commitlint.config.js +36 -0
  19. package/components.json +21 -0
  20. package/eslint.config.mjs +16 -0
  21. package/jest.config.js +31 -0
  22. package/jest.setup.js +15 -0
  23. package/next.config.js +15 -0
  24. package/next.config.ts +62 -0
  25. package/package.json +70 -52
  26. package/packages/ndpr-toolkit/README.md +467 -0
  27. package/packages/ndpr-toolkit/jest.config.js +23 -0
  28. package/packages/ndpr-toolkit/package-lock.json +8197 -0
  29. package/packages/ndpr-toolkit/package.json +71 -0
  30. package/packages/ndpr-toolkit/rollup.config.js +34 -0
  31. package/packages/ndpr-toolkit/src/__tests__/components/consent/ConsentBanner.test.tsx +119 -0
  32. package/packages/ndpr-toolkit/src/__tests__/components/consent/ConsentManager.test.tsx +122 -0
  33. package/packages/ndpr-toolkit/src/__tests__/components/consent/ConsentStorage.test.tsx +270 -0
  34. package/packages/ndpr-toolkit/src/__tests__/components/dsr/DSRDashboard.test.tsx +199 -0
  35. package/packages/ndpr-toolkit/src/__tests__/components/dsr/DSRRequestForm.test.tsx +224 -0
  36. package/packages/ndpr-toolkit/src/__tests__/components/dsr/DSRTracker.test.tsx +104 -0
  37. package/packages/ndpr-toolkit/src/__tests__/hooks/useConsent.test.tsx +161 -0
  38. package/packages/ndpr-toolkit/src/__tests__/hooks/useDSR.test.tsx +330 -0
  39. package/packages/ndpr-toolkit/src/__tests__/utils/breach.test.ts +149 -0
  40. package/packages/ndpr-toolkit/src/__tests__/utils/consent.test.ts +88 -0
  41. package/packages/ndpr-toolkit/src/__tests__/utils/dpia.test.ts +160 -0
  42. package/packages/ndpr-toolkit/src/__tests__/utils/dsr.test.ts +110 -0
  43. package/packages/ndpr-toolkit/src/__tests__/utils/privacy.test.ts +97 -0
  44. package/packages/ndpr-toolkit/src/components/breach/BreachNotificationManager.tsx +701 -0
  45. package/packages/ndpr-toolkit/src/components/breach/BreachReportForm.tsx +631 -0
  46. package/packages/ndpr-toolkit/src/components/breach/BreachRiskAssessment.tsx +569 -0
  47. package/packages/ndpr-toolkit/src/components/breach/RegulatoryReportGenerator.tsx +496 -0
  48. package/packages/ndpr-toolkit/src/components/consent/ConsentBanner.tsx +270 -0
  49. package/packages/ndpr-toolkit/src/components/consent/ConsentManager.tsx +217 -0
  50. package/packages/ndpr-toolkit/src/components/consent/ConsentStorage.tsx +206 -0
  51. package/packages/ndpr-toolkit/src/components/dpia/DPIAQuestionnaire.tsx +342 -0
  52. package/packages/ndpr-toolkit/src/components/dpia/DPIAReport.tsx +373 -0
  53. package/packages/ndpr-toolkit/src/components/dpia/StepIndicator.tsx +174 -0
  54. package/packages/ndpr-toolkit/src/components/dsr/DSRDashboard.tsx +717 -0
  55. package/packages/ndpr-toolkit/src/components/dsr/DSRRequestForm.tsx +476 -0
  56. package/packages/ndpr-toolkit/src/components/dsr/DSRTracker.tsx +620 -0
  57. package/packages/ndpr-toolkit/src/components/policy/PolicyExporter.tsx +541 -0
  58. package/packages/ndpr-toolkit/src/components/policy/PolicyGenerator.tsx +454 -0
  59. package/packages/ndpr-toolkit/src/components/policy/PolicyPreview.tsx +333 -0
  60. package/packages/ndpr-toolkit/src/hooks/useBreach.ts +409 -0
  61. package/packages/ndpr-toolkit/src/hooks/useConsent.ts +263 -0
  62. package/packages/ndpr-toolkit/src/hooks/useDPIA.ts +457 -0
  63. package/packages/ndpr-toolkit/src/hooks/useDSR.ts +236 -0
  64. package/packages/ndpr-toolkit/src/hooks/usePrivacyPolicy.ts +428 -0
  65. package/{dist/index.d.ts → packages/ndpr-toolkit/src/index.ts} +14 -1
  66. package/packages/ndpr-toolkit/src/setupTests.ts +5 -0
  67. package/packages/ndpr-toolkit/src/types/breach.ts +283 -0
  68. package/packages/ndpr-toolkit/src/types/consent.ts +111 -0
  69. package/packages/ndpr-toolkit/src/types/dpia.ts +236 -0
  70. package/packages/ndpr-toolkit/src/types/dsr.ts +192 -0
  71. package/packages/ndpr-toolkit/src/types/index.ts +42 -0
  72. package/packages/ndpr-toolkit/src/types/privacy.ts +246 -0
  73. package/packages/ndpr-toolkit/src/utils/breach.ts +122 -0
  74. package/packages/ndpr-toolkit/src/utils/consent.ts +51 -0
  75. package/packages/ndpr-toolkit/src/utils/dpia.ts +104 -0
  76. package/packages/ndpr-toolkit/src/utils/dsr.ts +77 -0
  77. package/packages/ndpr-toolkit/src/utils/privacy.ts +100 -0
  78. package/packages/ndpr-toolkit/tsconfig.json +23 -0
  79. package/postcss.config.mjs +5 -0
  80. package/public/NDPR TOOLKIT.svg +1 -0
  81. package/public/favicon/android-chrome-192x192.png +0 -0
  82. package/public/favicon/android-chrome-512x512.png +0 -0
  83. package/public/favicon/apple-touch-icon.png +0 -0
  84. package/public/favicon/favicon-16x16.png +0 -0
  85. package/public/favicon/favicon-32x32.png +0 -0
  86. package/public/favicon/site.webmanifest +1 -0
  87. package/public/file.svg +1 -0
  88. package/public/globe.svg +1 -0
  89. package/public/ndpr-toolkit-logo.svg +108 -0
  90. package/public/next.svg +1 -0
  91. package/public/vercel.svg +1 -0
  92. package/public/window.svg +1 -0
  93. package/src/__tests__/example.test.ts +13 -0
  94. package/src/__tests__/requestService.test.ts +57 -0
  95. package/src/app/accessibility.css +70 -0
  96. package/src/app/docs/components/DocLayout.tsx +267 -0
  97. package/src/app/docs/components/breach-notification/page.tsx +797 -0
  98. package/src/app/docs/components/consent-management/page.tsx +576 -0
  99. package/src/app/docs/components/data-subject-rights/page.tsx +511 -0
  100. package/src/app/docs/components/dpia-questionnaire/layout.tsx +15 -0
  101. package/src/app/docs/components/dpia-questionnaire/metadata.ts +31 -0
  102. package/src/app/docs/components/dpia-questionnaire/page.tsx +666 -0
  103. package/src/app/docs/components/hooks/page.tsx +305 -0
  104. package/src/app/docs/components/page.tsx +84 -0
  105. package/src/app/docs/components/privacy-policy-generator/page.tsx +634 -0
  106. package/src/app/docs/guides/breach-notification-process/components/BestPractices.tsx +123 -0
  107. package/src/app/docs/guides/breach-notification-process/components/ImplementationSteps.tsx +328 -0
  108. package/src/app/docs/guides/breach-notification-process/components/Introduction.tsx +28 -0
  109. package/src/app/docs/guides/breach-notification-process/components/NotificationTimeline.tsx +91 -0
  110. package/src/app/docs/guides/breach-notification-process/components/Resources.tsx +118 -0
  111. package/src/app/docs/guides/breach-notification-process/page.tsx +39 -0
  112. package/src/app/docs/guides/conducting-dpia/page.tsx +593 -0
  113. package/src/app/docs/guides/data-subject-requests/page.tsx +666 -0
  114. package/src/app/docs/guides/managing-consent/page.tsx +738 -0
  115. package/src/app/docs/guides/ndpr-compliance-checklist/components/ComplianceChecklist.tsx +296 -0
  116. package/src/app/docs/guides/ndpr-compliance-checklist/components/ImplementationTools.tsx +145 -0
  117. package/src/app/docs/guides/ndpr-compliance-checklist/components/Introduction.tsx +33 -0
  118. package/src/app/docs/guides/ndpr-compliance-checklist/components/KeyRequirements.tsx +99 -0
  119. package/src/app/docs/guides/ndpr-compliance-checklist/components/Resources.tsx +159 -0
  120. package/src/app/docs/guides/ndpr-compliance-checklist/page.tsx +38 -0
  121. package/src/app/docs/guides/page.tsx +67 -0
  122. package/src/app/docs/layout.tsx +15 -0
  123. package/src/app/docs/metadata.ts +31 -0
  124. package/src/app/docs/page.tsx +572 -0
  125. package/src/app/favicon.ico +0 -0
  126. package/src/app/globals.css +123 -0
  127. package/src/app/layout.tsx +37 -0
  128. package/src/app/ndpr-demos/breach/page.tsx +354 -0
  129. package/src/app/ndpr-demos/consent/page.tsx +366 -0
  130. package/src/app/ndpr-demos/dpia/page.tsx +495 -0
  131. package/src/app/ndpr-demos/dsr/page.tsx +280 -0
  132. package/src/app/ndpr-demos/page.tsx +73 -0
  133. package/src/app/ndpr-demos/policy/page.tsx +771 -0
  134. package/src/app/page.tsx +452 -0
  135. package/src/components/ErrorBoundary.tsx +90 -0
  136. package/src/components/breach-notification/BreachNotificationForm.tsx +479 -0
  137. package/src/components/consent/ConsentBanner.tsx +159 -0
  138. package/src/components/data-subject-rights/DataSubjectRequestForm.tsx +419 -0
  139. package/src/components/docs/DocLayout.tsx +289 -0
  140. package/src/components/docs/index.ts +2 -0
  141. package/src/components/dpia/DPIAQuestionnaire.tsx +483 -0
  142. package/src/components/privacy-policy/PolicyGenerator.tsx +1062 -0
  143. package/src/components/privacy-policy/data.ts +98 -0
  144. package/src/components/privacy-policy/shared/CheckboxField.tsx +38 -0
  145. package/src/components/privacy-policy/shared/CheckboxGroup.tsx +85 -0
  146. package/src/components/privacy-policy/shared/FormField.tsx +79 -0
  147. package/src/components/privacy-policy/shared/StepIndicator.tsx +86 -0
  148. package/src/components/privacy-policy/steps/CustomSectionsStep.tsx +335 -0
  149. package/src/components/privacy-policy/steps/DataCollectionStep.tsx +231 -0
  150. package/src/components/privacy-policy/steps/DataSharingStep.tsx +418 -0
  151. package/src/components/privacy-policy/steps/OrganizationInfoStep.tsx +202 -0
  152. package/src/components/privacy-policy/steps/PolicyPreviewStep.tsx +172 -0
  153. package/src/components/ui/Badge.tsx +46 -0
  154. package/src/components/ui/Button.tsx +59 -0
  155. package/src/components/ui/Card.tsx +92 -0
  156. package/src/components/ui/Checkbox.tsx +57 -0
  157. package/src/components/ui/FormField.tsx +50 -0
  158. package/src/components/ui/Input.tsx +38 -0
  159. package/src/components/ui/Loading.tsx +201 -0
  160. package/src/components/ui/Select.tsx +42 -0
  161. package/src/components/ui/TextArea.tsx +38 -0
  162. package/src/components/ui/label.tsx +24 -0
  163. package/src/components/ui/switch.tsx +31 -0
  164. package/src/components/ui/tabs.tsx +66 -0
  165. package/src/hooks/useConsent.ts +64 -0
  166. package/src/hooks/useLoadingState.ts +85 -0
  167. package/src/lib/consentService.ts +137 -0
  168. package/src/lib/dpiaQuestions.ts +148 -0
  169. package/src/lib/requestService.ts +75 -0
  170. package/src/lib/sanitize.ts +108 -0
  171. package/src/lib/storage.ts +222 -0
  172. package/src/lib/utils.ts +6 -0
  173. package/src/types/html-to-docx.d.ts +30 -0
  174. package/src/types/index.ts +72 -0
  175. package/tailwind.config.ts +65 -0
  176. package/tsconfig.json +41 -0
  177. package/dist/components/breach/BreachNotificationManager.d.ts +0 -62
  178. package/dist/components/breach/BreachReportForm.d.ts +0 -66
  179. package/dist/components/breach/BreachRiskAssessment.d.ts +0 -50
  180. package/dist/components/breach/RegulatoryReportGenerator.d.ts +0 -94
  181. package/dist/components/consent/ConsentBanner.d.ts +0 -79
  182. package/dist/components/consent/ConsentManager.d.ts +0 -73
  183. package/dist/components/consent/ConsentStorage.d.ts +0 -41
  184. package/dist/components/dpia/DPIAQuestionnaire.d.ts +0 -70
  185. package/dist/components/dpia/DPIAReport.d.ts +0 -40
  186. package/dist/components/dpia/StepIndicator.d.ts +0 -64
  187. package/dist/components/dsr/DSRDashboard.d.ts +0 -58
  188. package/dist/components/dsr/DSRRequestForm.d.ts +0 -74
  189. package/dist/components/dsr/DSRTracker.d.ts +0 -56
  190. package/dist/components/policy/PolicyExporter.d.ts +0 -65
  191. package/dist/components/policy/PolicyGenerator.d.ts +0 -54
  192. package/dist/components/policy/PolicyPreview.d.ts +0 -71
  193. package/dist/hooks/useBreach.d.ts +0 -97
  194. package/dist/hooks/useConsent.d.ts +0 -63
  195. package/dist/hooks/useDPIA.d.ts +0 -92
  196. package/dist/hooks/useDSR.d.ts +0 -72
  197. package/dist/hooks/usePrivacyPolicy.d.ts +0 -87
  198. package/dist/index.esm.js +0 -2
  199. package/dist/index.esm.js.map +0 -1
  200. package/dist/index.js +0 -2
  201. package/dist/index.js.map +0 -1
  202. package/dist/setupTests.d.ts +0 -2
  203. package/dist/types/breach.d.ts +0 -239
  204. package/dist/types/consent.d.ts +0 -95
  205. package/dist/types/dpia.d.ts +0 -196
  206. package/dist/types/dsr.d.ts +0 -162
  207. package/dist/types/privacy.d.ts +0 -204
  208. package/dist/utils/breach.d.ts +0 -14
  209. package/dist/utils/consent.d.ts +0 -10
  210. package/dist/utils/dpia.d.ts +0 -12
  211. package/dist/utils/dsr.d.ts +0 -11
  212. 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
+ };