@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,701 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { BreachReport, RiskAssessment, RegulatoryNotification, NotificationRequirement } from '../../types/breach';
3
+ import { calculateBreachSeverity } from '../../utils/breach';
4
+
5
+ export interface BreachNotificationManagerProps {
6
+ /**
7
+ * List of breach reports to manage
8
+ */
9
+ breachReports: BreachReport[];
10
+
11
+ /**
12
+ * List of risk assessments
13
+ */
14
+ riskAssessments: RiskAssessment[];
15
+
16
+ /**
17
+ * List of regulatory notifications
18
+ */
19
+ regulatoryNotifications: RegulatoryNotification[];
20
+
21
+ /**
22
+ * Callback function called when a breach is selected
23
+ */
24
+ onSelectBreach?: (breachId: string) => void;
25
+
26
+ /**
27
+ * Callback function called when a risk assessment is requested
28
+ */
29
+ onRequestAssessment?: (breachId: string) => void;
30
+
31
+ /**
32
+ * Callback function called when a notification is requested
33
+ */
34
+ onRequestNotification?: (breachId: string) => void;
35
+
36
+ /**
37
+ * Title displayed on the manager
38
+ * @default "Breach Notification Manager"
39
+ */
40
+ title?: string;
41
+
42
+ /**
43
+ * Description text displayed on the manager
44
+ * @default "Manage data breach notifications and track compliance with NDPR requirements."
45
+ */
46
+ description?: string;
47
+
48
+ /**
49
+ * Custom CSS class for the manager
50
+ */
51
+ className?: string;
52
+
53
+ /**
54
+ * Custom CSS class for the buttons
55
+ */
56
+ buttonClassName?: string;
57
+
58
+ /**
59
+ * Whether to show the breach details
60
+ * @default true
61
+ */
62
+ showBreachDetails?: boolean;
63
+
64
+ /**
65
+ * Whether to show the notification timeline
66
+ * @default true
67
+ */
68
+ showNotificationTimeline?: boolean;
69
+
70
+ /**
71
+ * Whether to show the deadline alerts
72
+ * @default true
73
+ */
74
+ showDeadlineAlerts?: boolean;
75
+ }
76
+
77
+ export const BreachNotificationManager: React.FC<BreachNotificationManagerProps> = ({
78
+ breachReports,
79
+ riskAssessments,
80
+ regulatoryNotifications,
81
+ onSelectBreach,
82
+ onRequestAssessment,
83
+ onRequestNotification,
84
+ title = "Breach Notification Manager",
85
+ description = "Manage data breach notifications and track compliance with NDPR requirements.",
86
+ className = "",
87
+ buttonClassName = "",
88
+ showBreachDetails = true,
89
+ showNotificationTimeline = true,
90
+ showDeadlineAlerts = true
91
+ }) => {
92
+ const [selectedBreachId, setSelectedBreachId] = useState<string | null>(null);
93
+ const [filteredBreaches, setFilteredBreaches] = useState<BreachReport[]>(breachReports);
94
+ const [statusFilter, setStatusFilter] = useState<string>('all');
95
+ const [searchTerm, setSearchTerm] = useState<string>('');
96
+ const [sortBy, setSortBy] = useState<string>('discoveredAt');
97
+ const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc');
98
+
99
+ // Update filtered breaches when filters change
100
+ useEffect(() => {
101
+ let filtered = [...breachReports];
102
+
103
+ // Apply status filter
104
+ if (statusFilter !== 'all') {
105
+ filtered = filtered.filter(breach => breach.status === statusFilter);
106
+ }
107
+
108
+ // Apply search filter
109
+ if (searchTerm) {
110
+ const term = searchTerm.toLowerCase();
111
+ filtered = filtered.filter(breach =>
112
+ breach.title.toLowerCase().includes(term) ||
113
+ breach.description.toLowerCase().includes(term) ||
114
+ breach.affectedSystems.some(system => system.toLowerCase().includes(term)) ||
115
+ breach.dataTypes.some(type => type.toLowerCase().includes(term))
116
+ );
117
+ }
118
+
119
+ // Apply sorting
120
+ filtered.sort((a, b) => {
121
+ let comparison = 0;
122
+
123
+ switch (sortBy) {
124
+ case 'title':
125
+ comparison = a.title.localeCompare(b.title);
126
+ break;
127
+ case 'discoveredAt':
128
+ comparison = a.discoveredAt - b.discoveredAt;
129
+ break;
130
+ case 'status':
131
+ comparison = a.status.localeCompare(b.status);
132
+ break;
133
+ case 'riskLevel':
134
+ const assessmentA = riskAssessments.find(assessment => assessment.breachId === a.id);
135
+ const assessmentB = riskAssessments.find(assessment => assessment.breachId === b.id);
136
+ const riskLevelA = assessmentA?.riskLevel || 'unknown';
137
+ const riskLevelB = assessmentB?.riskLevel || 'unknown';
138
+ comparison = riskLevelA.localeCompare(riskLevelB);
139
+ break;
140
+ default:
141
+ comparison = a.discoveredAt - b.discoveredAt;
142
+ }
143
+
144
+ return sortDirection === 'asc' ? comparison : -comparison;
145
+ });
146
+
147
+ setFilteredBreaches(filtered);
148
+ }, [breachReports, statusFilter, searchTerm, sortBy, sortDirection, riskAssessments]);
149
+
150
+ // Select the first breach if none is selected
151
+ useEffect(() => {
152
+ if (filteredBreaches.length > 0 && !selectedBreachId) {
153
+ setSelectedBreachId(filteredBreaches[0].id);
154
+ }
155
+ }, [filteredBreaches, selectedBreachId]);
156
+
157
+ // Handle breach selection
158
+ const handleSelectBreach = (breachId: string) => {
159
+ setSelectedBreachId(breachId);
160
+ if (onSelectBreach) {
161
+ onSelectBreach(breachId);
162
+ }
163
+ };
164
+
165
+ // Handle requesting a risk assessment
166
+ const handleRequestAssessment = () => {
167
+ if (selectedBreachId && onRequestAssessment) {
168
+ onRequestAssessment(selectedBreachId);
169
+ }
170
+ };
171
+
172
+ // Handle requesting a notification
173
+ const handleRequestNotification = () => {
174
+ if (selectedBreachId && onRequestNotification) {
175
+ onRequestNotification(selectedBreachId);
176
+ }
177
+ };
178
+
179
+ // Format a date from timestamp
180
+ const formatDate = (timestamp: number): string => {
181
+ return new Date(timestamp).toLocaleString();
182
+ };
183
+
184
+ // Get the selected breach
185
+ const selectedBreach = selectedBreachId
186
+ ? breachReports.find(breach => breach.id === selectedBreachId)
187
+ : null;
188
+
189
+ // Get the risk assessment for the selected breach
190
+ const selectedAssessment = selectedBreachId
191
+ ? riskAssessments.find(assessment => assessment.breachId === selectedBreachId)
192
+ : null;
193
+
194
+ // Get the notification for the selected breach
195
+ const selectedNotification = selectedBreachId
196
+ ? regulatoryNotifications.find(notification => notification.breachId === selectedBreachId)
197
+ : null;
198
+
199
+ // Calculate notification requirements for the selected breach
200
+ const notificationRequirements = selectedBreach
201
+ ? calculateNotificationRequirements(selectedBreach, selectedAssessment)
202
+ : null;
203
+
204
+ // Calculate notification requirements
205
+ function calculateNotificationRequirements(
206
+ breach: BreachReport,
207
+ assessment?: RiskAssessment | null
208
+ ): NotificationRequirement | null {
209
+ const result = calculateBreachSeverity(breach, assessment || undefined);
210
+
211
+ // Calculate the deadline (72 hours from discovery under NDPR)
212
+ const deadline = breach.discoveredAt + (result.timeframeHours * 60 * 60 * 1000);
213
+
214
+ return {
215
+ nitdaNotificationRequired: result.notificationRequired,
216
+ nitdaNotificationDeadline: deadline,
217
+ dataSubjectNotificationRequired: assessment?.highRisksToRightsAndFreedoms || false,
218
+ justification: result.justification
219
+ };
220
+ }
221
+
222
+ // Calculate hours remaining until deadline
223
+ function calculateHoursRemaining(deadline: number): number {
224
+ const now = Date.now();
225
+ const remaining = (deadline - now) / (60 * 60 * 1000);
226
+ return Number(remaining.toFixed(1));
227
+ }
228
+
229
+ // Render risk level badge
230
+ const renderRiskLevelBadge = (level?: 'low' | 'medium' | 'high' | 'critical') => {
231
+ if (!level) return null;
232
+
233
+ const colorClasses = {
234
+ low: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
235
+ medium: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200',
236
+ high: 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200',
237
+ critical: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'
238
+ };
239
+
240
+ return (
241
+ <span className={`px-2 py-1 rounded text-xs font-medium ${colorClasses[level]}`}>
242
+ {level.charAt(0).toUpperCase() + level.slice(1)}
243
+ </span>
244
+ );
245
+ };
246
+
247
+ // Render status badge
248
+ const renderStatusBadge = (status: string) => {
249
+ const colorClasses = {
250
+ ongoing: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
251
+ contained: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200',
252
+ resolved: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'
253
+ };
254
+
255
+ return (
256
+ <span className={`px-2 py-1 rounded text-xs font-medium ${colorClasses[status as keyof typeof colorClasses] || 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200'}`}>
257
+ {status.charAt(0).toUpperCase() + status.slice(1)}
258
+ </span>
259
+ );
260
+ };
261
+
262
+ // Render notification status
263
+ const renderNotificationStatus = () => {
264
+ if (!selectedBreach || !notificationRequirements) return null;
265
+
266
+ if (!notificationRequirements.nitdaNotificationRequired) {
267
+ return (
268
+ <div className="bg-green-50 dark:bg-green-900/20 p-3 rounded-md">
269
+ <p className="text-sm text-green-800 dark:text-green-200 font-medium">
270
+ Notification Not Required
271
+ </p>
272
+ <p className="text-xs text-green-700 dark:text-green-300 mt-1">
273
+ Based on the risk assessment, NITDA notification is not required for this breach.
274
+ </p>
275
+ </div>
276
+ );
277
+ }
278
+
279
+ if (selectedNotification) {
280
+ return (
281
+ <div className="bg-green-50 dark:bg-green-900/20 p-3 rounded-md">
282
+ <p className="text-sm text-green-800 dark:text-green-200 font-medium">
283
+ Notification Sent
284
+ </p>
285
+ <p className="text-xs text-green-700 dark:text-green-300 mt-1">
286
+ Notification was sent to NITDA on {formatDate(selectedNotification.sentAt)}.
287
+ </p>
288
+ </div>
289
+ );
290
+ }
291
+
292
+ const hoursRemaining = calculateHoursRemaining(notificationRequirements.nitdaNotificationDeadline);
293
+
294
+ if (hoursRemaining <= 0) {
295
+ return (
296
+ <div className="bg-red-50 dark:bg-red-900/20 p-3 rounded-md">
297
+ <p className="text-sm text-red-800 dark:text-red-200 font-medium">
298
+ Notification Deadline Passed
299
+ </p>
300
+ <p className="text-xs text-red-700 dark:text-red-300 mt-1">
301
+ The 72-hour deadline for NITDA notification has passed. Notification should be sent immediately.
302
+ </p>
303
+ </div>
304
+ );
305
+ }
306
+
307
+ if (hoursRemaining <= 24) {
308
+ return (
309
+ <div className="bg-red-50 dark:bg-red-900/20 p-3 rounded-md">
310
+ <p className="text-sm text-red-800 dark:text-red-200 font-medium">
311
+ Urgent: Notification Due Soon
312
+ </p>
313
+ <p className="text-xs text-red-700 dark:text-red-300 mt-1">
314
+ Only {hoursRemaining} hours remaining until the NITDA notification deadline.
315
+ </p>
316
+ </div>
317
+ );
318
+ }
319
+
320
+ return (
321
+ <div className="bg-yellow-50 dark:bg-yellow-900/20 p-3 rounded-md">
322
+ <p className="text-sm text-yellow-800 dark:text-yellow-200 font-medium">
323
+ Notification Required
324
+ </p>
325
+ <p className="text-xs text-yellow-700 dark:text-yellow-300 mt-1">
326
+ NITDA notification is required by {formatDate(notificationRequirements.nitdaNotificationDeadline)} ({hoursRemaining} hours remaining).
327
+ </p>
328
+ </div>
329
+ );
330
+ };
331
+
332
+ // Render notification timeline
333
+ const renderNotificationTimeline = () => {
334
+ if (!selectedBreach) return null;
335
+
336
+ const timeline = [
337
+ {
338
+ title: 'Breach Discovered',
339
+ date: selectedBreach.discoveredAt,
340
+ completed: true,
341
+ description: `Breach was discovered on ${formatDate(selectedBreach.discoveredAt)}.`
342
+ },
343
+ {
344
+ title: 'Risk Assessment',
345
+ date: selectedAssessment?.assessedAt,
346
+ completed: !!selectedAssessment,
347
+ description: selectedAssessment
348
+ ? `Risk assessment completed on ${formatDate(selectedAssessment.assessedAt)}.`
349
+ : 'Risk assessment has not been completed yet.'
350
+ }
351
+ ];
352
+
353
+ if (notificationRequirements?.nitdaNotificationRequired) {
354
+ timeline.push({
355
+ title: 'NITDA Notification',
356
+ date: selectedNotification?.sentAt,
357
+ completed: !!selectedNotification,
358
+ description: selectedNotification
359
+ ? `Notification sent to NITDA on ${formatDate(selectedNotification.sentAt)}.`
360
+ : `Notification must be sent to NITDA by ${formatDate(notificationRequirements.nitdaNotificationDeadline)}.`
361
+ });
362
+ }
363
+
364
+ if (notificationRequirements?.dataSubjectNotificationRequired) {
365
+ timeline.push({
366
+ title: 'Data Subject Notification',
367
+ date: undefined,
368
+ completed: false,
369
+ description: 'Notification to affected data subjects is required but has not been sent yet.'
370
+ });
371
+ }
372
+
373
+ return (
374
+ <div className="mt-6">
375
+ <h3 className="text-lg font-medium mb-4">Notification Timeline</h3>
376
+ <ol className="relative border-l border-gray-200 dark:border-gray-700">
377
+ {timeline.map((item, index) => (
378
+ <li key={index} className="mb-6 ml-4">
379
+ <div className={`absolute w-3 h-3 rounded-full mt-1.5 -left-1.5 border ${
380
+ item.completed
381
+ ? 'bg-green-500 border-green-500 dark:border-green-500'
382
+ : 'bg-gray-200 border-gray-200 dark:bg-gray-700 dark:border-gray-700'
383
+ }`}></div>
384
+ <time className="mb-1 text-sm font-normal leading-none text-gray-400 dark:text-gray-500">
385
+ {item.date ? formatDate(item.date) : 'Pending'}
386
+ </time>
387
+ <h4 className="text-sm font-semibold text-gray-900 dark:text-white">
388
+ {item.title}
389
+ </h4>
390
+ <p className="text-xs text-gray-500 dark:text-gray-400">
391
+ {item.description}
392
+ </p>
393
+ </li>
394
+ ))}
395
+ </ol>
396
+ </div>
397
+ );
398
+ };
399
+
400
+ return (
401
+ <div className={`bg-white dark:bg-gray-800 p-6 rounded-lg shadow-md ${className}`}>
402
+ <h2 className="text-xl font-bold mb-2">{title}</h2>
403
+ <p className="mb-6 text-gray-600 dark:text-gray-300">{description}</p>
404
+
405
+ {/* Filters and Search */}
406
+ <div className="mb-6 grid grid-cols-1 md:grid-cols-3 gap-4">
407
+ <div>
408
+ <label htmlFor="statusFilter" className="block text-sm font-medium mb-1">
409
+ Status Filter
410
+ </label>
411
+ <select
412
+ id="statusFilter"
413
+ value={statusFilter}
414
+ onChange={e => setStatusFilter(e.target.value)}
415
+ 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"
416
+ >
417
+ <option value="all">All Statuses</option>
418
+ <option value="ongoing">Ongoing</option>
419
+ <option value="contained">Contained</option>
420
+ <option value="resolved">Resolved</option>
421
+ </select>
422
+ </div>
423
+
424
+ <div>
425
+ <label htmlFor="sortBy" className="block text-sm font-medium mb-1">
426
+ Sort By
427
+ </label>
428
+ <select
429
+ id="sortBy"
430
+ value={sortBy}
431
+ onChange={e => setSortBy(e.target.value)}
432
+ 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"
433
+ >
434
+ <option value="discoveredAt">Discovery Date</option>
435
+ <option value="title">Title</option>
436
+ <option value="status">Status</option>
437
+ <option value="riskLevel">Risk Level</option>
438
+ </select>
439
+ </div>
440
+
441
+ <div>
442
+ <label htmlFor="searchTerm" className="block text-sm font-medium mb-1">
443
+ Search
444
+ </label>
445
+ <input
446
+ type="text"
447
+ id="searchTerm"
448
+ value={searchTerm}
449
+ onChange={e => setSearchTerm(e.target.value)}
450
+ placeholder="Search breaches..."
451
+ 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"
452
+ />
453
+ </div>
454
+ </div>
455
+
456
+ {/* Breach List and Details */}
457
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
458
+ {/* Breach List */}
459
+ <div className="md:col-span-1">
460
+ <h3 className="text-lg font-medium mb-3">Breach Reports</h3>
461
+
462
+ {filteredBreaches.length === 0 ? (
463
+ <p className="text-gray-500 dark:text-gray-400 text-sm">
464
+ No breach reports found.
465
+ </p>
466
+ ) : (
467
+ <div className="space-y-2 max-h-96 overflow-y-auto pr-2">
468
+ {filteredBreaches.map(breach => {
469
+ const assessment = riskAssessments.find(a => a.breachId === breach.id);
470
+ const notification = regulatoryNotifications.find(n => n.breachId === breach.id);
471
+ const requirements = calculateNotificationRequirements(breach, assessment);
472
+
473
+ // Calculate notification status for the list item
474
+ let notificationStatus = null;
475
+ if (requirements?.nitdaNotificationRequired) {
476
+ if (notification) {
477
+ notificationStatus = (
478
+ <span className="text-xs text-green-600 dark:text-green-400">
479
+ Notified
480
+ </span>
481
+ );
482
+ } else {
483
+ const hoursRemaining = calculateHoursRemaining(requirements.nitdaNotificationDeadline);
484
+ if (hoursRemaining <= 0) {
485
+ notificationStatus = (
486
+ <span className="text-xs text-red-600 dark:text-red-400 font-bold">
487
+ Overdue
488
+ </span>
489
+ );
490
+ } else if (hoursRemaining <= 24) {
491
+ notificationStatus = (
492
+ <span className="text-xs text-red-600 dark:text-red-400">
493
+ Urgent
494
+ </span>
495
+ );
496
+ } else {
497
+ notificationStatus = (
498
+ <span className="text-xs text-yellow-600 dark:text-yellow-400">
499
+ Required
500
+ </span>
501
+ );
502
+ }
503
+ }
504
+ } else {
505
+ notificationStatus = (
506
+ <span className="text-xs text-green-600 dark:text-green-400">
507
+ Not Required
508
+ </span>
509
+ );
510
+ }
511
+
512
+ return (
513
+ <div
514
+ key={breach.id}
515
+ className={`p-3 rounded-md cursor-pointer ${
516
+ selectedBreachId === breach.id
517
+ ? 'bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800'
518
+ : 'bg-gray-50 dark:bg-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600'
519
+ }`}
520
+ onClick={() => handleSelectBreach(breach.id)}
521
+ >
522
+ <div className="flex justify-between items-start mb-1">
523
+ <h4 className="font-medium text-sm">{breach.title}</h4>
524
+ {renderStatusBadge(breach.status)}
525
+ </div>
526
+ <p className="text-xs text-gray-500 dark:text-gray-400 mb-1">
527
+ Discovered: {new Date(breach.discoveredAt).toLocaleDateString()}
528
+ </p>
529
+ <div className="flex justify-between items-center mt-2">
530
+ <div>
531
+ {assessment && renderRiskLevelBadge(assessment.riskLevel)}
532
+ </div>
533
+ <div>
534
+ {notificationStatus}
535
+ </div>
536
+ </div>
537
+ </div>
538
+ );
539
+ })}
540
+ </div>
541
+ )}
542
+ </div>
543
+
544
+ {/* Breach Details */}
545
+ <div className="md:col-span-2">
546
+ {selectedBreach ? (
547
+ <div>
548
+ <div className="flex justify-between items-start mb-4">
549
+ <h3 className="text-lg font-medium">{selectedBreach.title}</h3>
550
+ {renderStatusBadge(selectedBreach.status)}
551
+ </div>
552
+
553
+ {/* Notification Status Alert */}
554
+ {showDeadlineAlerts && (
555
+ <div className="mb-4">
556
+ {renderNotificationStatus()}
557
+ </div>
558
+ )}
559
+
560
+ {/* Breach Details */}
561
+ {showBreachDetails && (
562
+ <div className="mb-6">
563
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
564
+ <div>
565
+ <p className="text-sm"><span className="font-medium">Discovered:</span> {formatDate(selectedBreach.discoveredAt)}</p>
566
+ {selectedBreach.occurredAt && (
567
+ <p className="text-sm"><span className="font-medium">Occurred:</span> {formatDate(selectedBreach.occurredAt)}</p>
568
+ )}
569
+ <p className="text-sm"><span className="font-medium">Reporter:</span> {selectedBreach.reporter.name}</p>
570
+ </div>
571
+ <div>
572
+ <p className="text-sm"><span className="font-medium">Affected Systems:</span> {selectedBreach.affectedSystems.join(', ')}</p>
573
+ <p className="text-sm"><span className="font-medium">Data Types:</span> {selectedBreach.dataTypes.join(', ')}</p>
574
+ <p className="text-sm">
575
+ <span className="font-medium">Affected Subjects:</span> {selectedBreach.estimatedAffectedSubjects || 'Unknown'}
576
+ </p>
577
+ </div>
578
+ </div>
579
+
580
+ <div className="mb-4">
581
+ <p className="text-sm font-medium">Description:</p>
582
+ <p className="text-sm text-gray-600 dark:text-gray-300 bg-gray-50 dark:bg-gray-700 p-2 rounded-md mt-1">
583
+ {selectedBreach.description}
584
+ </p>
585
+ </div>
586
+
587
+ {selectedBreach.initialActions && (
588
+ <div>
589
+ <p className="text-sm font-medium">Initial Actions Taken:</p>
590
+ <p className="text-sm text-gray-600 dark:text-gray-300 bg-gray-50 dark:bg-gray-700 p-2 rounded-md mt-1">
591
+ {selectedBreach.initialActions}
592
+ </p>
593
+ </div>
594
+ )}
595
+ </div>
596
+ )}
597
+
598
+ {/* Risk Assessment Summary */}
599
+ <div className="mb-6">
600
+ <h3 className="text-lg font-medium mb-3">Risk Assessment</h3>
601
+
602
+ {selectedAssessment ? (
603
+ <div className="bg-gray-50 dark:bg-gray-700 p-3 rounded-md">
604
+ <div className="flex justify-between items-center mb-2">
605
+ <p className="text-sm font-medium">Risk Level:</p>
606
+ {renderRiskLevelBadge(selectedAssessment.riskLevel)}
607
+ </div>
608
+ <p className="text-sm mb-2">
609
+ <span className="font-medium">Risk Score:</span> {selectedAssessment.overallRiskScore} / 5
610
+ </p>
611
+ <p className="text-sm mb-2">
612
+ <span className="font-medium">Risks to Rights and Freedoms:</span> {selectedAssessment.risksToRightsAndFreedoms ? 'Yes' : 'No'}
613
+ </p>
614
+ <p className="text-sm mb-2">
615
+ <span className="font-medium">High Risks to Rights and Freedoms:</span> {selectedAssessment.highRisksToRightsAndFreedoms ? 'Yes' : 'No'}
616
+ </p>
617
+ <p className="text-sm mb-1">
618
+ <span className="font-medium">Justification:</span>
619
+ </p>
620
+ <p className="text-xs text-gray-600 dark:text-gray-300 bg-white dark:bg-gray-800 p-2 rounded-md">
621
+ {selectedAssessment.justification}
622
+ </p>
623
+ </div>
624
+ ) : (
625
+ <div className="bg-yellow-50 dark:bg-yellow-900/20 p-3 rounded-md">
626
+ <p className="text-sm text-yellow-800 dark:text-yellow-200">
627
+ Risk assessment has not been conducted yet.
628
+ </p>
629
+ <button
630
+ onClick={handleRequestAssessment}
631
+ className={`mt-2 px-3 py-1 text-xs bg-yellow-600 text-white rounded hover:bg-yellow-700 ${buttonClassName}`}
632
+ >
633
+ Conduct Risk Assessment
634
+ </button>
635
+ </div>
636
+ )}
637
+ </div>
638
+
639
+ {/* Notification Status */}
640
+ <div className="mb-6">
641
+ <h3 className="text-lg font-medium mb-3">Notification Status</h3>
642
+
643
+ {selectedNotification ? (
644
+ <div className="bg-gray-50 dark:bg-gray-700 p-3 rounded-md">
645
+ <p className="text-sm mb-2">
646
+ <span className="font-medium">Notification Sent:</span> {formatDate(selectedNotification.sentAt)}
647
+ </p>
648
+ <p className="text-sm mb-2">
649
+ <span className="font-medium">Method:</span> {selectedNotification.method.charAt(0).toUpperCase() + selectedNotification.method.slice(1)}
650
+ </p>
651
+ {selectedNotification.referenceNumber && (
652
+ <p className="text-sm mb-2">
653
+ <span className="font-medium">Reference Number:</span> {selectedNotification.referenceNumber}
654
+ </p>
655
+ )}
656
+ {selectedNotification.nitdaContact && (
657
+ <p className="text-sm mb-2">
658
+ <span className="font-medium">NITDA Contact:</span> {selectedNotification.nitdaContact.name}
659
+ </p>
660
+ )}
661
+ </div>
662
+ ) : (
663
+ <div>
664
+ {notificationRequirements?.nitdaNotificationRequired ? (
665
+ <div className="bg-yellow-50 dark:bg-yellow-900/20 p-3 rounded-md">
666
+ <p className="text-sm text-yellow-800 dark:text-yellow-200">
667
+ NITDA notification is required but has not been sent yet.
668
+ </p>
669
+ <button
670
+ onClick={handleRequestNotification}
671
+ className={`mt-2 px-3 py-1 text-xs bg-yellow-600 text-white rounded hover:bg-yellow-700 ${buttonClassName}`}
672
+ >
673
+ Generate Notification
674
+ </button>
675
+ </div>
676
+ ) : (
677
+ <div className="bg-green-50 dark:bg-green-900/20 p-3 rounded-md">
678
+ <p className="text-sm text-green-800 dark:text-green-200">
679
+ NITDA notification is not required for this breach.
680
+ </p>
681
+ </div>
682
+ )}
683
+ </div>
684
+ )}
685
+ </div>
686
+
687
+ {/* Notification Timeline */}
688
+ {showNotificationTimeline && renderNotificationTimeline()}
689
+ </div>
690
+ ) : (
691
+ <div className="flex items-center justify-center h-64 bg-gray-50 dark:bg-gray-700 rounded-md">
692
+ <p className="text-gray-500 dark:text-gray-400">
693
+ Select a breach to view details
694
+ </p>
695
+ </div>
696
+ )}
697
+ </div>
698
+ </div>
699
+ </div>
700
+ );
701
+ };