@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.
- package/.claude/settings.local.json +20 -0
- package/.eslintrc.json +10 -0
- package/.github/workflows/ci.yml +36 -0
- package/.github/workflows/nextjs.yml +104 -0
- package/.husky/commit-msg +4 -0
- package/.husky/pre-commit +4 -0
- package/.lintstagedrc.js +4 -0
- package/.nvmrc +1 -0
- package/.versionrc +17 -0
- package/CHANGELOG.md +16 -0
- package/CLAUDE.md +90 -0
- package/CNAME +1 -0
- package/CONTRIBUTING.md +87 -0
- package/README.md +84 -447
- package/RELEASE-NOTES-v1.0.0.md +140 -0
- package/RELEASE-NOTES-v1.0.1.md +69 -0
- package/SECURITY.md +21 -0
- package/commitlint.config.js +36 -0
- package/components.json +21 -0
- package/eslint.config.mjs +16 -0
- package/jest.config.js +31 -0
- package/jest.setup.js +15 -0
- package/next.config.js +15 -0
- package/next.config.ts +62 -0
- package/package.json +70 -52
- package/packages/ndpr-toolkit/README.md +467 -0
- package/packages/ndpr-toolkit/jest.config.js +23 -0
- package/packages/ndpr-toolkit/package-lock.json +8197 -0
- package/packages/ndpr-toolkit/package.json +71 -0
- package/packages/ndpr-toolkit/rollup.config.js +34 -0
- package/packages/ndpr-toolkit/src/__tests__/components/consent/ConsentBanner.test.tsx +119 -0
- package/packages/ndpr-toolkit/src/__tests__/components/consent/ConsentManager.test.tsx +122 -0
- package/packages/ndpr-toolkit/src/__tests__/components/consent/ConsentStorage.test.tsx +270 -0
- package/packages/ndpr-toolkit/src/__tests__/components/dsr/DSRDashboard.test.tsx +199 -0
- package/packages/ndpr-toolkit/src/__tests__/components/dsr/DSRRequestForm.test.tsx +224 -0
- package/packages/ndpr-toolkit/src/__tests__/components/dsr/DSRTracker.test.tsx +104 -0
- package/packages/ndpr-toolkit/src/__tests__/hooks/useConsent.test.tsx +161 -0
- package/packages/ndpr-toolkit/src/__tests__/hooks/useDSR.test.tsx +330 -0
- package/packages/ndpr-toolkit/src/__tests__/utils/breach.test.ts +149 -0
- package/packages/ndpr-toolkit/src/__tests__/utils/consent.test.ts +88 -0
- package/packages/ndpr-toolkit/src/__tests__/utils/dpia.test.ts +160 -0
- package/packages/ndpr-toolkit/src/__tests__/utils/dsr.test.ts +110 -0
- package/packages/ndpr-toolkit/src/__tests__/utils/privacy.test.ts +97 -0
- package/packages/ndpr-toolkit/src/components/breach/BreachNotificationManager.tsx +701 -0
- package/packages/ndpr-toolkit/src/components/breach/BreachReportForm.tsx +631 -0
- package/packages/ndpr-toolkit/src/components/breach/BreachRiskAssessment.tsx +569 -0
- package/packages/ndpr-toolkit/src/components/breach/RegulatoryReportGenerator.tsx +496 -0
- package/packages/ndpr-toolkit/src/components/consent/ConsentBanner.tsx +270 -0
- package/packages/ndpr-toolkit/src/components/consent/ConsentManager.tsx +217 -0
- package/packages/ndpr-toolkit/src/components/consent/ConsentStorage.tsx +206 -0
- package/packages/ndpr-toolkit/src/components/dpia/DPIAQuestionnaire.tsx +342 -0
- package/packages/ndpr-toolkit/src/components/dpia/DPIAReport.tsx +373 -0
- package/packages/ndpr-toolkit/src/components/dpia/StepIndicator.tsx +174 -0
- package/packages/ndpr-toolkit/src/components/dsr/DSRDashboard.tsx +717 -0
- package/packages/ndpr-toolkit/src/components/dsr/DSRRequestForm.tsx +476 -0
- package/packages/ndpr-toolkit/src/components/dsr/DSRTracker.tsx +620 -0
- package/packages/ndpr-toolkit/src/components/policy/PolicyExporter.tsx +541 -0
- package/packages/ndpr-toolkit/src/components/policy/PolicyGenerator.tsx +454 -0
- package/packages/ndpr-toolkit/src/components/policy/PolicyPreview.tsx +333 -0
- package/packages/ndpr-toolkit/src/hooks/useBreach.ts +409 -0
- package/packages/ndpr-toolkit/src/hooks/useConsent.ts +263 -0
- package/packages/ndpr-toolkit/src/hooks/useDPIA.ts +457 -0
- package/packages/ndpr-toolkit/src/hooks/useDSR.ts +236 -0
- package/packages/ndpr-toolkit/src/hooks/usePrivacyPolicy.ts +428 -0
- package/{dist/index.d.ts → packages/ndpr-toolkit/src/index.ts} +13 -0
- package/packages/ndpr-toolkit/src/setupTests.ts +5 -0
- package/packages/ndpr-toolkit/src/types/breach.ts +283 -0
- package/packages/ndpr-toolkit/src/types/consent.ts +111 -0
- package/packages/ndpr-toolkit/src/types/dpia.ts +236 -0
- package/packages/ndpr-toolkit/src/types/dsr.ts +192 -0
- package/packages/ndpr-toolkit/src/types/index.ts +42 -0
- package/packages/ndpr-toolkit/src/types/privacy.ts +246 -0
- package/packages/ndpr-toolkit/src/utils/breach.ts +122 -0
- package/packages/ndpr-toolkit/src/utils/consent.ts +51 -0
- package/packages/ndpr-toolkit/src/utils/dpia.ts +104 -0
- package/packages/ndpr-toolkit/src/utils/dsr.ts +77 -0
- package/packages/ndpr-toolkit/src/utils/privacy.ts +100 -0
- package/packages/ndpr-toolkit/tsconfig.json +23 -0
- package/postcss.config.mjs +5 -0
- package/public/NDPR TOOLKIT.svg +1 -0
- package/public/favicon/android-chrome-192x192.png +0 -0
- package/public/favicon/android-chrome-512x512.png +0 -0
- package/public/favicon/apple-touch-icon.png +0 -0
- package/public/favicon/favicon-16x16.png +0 -0
- package/public/favicon/favicon-32x32.png +0 -0
- package/public/favicon/site.webmanifest +1 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/ndpr-toolkit-logo.svg +108 -0
- package/public/next.svg +1 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/src/__tests__/example.test.ts +13 -0
- package/src/__tests__/requestService.test.ts +57 -0
- package/src/app/accessibility.css +70 -0
- package/src/app/docs/components/DocLayout.tsx +267 -0
- package/src/app/docs/components/breach-notification/page.tsx +797 -0
- package/src/app/docs/components/consent-management/page.tsx +576 -0
- package/src/app/docs/components/data-subject-rights/page.tsx +511 -0
- package/src/app/docs/components/dpia-questionnaire/layout.tsx +15 -0
- package/src/app/docs/components/dpia-questionnaire/metadata.ts +31 -0
- package/src/app/docs/components/dpia-questionnaire/page.tsx +666 -0
- package/src/app/docs/components/hooks/page.tsx +305 -0
- package/src/app/docs/components/page.tsx +84 -0
- package/src/app/docs/components/privacy-policy-generator/page.tsx +634 -0
- package/src/app/docs/guides/breach-notification-process/components/BestPractices.tsx +123 -0
- package/src/app/docs/guides/breach-notification-process/components/ImplementationSteps.tsx +328 -0
- package/src/app/docs/guides/breach-notification-process/components/Introduction.tsx +28 -0
- package/src/app/docs/guides/breach-notification-process/components/NotificationTimeline.tsx +91 -0
- package/src/app/docs/guides/breach-notification-process/components/Resources.tsx +118 -0
- package/src/app/docs/guides/breach-notification-process/page.tsx +39 -0
- package/src/app/docs/guides/conducting-dpia/page.tsx +593 -0
- package/src/app/docs/guides/data-subject-requests/page.tsx +666 -0
- package/src/app/docs/guides/managing-consent/page.tsx +738 -0
- package/src/app/docs/guides/ndpr-compliance-checklist/components/ComplianceChecklist.tsx +296 -0
- package/src/app/docs/guides/ndpr-compliance-checklist/components/ImplementationTools.tsx +145 -0
- package/src/app/docs/guides/ndpr-compliance-checklist/components/Introduction.tsx +33 -0
- package/src/app/docs/guides/ndpr-compliance-checklist/components/KeyRequirements.tsx +99 -0
- package/src/app/docs/guides/ndpr-compliance-checklist/components/Resources.tsx +159 -0
- package/src/app/docs/guides/ndpr-compliance-checklist/page.tsx +38 -0
- package/src/app/docs/guides/page.tsx +67 -0
- package/src/app/docs/layout.tsx +15 -0
- package/src/app/docs/metadata.ts +31 -0
- package/src/app/docs/page.tsx +572 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +123 -0
- package/src/app/layout.tsx +37 -0
- package/src/app/ndpr-demos/breach/page.tsx +354 -0
- package/src/app/ndpr-demos/consent/page.tsx +366 -0
- package/src/app/ndpr-demos/dpia/page.tsx +495 -0
- package/src/app/ndpr-demos/dsr/page.tsx +280 -0
- package/src/app/ndpr-demos/page.tsx +73 -0
- package/src/app/ndpr-demos/policy/page.tsx +771 -0
- package/src/app/page.tsx +452 -0
- package/src/components/ErrorBoundary.tsx +90 -0
- package/src/components/breach-notification/BreachNotificationForm.tsx +479 -0
- package/src/components/consent/ConsentBanner.tsx +159 -0
- package/src/components/data-subject-rights/DataSubjectRequestForm.tsx +419 -0
- package/src/components/docs/DocLayout.tsx +289 -0
- package/src/components/docs/index.ts +2 -0
- package/src/components/dpia/DPIAQuestionnaire.tsx +483 -0
- package/src/components/privacy-policy/PolicyGenerator.tsx +1062 -0
- package/src/components/privacy-policy/data.ts +98 -0
- package/src/components/privacy-policy/shared/CheckboxField.tsx +38 -0
- package/src/components/privacy-policy/shared/CheckboxGroup.tsx +85 -0
- package/src/components/privacy-policy/shared/FormField.tsx +79 -0
- package/src/components/privacy-policy/shared/StepIndicator.tsx +86 -0
- package/src/components/privacy-policy/steps/CustomSectionsStep.tsx +335 -0
- package/src/components/privacy-policy/steps/DataCollectionStep.tsx +231 -0
- package/src/components/privacy-policy/steps/DataSharingStep.tsx +418 -0
- package/src/components/privacy-policy/steps/OrganizationInfoStep.tsx +202 -0
- package/src/components/privacy-policy/steps/PolicyPreviewStep.tsx +172 -0
- package/src/components/ui/Badge.tsx +46 -0
- package/src/components/ui/Button.tsx +59 -0
- package/src/components/ui/Card.tsx +92 -0
- package/src/components/ui/Checkbox.tsx +57 -0
- package/src/components/ui/FormField.tsx +50 -0
- package/src/components/ui/Input.tsx +38 -0
- package/src/components/ui/Loading.tsx +201 -0
- package/src/components/ui/Select.tsx +42 -0
- package/src/components/ui/TextArea.tsx +38 -0
- package/src/components/ui/label.tsx +24 -0
- package/src/components/ui/switch.tsx +31 -0
- package/src/components/ui/tabs.tsx +66 -0
- package/src/hooks/useConsent.ts +64 -0
- package/src/hooks/useLoadingState.ts +85 -0
- package/src/lib/consentService.ts +137 -0
- package/src/lib/dpiaQuestions.ts +148 -0
- package/src/lib/requestService.ts +75 -0
- package/src/lib/sanitize.ts +108 -0
- package/src/lib/storage.ts +222 -0
- package/src/lib/utils.ts +6 -0
- package/src/types/html-to-docx.d.ts +30 -0
- package/src/types/index.ts +72 -0
- package/tailwind.config.ts +65 -0
- package/tsconfig.json +41 -0
- package/dist/components/breach/BreachNotificationManager.d.ts +0 -62
- package/dist/components/breach/BreachReportForm.d.ts +0 -66
- package/dist/components/breach/BreachRiskAssessment.d.ts +0 -50
- package/dist/components/breach/RegulatoryReportGenerator.d.ts +0 -94
- package/dist/components/consent/ConsentBanner.d.ts +0 -79
- package/dist/components/consent/ConsentManager.d.ts +0 -73
- package/dist/components/consent/ConsentStorage.d.ts +0 -41
- package/dist/components/dpia/DPIAQuestionnaire.d.ts +0 -70
- package/dist/components/dpia/DPIAReport.d.ts +0 -40
- package/dist/components/dpia/StepIndicator.d.ts +0 -64
- package/dist/components/dsr/DSRDashboard.d.ts +0 -58
- package/dist/components/dsr/DSRRequestForm.d.ts +0 -74
- package/dist/components/dsr/DSRTracker.d.ts +0 -56
- package/dist/components/policy/PolicyExporter.d.ts +0 -65
- package/dist/components/policy/PolicyGenerator.d.ts +0 -54
- package/dist/components/policy/PolicyPreview.d.ts +0 -71
- package/dist/hooks/useBreach.d.ts +0 -97
- package/dist/hooks/useConsent.d.ts +0 -63
- package/dist/hooks/useDPIA.d.ts +0 -92
- package/dist/hooks/useDSR.d.ts +0 -72
- package/dist/hooks/usePrivacyPolicy.d.ts +0 -87
- package/dist/index.esm.js +0 -2
- package/dist/index.esm.js.map +0 -1
- package/dist/index.js +0 -2
- package/dist/index.js.map +0 -1
- package/dist/setupTests.d.ts +0 -2
- package/dist/types/breach.d.ts +0 -239
- package/dist/types/consent.d.ts +0 -95
- package/dist/types/dpia.d.ts +0 -196
- package/dist/types/dsr.d.ts +0 -162
- package/dist/types/privacy.d.ts +0 -204
- package/dist/utils/breach.d.ts +0 -14
- package/dist/utils/consent.d.ts +0 -10
- package/dist/utils/dpia.d.ts +0 -12
- package/dist/utils/dsr.d.ts +0 -11
- package/dist/utils/privacy.d.ts +0 -12
|
@@ -0,0 +1,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
|
+
};
|