@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,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
|
+
};
|