@tantainnovative/ndpr-toolkit 1.0.2 → 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 -447
  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} +13 -0
  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,717 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { DSRRequest, DSRStatus, DSRType } from '../../types/dsr';
3
+ import { formatDSRRequest } from '../../utils/dsr';
4
+
5
+ export interface DSRDashboardProps {
6
+ /**
7
+ * List of DSR requests to display
8
+ */
9
+ requests: DSRRequest[];
10
+
11
+ /**
12
+ * Callback function called when a request is selected
13
+ */
14
+ onSelectRequest?: (requestId: string) => void;
15
+
16
+ /**
17
+ * Callback function called when a request status is updated
18
+ */
19
+ onUpdateStatus?: (requestId: string, status: DSRStatus) => void;
20
+
21
+ /**
22
+ * Callback function called when a request is assigned
23
+ */
24
+ onAssignRequest?: (requestId: string, assignee: string) => void;
25
+
26
+ /**
27
+ * Title displayed on the dashboard
28
+ * @default "Data Subject Request Dashboard"
29
+ */
30
+ title?: string;
31
+
32
+ /**
33
+ * Description text displayed on the dashboard
34
+ * @default "Track and manage data subject requests in compliance with NDPR requirements."
35
+ */
36
+ description?: string;
37
+
38
+ /**
39
+ * Custom CSS class for the dashboard
40
+ */
41
+ className?: string;
42
+
43
+ /**
44
+ * Custom CSS class for the buttons
45
+ */
46
+ buttonClassName?: string;
47
+
48
+ /**
49
+ * Whether to show the request details
50
+ * @default true
51
+ */
52
+ showRequestDetails?: boolean;
53
+
54
+ /**
55
+ * Whether to show the request timeline
56
+ * @default true
57
+ */
58
+ showRequestTimeline?: boolean;
59
+
60
+ /**
61
+ * Whether to show the deadline alerts
62
+ * @default true
63
+ */
64
+ showDeadlineAlerts?: boolean;
65
+
66
+ /**
67
+ * List of possible assignees
68
+ */
69
+ assignees?: string[];
70
+ }
71
+
72
+ export const DSRDashboard: React.FC<DSRDashboardProps> = ({
73
+ requests,
74
+ onSelectRequest,
75
+ onUpdateStatus,
76
+ onAssignRequest,
77
+ title = "Data Subject Request Dashboard",
78
+ description = "Track and manage data subject requests in compliance with NDPR requirements.",
79
+ className = "",
80
+ buttonClassName = "",
81
+ showRequestDetails = true,
82
+ showRequestTimeline = true,
83
+ showDeadlineAlerts = true,
84
+ assignees = []
85
+ }) => {
86
+ const [selectedRequestId, setSelectedRequestId] = useState<string | null>(null);
87
+ const [filteredRequests, setFilteredRequests] = useState<DSRRequest[]>(requests);
88
+ const [statusFilter, setStatusFilter] = useState<string>('all');
89
+ const [typeFilter, setTypeFilter] = useState<string>('all');
90
+ const [searchTerm, setSearchTerm] = useState<string>('');
91
+ const [sortBy, setSortBy] = useState<string>('createdAt');
92
+ const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc');
93
+ const [assignee, setAssignee] = useState<string>('');
94
+
95
+ // Update filtered requests when filters change
96
+ useEffect(() => {
97
+ let filtered = [...requests];
98
+
99
+ // Apply status filter
100
+ if (statusFilter !== 'all') {
101
+ filtered = filtered.filter(request => request.status === statusFilter);
102
+ }
103
+
104
+ // Apply type filter
105
+ if (typeFilter !== 'all') {
106
+ filtered = filtered.filter(request => request.type === typeFilter);
107
+ }
108
+
109
+ // Apply search filter
110
+ if (searchTerm) {
111
+ const term = searchTerm.toLowerCase();
112
+ filtered = filtered.filter(request =>
113
+ request.subject.name.toLowerCase().includes(term) ||
114
+ request.subject.email.toLowerCase().includes(term) ||
115
+ (request.description && request.description.toLowerCase().includes(term))
116
+ );
117
+ }
118
+
119
+ // Apply sorting
120
+ filtered.sort((a, b) => {
121
+ let comparison = 0;
122
+
123
+ switch (sortBy) {
124
+ case 'createdAt':
125
+ comparison = a.createdAt - b.createdAt;
126
+ break;
127
+ case 'dueDate':
128
+ comparison = (a.dueDate || 0) - (b.dueDate || 0);
129
+ break;
130
+ case 'type':
131
+ comparison = a.type.localeCompare(b.type);
132
+ break;
133
+ case 'status':
134
+ comparison = a.status.localeCompare(b.status);
135
+ break;
136
+ default:
137
+ comparison = a.createdAt - b.createdAt;
138
+ }
139
+
140
+ return sortDirection === 'asc' ? comparison : -comparison;
141
+ });
142
+
143
+ setFilteredRequests(filtered);
144
+ }, [requests, statusFilter, typeFilter, searchTerm, sortBy, sortDirection]);
145
+
146
+ // Select the first request if none is selected
147
+ useEffect(() => {
148
+ if (filteredRequests.length > 0 && !selectedRequestId) {
149
+ setSelectedRequestId(filteredRequests[0].id);
150
+ }
151
+ }, [filteredRequests, selectedRequestId]);
152
+
153
+ // Handle request selection
154
+ const handleSelectRequest = (requestId: string) => {
155
+ setSelectedRequestId(requestId);
156
+ if (onSelectRequest) {
157
+ onSelectRequest(requestId);
158
+ }
159
+ };
160
+
161
+ // Handle status update
162
+ const handleUpdateStatus = (status: DSRStatus) => {
163
+ if (selectedRequestId && onUpdateStatus) {
164
+ onUpdateStatus(selectedRequestId, status);
165
+ }
166
+ };
167
+
168
+ // Handle request assignment
169
+ const handleAssignRequest = () => {
170
+ if (selectedRequestId && assignee && onAssignRequest) {
171
+ onAssignRequest(selectedRequestId, assignee);
172
+ setAssignee('');
173
+ }
174
+ };
175
+
176
+ // Format a date from timestamp
177
+ const formatDate = (timestamp: number): string => {
178
+ return new Date(timestamp).toLocaleDateString();
179
+ };
180
+
181
+ // Calculate days remaining until deadline
182
+ const calculateDaysRemaining = (dueDate: number): number => {
183
+ const now = Date.now();
184
+ const remaining = (dueDate - now) / (24 * 60 * 60 * 1000);
185
+ return Math.ceil(remaining);
186
+ };
187
+
188
+ // Get the selected request
189
+ const selectedRequest = selectedRequestId
190
+ ? requests.find(request => request.id === selectedRequestId)
191
+ : null;
192
+
193
+ // Render type badge
194
+ const renderTypeBadge = (type: DSRType) => {
195
+ const colorClasses = {
196
+ access: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200',
197
+ rectification: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
198
+ erasure: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
199
+ restriction: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200',
200
+ portability: 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200',
201
+ objection: 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200'
202
+ };
203
+
204
+ return (
205
+ <span className={`px-2 py-1 rounded text-xs font-medium ${colorClasses[type]}`}>
206
+ {type.charAt(0).toUpperCase() + type.slice(1)}
207
+ </span>
208
+ );
209
+ };
210
+
211
+ // Render status badge
212
+ const renderStatusBadge = (status: DSRStatus) => {
213
+ const colorClasses = {
214
+ pending: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200',
215
+ inProgress: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200',
216
+ completed: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
217
+ rejected: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
218
+ awaitingVerification: 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200'
219
+ };
220
+
221
+ return (
222
+ <span className={`px-2 py-1 rounded text-xs font-medium ${colorClasses[status]}`}>
223
+ {status === 'inProgress' ? 'In Progress' :
224
+ status === 'awaitingVerification' ? 'Awaiting Verification' :
225
+ status.charAt(0).toUpperCase() + status.slice(1)}
226
+ </span>
227
+ );
228
+ };
229
+
230
+ // Render deadline alert
231
+ const renderDeadlineAlert = (request: DSRRequest) => {
232
+ if (!request.dueDate) return null;
233
+
234
+ const daysRemaining = calculateDaysRemaining(request.dueDate);
235
+
236
+ if (daysRemaining <= 0) {
237
+ return (
238
+ <div className="bg-red-50 dark:bg-red-900/20 p-3 rounded-md">
239
+ <p className="text-sm text-red-800 dark:text-red-200 font-medium">
240
+ Deadline Passed
241
+ </p>
242
+ <p className="text-xs text-red-700 dark:text-red-300 mt-1">
243
+ The response deadline has passed. Immediate action is required.
244
+ </p>
245
+ </div>
246
+ );
247
+ }
248
+
249
+ if (daysRemaining <= 3) {
250
+ return (
251
+ <div className="bg-red-50 dark:bg-red-900/20 p-3 rounded-md">
252
+ <p className="text-sm text-red-800 dark:text-red-200 font-medium">
253
+ Urgent: Deadline Approaching
254
+ </p>
255
+ <p className="text-xs text-red-700 dark:text-red-300 mt-1">
256
+ Only {daysRemaining} day{daysRemaining !== 1 ? 's' : ''} remaining until the response deadline.
257
+ </p>
258
+ </div>
259
+ );
260
+ }
261
+
262
+ if (daysRemaining <= 7) {
263
+ return (
264
+ <div className="bg-yellow-50 dark:bg-yellow-900/20 p-3 rounded-md">
265
+ <p className="text-sm text-yellow-800 dark:text-yellow-200 font-medium">
266
+ Deadline Approaching
267
+ </p>
268
+ <p className="text-xs text-yellow-700 dark:text-yellow-300 mt-1">
269
+ {daysRemaining} days remaining until the response deadline.
270
+ </p>
271
+ </div>
272
+ );
273
+ }
274
+
275
+ return (
276
+ <div className="bg-green-50 dark:bg-green-900/20 p-3 rounded-md">
277
+ <p className="text-sm text-green-800 dark:text-green-200 font-medium">
278
+ Deadline Tracking
279
+ </p>
280
+ <p className="text-xs text-green-700 dark:text-green-300 mt-1">
281
+ {daysRemaining} days remaining until the response deadline.
282
+ </p>
283
+ </div>
284
+ );
285
+ };
286
+
287
+ // Render request timeline
288
+ const renderRequestTimeline = (request: DSRRequest) => {
289
+ const timeline = [
290
+ {
291
+ title: 'Request Received',
292
+ date: request.createdAt,
293
+ completed: true,
294
+ description: `Request was received on ${formatDate(request.createdAt)}.`
295
+ }
296
+ ];
297
+
298
+ if (request.verifiedAt) {
299
+ timeline.push({
300
+ title: 'Identity Verified',
301
+ date: request.verifiedAt,
302
+ completed: true,
303
+ description: `Data subject's identity was verified on ${formatDate(request.verifiedAt)}.`
304
+ });
305
+ } else if (request.status === 'awaitingVerification') {
306
+ timeline.push({
307
+ title: 'Identity Verification',
308
+ date: Date.now(),
309
+ completed: false,
310
+ description: 'Awaiting verification of data subject\'s identity.'
311
+ });
312
+ }
313
+
314
+ if (request.status === 'inProgress' || request.status === 'completed' || request.status === 'rejected') {
315
+ timeline.push({
316
+ title: 'Processing Started',
317
+ date: request.updatedAt,
318
+ completed: true,
319
+ description: `Request processing started on ${formatDate(request.updatedAt)}.`
320
+ });
321
+ }
322
+
323
+ if (request.status === 'completed') {
324
+ timeline.push({
325
+ title: 'Request Completed',
326
+ date: request.completedAt || Date.now(),
327
+ completed: true,
328
+ description: `Request was completed on ${formatDate(request.completedAt || Date.now())}.`
329
+ });
330
+ } else if (request.status === 'rejected') {
331
+ timeline.push({
332
+ title: 'Request Rejected',
333
+ date: request.completedAt || Date.now(),
334
+ completed: true,
335
+ description: `Request was rejected on ${formatDate(request.completedAt || Date.now())}.${request.rejectionReason ? ` Reason: ${request.rejectionReason}` : ''}`
336
+ });
337
+ }
338
+
339
+ if (request.dueDate) {
340
+ timeline.push({
341
+ title: 'Response Deadline',
342
+ date: request.dueDate,
343
+ completed: Date.now() > request.dueDate,
344
+ description: `Response is due by ${formatDate(request.dueDate)}.`
345
+ });
346
+ }
347
+
348
+ return (
349
+ <div className="mt-6">
350
+ <h3 className="text-lg font-medium mb-4">Request Timeline</h3>
351
+ <ol className="relative border-l border-gray-200 dark:border-gray-700">
352
+ {timeline.map((item, index) => (
353
+ <li key={index} className="mb-6 ml-4">
354
+ <div className={`absolute w-3 h-3 rounded-full mt-1.5 -left-1.5 border ${
355
+ item.completed
356
+ ? 'bg-green-500 border-green-500 dark:border-green-500'
357
+ : 'bg-gray-200 border-gray-200 dark:bg-gray-700 dark:border-gray-700'
358
+ }`}></div>
359
+ <time className="mb-1 text-sm font-normal leading-none text-gray-400 dark:text-gray-500">
360
+ {item.date ? formatDate(item.date) : 'Pending'}
361
+ </time>
362
+ <h4 className="text-sm font-semibold text-gray-900 dark:text-white">
363
+ {item.title}
364
+ </h4>
365
+ <p className="text-xs text-gray-500 dark:text-gray-400">
366
+ {item.description}
367
+ </p>
368
+ </li>
369
+ ))}
370
+ </ol>
371
+ </div>
372
+ );
373
+ };
374
+
375
+ // Render the type filter options
376
+ const renderTypeOptions = () => {
377
+ const options = [
378
+ { value: 'all', label: 'All Types' },
379
+ { value: 'access', label: 'Access' },
380
+ { value: 'rectification', label: 'Rectification' },
381
+ { value: 'erasure', label: 'Erasure' },
382
+ { value: 'restriction', label: 'Restriction' },
383
+ { value: 'portability', label: 'Portability' },
384
+ { value: 'objection', label: 'Objection' }
385
+ ];
386
+
387
+ return options.map(option => (
388
+ <option key={option.value} value={option.value}>
389
+ {option.label}
390
+ </option>
391
+ ));
392
+ };
393
+
394
+ // Render the status filter options
395
+ const renderStatusOptions = () => {
396
+ const options = [
397
+ { value: 'all', label: 'All Statuses' },
398
+ { value: 'pending', label: 'Pending' },
399
+ { value: 'awaitingVerification', label: 'Awaiting Verification' },
400
+ { value: 'inProgress', label: 'In Progress' },
401
+ { value: 'completed', label: 'Completed' },
402
+ { value: 'rejected', label: 'Rejected' }
403
+ ];
404
+
405
+ return options.map(option => (
406
+ <option key={option.value} value={option.value}>
407
+ {option.label}
408
+ </option>
409
+ ));
410
+ };
411
+
412
+ // Render the status update options
413
+ const renderStatusUpdateOptions = () => {
414
+ const options = [
415
+ { value: 'pending', label: 'Pending' },
416
+ { value: 'awaitingVerification', label: 'Awaiting Verification' },
417
+ { value: 'inProgress', label: 'In Progress' },
418
+ { value: 'completed', label: 'Completed' },
419
+ { value: 'rejected', label: 'Rejected' }
420
+ ];
421
+
422
+ return options.map(option => (
423
+ <option key={option.value} value={option.value}>
424
+ {option.label}
425
+ </option>
426
+ ));
427
+ };
428
+
429
+ return (
430
+ <div className={`bg-white dark:bg-gray-800 p-6 rounded-lg shadow-md ${className}`}>
431
+ <h2 className="text-xl font-bold mb-2">{title}</h2>
432
+ <p className="mb-6 text-gray-600 dark:text-gray-300">{description}</p>
433
+
434
+ {/* Filters and Search */}
435
+ <div className="mb-6 grid grid-cols-1 md:grid-cols-4 gap-4">
436
+ <div>
437
+ <label htmlFor="statusFilter" className="block text-sm font-medium mb-1">
438
+ Status Filter
439
+ </label>
440
+ <select
441
+ id="statusFilter"
442
+ value={statusFilter}
443
+ onChange={e => setStatusFilter(e.target.value)}
444
+ className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
445
+ >
446
+ {renderStatusOptions()}
447
+ </select>
448
+ </div>
449
+
450
+ <div>
451
+ <label htmlFor="typeFilter" className="block text-sm font-medium mb-1">
452
+ Request Type Filter
453
+ </label>
454
+ <select
455
+ id="typeFilter"
456
+ value={typeFilter}
457
+ onChange={e => setTypeFilter(e.target.value)}
458
+ className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
459
+ >
460
+ {renderTypeOptions()}
461
+ </select>
462
+ </div>
463
+
464
+ <div>
465
+ <label htmlFor="sortBy" className="block text-sm font-medium mb-1">
466
+ Sort By
467
+ </label>
468
+ <select
469
+ id="sortBy"
470
+ value={sortBy}
471
+ onChange={e => setSortBy(e.target.value)}
472
+ className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
473
+ >
474
+ <option value="createdAt">Date Received</option>
475
+ <option value="dueDate">Due Date</option>
476
+ <option value="type">Request Type</option>
477
+ <option value="status">Status</option>
478
+ </select>
479
+ </div>
480
+
481
+ <div>
482
+ <label htmlFor="searchTerm" className="block text-sm font-medium mb-1">
483
+ Search
484
+ </label>
485
+ <input
486
+ type="text"
487
+ id="searchTerm"
488
+ value={searchTerm}
489
+ onChange={e => setSearchTerm(e.target.value)}
490
+ placeholder="Search requests..."
491
+ className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
492
+ />
493
+ </div>
494
+ </div>
495
+
496
+ {/* Request List and Details */}
497
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
498
+ {/* Request List */}
499
+ <div className="md:col-span-1">
500
+ <h3 className="text-lg font-medium mb-3">DSR Requests</h3>
501
+
502
+ {filteredRequests.length === 0 ? (
503
+ <p className="text-gray-500 dark:text-gray-400 text-sm">
504
+ No data subject requests found.
505
+ </p>
506
+ ) : (
507
+ <div className="space-y-2 max-h-96 overflow-y-auto pr-2">
508
+ {filteredRequests.map(request => {
509
+ // Calculate days remaining for the list item
510
+ const daysRemaining = request.dueDate ? calculateDaysRemaining(request.dueDate) : null;
511
+
512
+ // Determine deadline status for the list item
513
+ let deadlineStatus = null;
514
+ if (daysRemaining !== null) {
515
+ if (daysRemaining <= 0) {
516
+ deadlineStatus = (
517
+ <span className="text-xs text-red-600 dark:text-red-400 font-bold">
518
+ Overdue
519
+ </span>
520
+ );
521
+ } else if (daysRemaining <= 3) {
522
+ deadlineStatus = (
523
+ <span className="text-xs text-red-600 dark:text-red-400">
524
+ Urgent
525
+ </span>
526
+ );
527
+ } else if (daysRemaining <= 7) {
528
+ deadlineStatus = (
529
+ <span className="text-xs text-yellow-600 dark:text-yellow-400">
530
+ Soon
531
+ </span>
532
+ );
533
+ }
534
+ }
535
+
536
+ return (
537
+ <div
538
+ key={request.id}
539
+ className={`p-3 rounded-md cursor-pointer ${
540
+ selectedRequestId === request.id
541
+ ? 'bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800'
542
+ : 'bg-gray-50 dark:bg-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600'
543
+ }`}
544
+ onClick={() => handleSelectRequest(request.id)}
545
+ >
546
+ <div className="flex justify-between items-start mb-1">
547
+ <h4 className="font-medium text-sm">{request.subject.name}</h4>
548
+ {renderTypeBadge(request.type)}
549
+ </div>
550
+ <p className="text-xs text-gray-500 dark:text-gray-400 mb-1">
551
+ {request.subject.email}
552
+ </p>
553
+ <p className="text-xs text-gray-500 dark:text-gray-400 mb-1">
554
+ Received: {formatDate(request.createdAt)}
555
+ </p>
556
+ <div className="flex justify-between items-center mt-2">
557
+ <div>
558
+ {renderStatusBadge(request.status)}
559
+ </div>
560
+ <div>
561
+ {deadlineStatus}
562
+ </div>
563
+ </div>
564
+ </div>
565
+ );
566
+ })}
567
+ </div>
568
+ )}
569
+ </div>
570
+
571
+ {/* Request Details */}
572
+ <div className="md:col-span-2">
573
+ {selectedRequest ? (
574
+ <div>
575
+ <div className="flex justify-between items-start mb-4">
576
+ <h3 className="text-lg font-medium">{selectedRequest.subject.name}</h3>
577
+ <div className="flex space-x-2">
578
+ {renderTypeBadge(selectedRequest.type)}
579
+ {renderStatusBadge(selectedRequest.status)}
580
+ </div>
581
+ </div>
582
+
583
+ {/* Deadline Alert */}
584
+ {showDeadlineAlerts && selectedRequest.dueDate && (
585
+ <div className="mb-4">
586
+ {renderDeadlineAlert(selectedRequest)}
587
+ </div>
588
+ )}
589
+
590
+ {/* Request Details */}
591
+ {showRequestDetails && (
592
+ <div className="mb-6">
593
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
594
+ <div>
595
+ <p className="text-sm"><span className="font-medium">Email:</span> {selectedRequest.subject.email}</p>
596
+ {selectedRequest.subject.phone && (
597
+ <p className="text-sm"><span className="font-medium">Phone:</span> {selectedRequest.subject.phone}</p>
598
+ )}
599
+ <p className="text-sm"><span className="font-medium">Received:</span> {formatDate(selectedRequest.createdAt)}</p>
600
+ </div>
601
+ <div>
602
+ <p className="text-sm">
603
+ <span className="font-medium">Request Type:</span> {selectedRequest.type.charAt(0).toUpperCase() + selectedRequest.type.slice(1)}
604
+ </p>
605
+ <p className="text-sm">
606
+ <span className="font-medium">Status:</span> {
607
+ selectedRequest.status === 'inProgress' ? 'In Progress' :
608
+ selectedRequest.status === 'awaitingVerification' ? 'Awaiting Verification' :
609
+ selectedRequest.status.charAt(0).toUpperCase() + selectedRequest.status.slice(1)
610
+ }
611
+ </p>
612
+ {selectedRequest.dueDate && (
613
+ <p className="text-sm">
614
+ <span className="font-medium">Due Date:</span> {formatDate(selectedRequest.dueDate)}
615
+ </p>
616
+ )}
617
+ </div>
618
+ </div>
619
+
620
+ {selectedRequest.description && (
621
+ <div className="mb-4">
622
+ <p className="text-sm font-medium">Request Details:</p>
623
+ <p className="text-sm text-gray-600 dark:text-gray-300 bg-gray-50 dark:bg-gray-700 p-2 rounded-md mt-1">
624
+ {selectedRequest.description}
625
+ </p>
626
+ </div>
627
+ )}
628
+
629
+ {selectedRequest.additionalInfo && (
630
+ <div>
631
+ <p className="text-sm font-medium">Additional Information:</p>
632
+ <p className="text-sm text-gray-600 dark:text-gray-300 bg-gray-50 dark:bg-gray-700 p-2 rounded-md mt-1">
633
+ {typeof selectedRequest.additionalInfo === 'object' ?
634
+ JSON.stringify(selectedRequest.additionalInfo, null, 2) :
635
+ String(selectedRequest.additionalInfo || 'No additional information provided')}
636
+ </p>
637
+ </div>
638
+ )}
639
+ </div>
640
+ )}
641
+
642
+ {/* Request Management */}
643
+ <div className="mb-6 grid grid-cols-1 md:grid-cols-2 gap-4">
644
+ {/* Status Update */}
645
+ <div>
646
+ <h3 className="text-md font-medium mb-2">Update Status</h3>
647
+ <div className="flex space-x-2">
648
+ <select
649
+ value={selectedRequest.status}
650
+ onChange={e => handleUpdateStatus(e.target.value as DSRStatus)}
651
+ className="flex-grow px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
652
+ >
653
+ {renderStatusUpdateOptions()}
654
+ </select>
655
+ <button
656
+ onClick={() => handleUpdateStatus(selectedRequest.status)}
657
+ className={`px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 ${buttonClassName}`}
658
+ >
659
+ Update
660
+ </button>
661
+ </div>
662
+ </div>
663
+
664
+ {/* Assign Request */}
665
+ {assignees.length > 0 && (
666
+ <div>
667
+ <h3 className="text-md font-medium mb-2">Assign Request</h3>
668
+ <div className="flex space-x-2">
669
+ <select
670
+ value={assignee}
671
+ onChange={e => setAssignee(e.target.value)}
672
+ className="flex-grow px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
673
+ >
674
+ <option value="">Select Assignee</option>
675
+ {assignees.map(name => (
676
+ <option key={name} value={name}>{name}</option>
677
+ ))}
678
+ </select>
679
+ <button
680
+ onClick={handleAssignRequest}
681
+ disabled={!assignee}
682
+ className={`px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:bg-gray-300 disabled:text-gray-500 ${buttonClassName}`}
683
+ >
684
+ Assign
685
+ </button>
686
+ </div>
687
+ </div>
688
+ )}
689
+ </div>
690
+
691
+ {/* Request Summary */}
692
+ <div className="mb-6">
693
+ <h3 className="text-lg font-medium mb-3">Request Summary</h3>
694
+ <div className="bg-gray-50 dark:bg-gray-700 p-3 rounded-md">
695
+ <pre className="whitespace-pre-wrap text-sm font-mono text-gray-800 dark:text-gray-200">
696
+ <pre>
697
+ {JSON.stringify(formatDSRRequest(selectedRequest), null, 2)}
698
+ </pre>
699
+ </pre>
700
+ </div>
701
+ </div>
702
+
703
+ {/* Request Timeline */}
704
+ {showRequestTimeline && renderRequestTimeline(selectedRequest)}
705
+ </div>
706
+ ) : (
707
+ <div className="flex items-center justify-center h-64 bg-gray-50 dark:bg-gray-700 rounded-md">
708
+ <p className="text-gray-500 dark:text-gray-400">
709
+ Select a request to view details
710
+ </p>
711
+ </div>
712
+ )}
713
+ </div>
714
+ </div>
715
+ </div>
716
+ );
717
+ };