feat: Consent-Service Module nach Compliance migriert (DSR, E-Mail-Templates, Legal Docs, Banner)
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 36s
CI / test-python-backend-compliance (push) Successful in 31s
CI / test-python-document-crawler (push) Successful in 23s
CI / test-python-dsms-gateway (push) Successful in 18s
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 36s
CI / test-python-backend-compliance (push) Successful in 31s
CI / test-python-document-crawler (push) Successful in 23s
CI / test-python-dsms-gateway (push) Successful in 18s
5-Phasen-Migration: Go consent-service Proxies durch native Python/FastAPI ersetzt. Phase 1 — DSR (Betroffenenrechte): 6 Tabellen, 30 Endpoints, Frontend-API umgestellt Phase 2 — E-Mail-Templates: 5 Tabellen, 20 Endpoints, neues Frontend, SDK_STEPS erweitert Phase 3 — Legal Documents Extension: User Consents, Audit Log, Cookie-Kategorien Phase 4 — Banner Consent: Device-Consents, Site-Configs, Kategorien, Vendors Phase 5 — Cleanup: DSR-Proxy aus main.py entfernt, Frontend-URLs aktualisiert 148 neue Tests (50 + 47 + 26 + 25), alle bestanden. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,507 +1,121 @@
|
||||
/**
|
||||
* DSR API Client
|
||||
*
|
||||
* API client for Data Subject Request management
|
||||
* Connects to the Go Consent Service backend
|
||||
* API client for Data Subject Request management.
|
||||
* Connects to the native compliance backend (Python/FastAPI).
|
||||
*/
|
||||
|
||||
import {
|
||||
DSRRequest,
|
||||
DSRListResponse,
|
||||
DSRFilters,
|
||||
DSRCreateRequest,
|
||||
DSRUpdateRequest,
|
||||
DSRVerifyIdentityRequest,
|
||||
DSRCompleteRequest,
|
||||
DSRRejectRequest,
|
||||
DSRExtendDeadlineRequest,
|
||||
DSRSendCommunicationRequest,
|
||||
DSRCommunication,
|
||||
DSRAuditEntry,
|
||||
DSRStatistics,
|
||||
DSRDataExport,
|
||||
DSRErasureChecklist
|
||||
} from './types'
|
||||
|
||||
// =============================================================================
|
||||
// CONFIGURATION
|
||||
// =============================================================================
|
||||
|
||||
const DSR_API_BASE = process.env.NEXT_PUBLIC_CONSENT_SERVICE_URL || 'http://localhost:8081'
|
||||
const API_TIMEOUT = 30000 // 30 seconds
|
||||
|
||||
// =============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
function getTenantId(): string {
|
||||
// In a real app, this would come from auth context or localStorage
|
||||
if (typeof window !== 'undefined') {
|
||||
return localStorage.getItem('tenantId') || 'default-tenant'
|
||||
}
|
||||
return 'default-tenant'
|
||||
}
|
||||
|
||||
function getAuthHeaders(): HeadersInit {
|
||||
const headers: HeadersInit = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': getTenantId()
|
||||
}
|
||||
|
||||
// Add auth token if available
|
||||
if (typeof window !== 'undefined') {
|
||||
const token = localStorage.getItem('authToken')
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
async function fetchWithTimeout<T>(
|
||||
url: string,
|
||||
options: RequestInit = {},
|
||||
timeout: number = API_TIMEOUT
|
||||
): Promise<T> {
|
||||
const controller = new AbortController()
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeout)
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
...getAuthHeaders(),
|
||||
...options.headers
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorBody = await response.text()
|
||||
let errorMessage = `HTTP ${response.status}: ${response.statusText}`
|
||||
try {
|
||||
const errorJson = JSON.parse(errorBody)
|
||||
errorMessage = errorJson.error || errorJson.message || errorMessage
|
||||
} catch {
|
||||
// Keep the HTTP status message
|
||||
}
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
// Handle empty responses
|
||||
const contentType = response.headers.get('content-type')
|
||||
if (contentType && contentType.includes('application/json')) {
|
||||
return response.json()
|
||||
}
|
||||
|
||||
return {} as T
|
||||
} finally {
|
||||
clearTimeout(timeoutId)
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DSR LIST & CRUD
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Fetch all DSR requests with optional filters
|
||||
*/
|
||||
export async function fetchDSRList(filters?: DSRFilters): Promise<DSRListResponse> {
|
||||
const params = new URLSearchParams()
|
||||
|
||||
if (filters) {
|
||||
if (filters.status) {
|
||||
const statuses = Array.isArray(filters.status) ? filters.status : [filters.status]
|
||||
statuses.forEach(s => params.append('status', s))
|
||||
}
|
||||
if (filters.type) {
|
||||
const types = Array.isArray(filters.type) ? filters.type : [filters.type]
|
||||
types.forEach(t => params.append('type', t))
|
||||
}
|
||||
if (filters.priority) params.set('priority', filters.priority)
|
||||
if (filters.assignedTo) params.set('assignedTo', filters.assignedTo)
|
||||
if (filters.overdue !== undefined) params.set('overdue', String(filters.overdue))
|
||||
if (filters.search) params.set('search', filters.search)
|
||||
if (filters.dateFrom) params.set('dateFrom', filters.dateFrom)
|
||||
if (filters.dateTo) params.set('dateTo', filters.dateTo)
|
||||
}
|
||||
|
||||
const queryString = params.toString()
|
||||
const url = `${DSR_API_BASE}/api/v1/admin/dsr${queryString ? `?${queryString}` : ''}`
|
||||
|
||||
return fetchWithTimeout<DSRListResponse>(url)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a single DSR request by ID
|
||||
*/
|
||||
export async function fetchDSR(id: string): Promise<DSRRequest> {
|
||||
return fetchWithTimeout<DSRRequest>(`${DSR_API_BASE}/api/v1/admin/dsr/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new DSR request
|
||||
*/
|
||||
export async function createDSR(request: DSRCreateRequest): Promise<DSRRequest> {
|
||||
return fetchWithTimeout<DSRRequest>(`${DSR_API_BASE}/api/v1/admin/dsr`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(request)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a DSR request
|
||||
*/
|
||||
export async function updateDSR(id: string, update: DSRUpdateRequest): Promise<DSRRequest> {
|
||||
return fetchWithTimeout<DSRRequest>(`${DSR_API_BASE}/api/v1/admin/dsr/${id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(update)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a DSR request (soft delete - marks as cancelled)
|
||||
*/
|
||||
export async function deleteDSR(id: string): Promise<void> {
|
||||
await fetchWithTimeout<void>(`${DSR_API_BASE}/api/v1/admin/dsr/${id}`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DSR WORKFLOW ACTIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Verify the identity of the requester
|
||||
*/
|
||||
export async function verifyIdentity(
|
||||
dsrId: string,
|
||||
verification: DSRVerifyIdentityRequest
|
||||
): Promise<DSRRequest> {
|
||||
return fetchWithTimeout<DSRRequest>(
|
||||
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/verify-identity`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify(verification)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete a DSR request
|
||||
*/
|
||||
export async function completeDSR(
|
||||
dsrId: string,
|
||||
completion?: DSRCompleteRequest
|
||||
): Promise<DSRRequest> {
|
||||
return fetchWithTimeout<DSRRequest>(
|
||||
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/complete`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify(completion || {})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject a DSR request
|
||||
*/
|
||||
export async function rejectDSR(
|
||||
dsrId: string,
|
||||
rejection: DSRRejectRequest
|
||||
): Promise<DSRRequest> {
|
||||
return fetchWithTimeout<DSRRequest>(
|
||||
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/reject`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify(rejection)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend the deadline for a DSR request
|
||||
*/
|
||||
export async function extendDeadline(
|
||||
dsrId: string,
|
||||
extension: DSRExtendDeadlineRequest
|
||||
): Promise<DSRRequest> {
|
||||
return fetchWithTimeout<DSRRequest>(
|
||||
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/extend`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify(extension)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign a DSR request to a user
|
||||
*/
|
||||
export async function assignDSR(
|
||||
dsrId: string,
|
||||
assignedTo: string
|
||||
): Promise<DSRRequest> {
|
||||
return fetchWithTimeout<DSRRequest>(
|
||||
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/assign`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ assignedTo })
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// COMMUNICATION
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Get all communications for a DSR request
|
||||
*/
|
||||
export async function getCommunications(dsrId: string): Promise<DSRCommunication[]> {
|
||||
return fetchWithTimeout<DSRCommunication[]>(
|
||||
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/communications`
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a communication (email, letter, internal note)
|
||||
*/
|
||||
export async function sendCommunication(
|
||||
dsrId: string,
|
||||
communication: DSRSendCommunicationRequest
|
||||
): Promise<DSRCommunication> {
|
||||
return fetchWithTimeout<DSRCommunication>(
|
||||
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/send-communication`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify(communication)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// AUDIT LOG
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Get audit log entries for a DSR request
|
||||
*/
|
||||
export async function getAuditLog(dsrId: string): Promise<DSRAuditEntry[]> {
|
||||
return fetchWithTimeout<DSRAuditEntry[]>(
|
||||
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/audit`
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// STATISTICS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Get DSR statistics
|
||||
*/
|
||||
export async function getDSRStatistics(): Promise<DSRStatistics> {
|
||||
return fetchWithTimeout<DSRStatistics>(
|
||||
`${DSR_API_BASE}/api/v1/admin/dsr/statistics`
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DATA EXPORT (Art. 15, 20)
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Generate data export for Art. 15 (access) or Art. 20 (portability)
|
||||
*/
|
||||
export async function generateDataExport(
|
||||
dsrId: string,
|
||||
format: 'json' | 'csv' | 'xml' | 'pdf' = 'json'
|
||||
): Promise<DSRDataExport> {
|
||||
return fetchWithTimeout<DSRDataExport>(
|
||||
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/export`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ format })
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Download generated data export
|
||||
*/
|
||||
export async function downloadDataExport(dsrId: string): Promise<Blob> {
|
||||
const response = await fetch(
|
||||
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/export/download`,
|
||||
{
|
||||
headers: getAuthHeaders()
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Download failed: ${response.statusText}`)
|
||||
}
|
||||
|
||||
return response.blob()
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ERASURE CHECKLIST (Art. 17)
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Get the erasure checklist for an Art. 17 request
|
||||
*/
|
||||
export async function getErasureChecklist(dsrId: string): Promise<DSRErasureChecklist> {
|
||||
return fetchWithTimeout<DSRErasureChecklist>(
|
||||
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/erasure-checklist`
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the erasure checklist
|
||||
*/
|
||||
export async function updateErasureChecklist(
|
||||
dsrId: string,
|
||||
checklist: DSRErasureChecklist
|
||||
): Promise<DSRErasureChecklist> {
|
||||
return fetchWithTimeout<DSRErasureChecklist>(
|
||||
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/erasure-checklist`,
|
||||
{
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(checklist)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// EMAIL TEMPLATES
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Get available email templates
|
||||
*/
|
||||
export async function getEmailTemplates(): Promise<{ id: string; name: string; stage: string }[]> {
|
||||
return fetchWithTimeout<{ id: string; name: string; stage: string }[]>(
|
||||
`${DSR_API_BASE}/api/v1/admin/dsr/email-templates`
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Preview an email with variables filled in
|
||||
*/
|
||||
export async function previewEmail(
|
||||
templateId: string,
|
||||
dsrId: string
|
||||
): Promise<{ subject: string; body: string }> {
|
||||
return fetchWithTimeout<{ subject: string; body: string }>(
|
||||
`${DSR_API_BASE}/api/v1/admin/dsr/email-templates/${templateId}/preview`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ dsrId })
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SDK API FUNCTIONS (via Next.js proxy to ai-compliance-sdk)
|
||||
// SDK API FUNCTIONS (via Next.js proxy to compliance backend)
|
||||
// =============================================================================
|
||||
|
||||
interface BackendDSR {
|
||||
id: string
|
||||
tenant_id: string
|
||||
namespace_id?: string
|
||||
request_number: string
|
||||
request_type: string
|
||||
status: string
|
||||
subject_name: string
|
||||
subject_email: string
|
||||
subject_identifier?: string
|
||||
request_description: string
|
||||
request_channel: string
|
||||
priority: string
|
||||
requester_name: string
|
||||
requester_email: string
|
||||
requester_phone?: string
|
||||
requester_address?: string
|
||||
requester_customer_id?: string
|
||||
source: string
|
||||
source_details?: string
|
||||
request_text?: string
|
||||
notes?: string
|
||||
internal_notes?: string
|
||||
received_at: string
|
||||
verified_at?: string
|
||||
verification_method?: string
|
||||
deadline_at: string
|
||||
extended_deadline_at?: string
|
||||
extension_reason?: string
|
||||
completed_at?: string
|
||||
response_sent: boolean
|
||||
response_sent_at?: string
|
||||
response_method?: string
|
||||
rejection_reason?: string
|
||||
notes?: string
|
||||
affected_systems?: string[]
|
||||
extension_approved_by?: string
|
||||
extension_approved_at?: string
|
||||
identity_verified: boolean
|
||||
verification_method?: string
|
||||
verified_at?: string
|
||||
verified_by?: string
|
||||
verification_notes?: string
|
||||
verification_document_ref?: string
|
||||
assigned_to?: string
|
||||
assigned_at?: string
|
||||
assigned_by?: string
|
||||
completed_at?: string
|
||||
completion_notes?: string
|
||||
rejection_reason?: string
|
||||
rejection_legal_basis?: string
|
||||
erasure_checklist?: any[]
|
||||
data_export?: any
|
||||
rectification_details?: any
|
||||
objection_details?: any
|
||||
affected_systems?: string[]
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
function mapBackendStatus(status: string): import('./types').DSRStatus {
|
||||
const mapping: Record<string, import('./types').DSRStatus> = {
|
||||
'received': 'intake',
|
||||
'verified': 'identity_verification',
|
||||
'in_progress': 'processing',
|
||||
'completed': 'completed',
|
||||
'rejected': 'rejected',
|
||||
'extended': 'processing',
|
||||
}
|
||||
return mapping[status] || 'intake'
|
||||
}
|
||||
|
||||
function mapBackendChannel(channel: string): import('./types').DSRSource {
|
||||
const mapping: Record<string, import('./types').DSRSource> = {
|
||||
'email': 'email',
|
||||
'form': 'web_form',
|
||||
'phone': 'phone',
|
||||
'letter': 'letter',
|
||||
}
|
||||
return mapping[channel] || 'other'
|
||||
created_by?: string
|
||||
updated_by?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform flat backend DSR to nested SDK DSRRequest format
|
||||
* Transform flat backend DSR to nested SDK DSRRequest format.
|
||||
* New compliance backend already uses the same status names as frontend types.
|
||||
*/
|
||||
export function transformBackendDSR(b: BackendDSR): DSRRequest {
|
||||
const deadlineAt = b.extended_deadline_at || b.deadline_at
|
||||
const receivedDate = new Date(b.received_at)
|
||||
const defaultDeadlineDays = 30
|
||||
const originalDeadline = b.deadline_at || new Date(receivedDate.getTime() + defaultDeadlineDays * 24 * 60 * 60 * 1000).toISOString()
|
||||
|
||||
return {
|
||||
id: b.id,
|
||||
referenceNumber: `DSR-${new Date(b.created_at).getFullYear()}-${b.id.slice(0, 6).toUpperCase()}`,
|
||||
referenceNumber: b.request_number,
|
||||
type: b.request_type as DSRRequest['type'],
|
||||
status: mapBackendStatus(b.status),
|
||||
priority: 'normal',
|
||||
status: (b.status as DSRRequest['status']) || 'intake',
|
||||
priority: (b.priority as DSRRequest['priority']) || 'normal',
|
||||
requester: {
|
||||
name: b.subject_name,
|
||||
email: b.subject_email,
|
||||
customerId: b.subject_identifier,
|
||||
name: b.requester_name,
|
||||
email: b.requester_email,
|
||||
phone: b.requester_phone,
|
||||
address: b.requester_address,
|
||||
customerId: b.requester_customer_id,
|
||||
},
|
||||
source: mapBackendChannel(b.request_channel),
|
||||
requestText: b.request_description,
|
||||
source: (b.source as DSRRequest['source']) || 'email',
|
||||
sourceDetails: b.source_details,
|
||||
requestText: b.request_text,
|
||||
receivedAt: b.received_at,
|
||||
deadline: {
|
||||
originalDeadline,
|
||||
currentDeadline: deadlineAt,
|
||||
originalDeadline: b.deadline_at,
|
||||
currentDeadline: b.extended_deadline_at || b.deadline_at,
|
||||
extended: !!b.extended_deadline_at,
|
||||
extensionReason: b.extension_reason,
|
||||
extensionApprovedBy: b.extension_approved_by,
|
||||
extensionApprovedAt: b.extension_approved_at,
|
||||
},
|
||||
completedAt: b.completed_at,
|
||||
identityVerification: {
|
||||
verified: !!b.verified_at,
|
||||
verifiedAt: b.verified_at,
|
||||
verified: b.identity_verified,
|
||||
method: b.verification_method as any,
|
||||
verifiedAt: b.verified_at,
|
||||
verifiedBy: b.verified_by,
|
||||
notes: b.verification_notes,
|
||||
documentRef: b.verification_document_ref,
|
||||
},
|
||||
assignment: {
|
||||
assignedTo: b.assigned_to || null,
|
||||
assignedAt: b.assigned_at,
|
||||
assignedBy: b.assigned_by,
|
||||
},
|
||||
notes: b.notes,
|
||||
internalNotes: b.internal_notes,
|
||||
erasureChecklist: b.erasure_checklist ? { items: b.erasure_checklist, canProceedWithErasure: true } : undefined,
|
||||
dataExport: b.data_export && Object.keys(b.data_export).length > 0 ? b.data_export : undefined,
|
||||
rectificationDetails: b.rectification_details && Object.keys(b.rectification_details).length > 0 ? b.rectification_details : undefined,
|
||||
objectionDetails: b.objection_details && Object.keys(b.objection_details).length > 0 ? b.objection_details : undefined,
|
||||
createdAt: b.created_at,
|
||||
createdBy: 'system',
|
||||
createdBy: b.created_by || 'system',
|
||||
updatedAt: b.updated_at,
|
||||
updatedBy: b.updated_by,
|
||||
tenantId: b.tenant_id,
|
||||
}
|
||||
}
|
||||
@@ -516,74 +130,83 @@ function getSdkHeaders(): HeadersInit {
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch DSR list from SDK backend via proxy
|
||||
* Fetch DSR list from compliance backend via proxy
|
||||
*/
|
||||
export async function fetchSDKDSRList(): Promise<{ requests: DSRRequest[]; statistics: DSRStatistics }> {
|
||||
const res = await fetch('/api/sdk/v1/dsgvo/dsr', {
|
||||
headers: getSdkHeaders(),
|
||||
})
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP ${res.status}`)
|
||||
// Fetch list and stats in parallel
|
||||
const [listRes, statsRes] = await Promise.all([
|
||||
fetch('/api/sdk/v1/compliance/dsr?limit=100', { headers: getSdkHeaders() }),
|
||||
fetch('/api/sdk/v1/compliance/dsr/stats', { headers: getSdkHeaders() }),
|
||||
])
|
||||
|
||||
if (!listRes.ok) {
|
||||
throw new Error(`HTTP ${listRes.status}`)
|
||||
}
|
||||
const data = await res.json()
|
||||
const backendDSRs: BackendDSR[] = data.dsrs || []
|
||||
|
||||
const listData = await listRes.json()
|
||||
const backendDSRs: BackendDSR[] = listData.requests || []
|
||||
const requests = backendDSRs.map(transformBackendDSR)
|
||||
|
||||
// Calculate statistics locally
|
||||
const now = new Date()
|
||||
const statistics: DSRStatistics = {
|
||||
total: requests.length,
|
||||
byStatus: {
|
||||
intake: requests.filter(r => r.status === 'intake').length,
|
||||
identity_verification: requests.filter(r => r.status === 'identity_verification').length,
|
||||
processing: requests.filter(r => r.status === 'processing').length,
|
||||
completed: requests.filter(r => r.status === 'completed').length,
|
||||
rejected: requests.filter(r => r.status === 'rejected').length,
|
||||
cancelled: requests.filter(r => r.status === 'cancelled').length,
|
||||
},
|
||||
byType: {
|
||||
access: requests.filter(r => r.type === 'access').length,
|
||||
rectification: requests.filter(r => r.type === 'rectification').length,
|
||||
erasure: requests.filter(r => r.type === 'erasure').length,
|
||||
restriction: requests.filter(r => r.type === 'restriction').length,
|
||||
portability: requests.filter(r => r.type === 'portability').length,
|
||||
objection: requests.filter(r => r.type === 'objection').length,
|
||||
},
|
||||
overdue: requests.filter(r => {
|
||||
if (r.status === 'completed' || r.status === 'rejected' || r.status === 'cancelled') return false
|
||||
return new Date(r.deadline.currentDeadline) < now
|
||||
}).length,
|
||||
dueThisWeek: requests.filter(r => {
|
||||
if (r.status === 'completed' || r.status === 'rejected' || r.status === 'cancelled') return false
|
||||
const deadline = new Date(r.deadline.currentDeadline)
|
||||
const weekFromNow = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000)
|
||||
return deadline >= now && deadline <= weekFromNow
|
||||
}).length,
|
||||
averageProcessingDays: 0,
|
||||
completedThisMonth: requests.filter(r => {
|
||||
if (r.status !== 'completed' || !r.completedAt) return false
|
||||
const completed = new Date(r.completedAt)
|
||||
return completed.getMonth() === now.getMonth() && completed.getFullYear() === now.getFullYear()
|
||||
}).length,
|
||||
let statistics: DSRStatistics
|
||||
if (statsRes.ok) {
|
||||
const statsData = await statsRes.json()
|
||||
statistics = {
|
||||
total: statsData.total || 0,
|
||||
byStatus: statsData.by_status || { intake: 0, identity_verification: 0, processing: 0, completed: 0, rejected: 0, cancelled: 0 },
|
||||
byType: statsData.by_type || { access: 0, rectification: 0, erasure: 0, restriction: 0, portability: 0, objection: 0 },
|
||||
overdue: statsData.overdue || 0,
|
||||
dueThisWeek: statsData.due_this_week || 0,
|
||||
averageProcessingDays: statsData.average_processing_days || 0,
|
||||
completedThisMonth: statsData.completed_this_month || 0,
|
||||
}
|
||||
} else {
|
||||
// Fallback: calculate locally
|
||||
const now = new Date()
|
||||
statistics = {
|
||||
total: requests.length,
|
||||
byStatus: {
|
||||
intake: requests.filter(r => r.status === 'intake').length,
|
||||
identity_verification: requests.filter(r => r.status === 'identity_verification').length,
|
||||
processing: requests.filter(r => r.status === 'processing').length,
|
||||
completed: requests.filter(r => r.status === 'completed').length,
|
||||
rejected: requests.filter(r => r.status === 'rejected').length,
|
||||
cancelled: requests.filter(r => r.status === 'cancelled').length,
|
||||
},
|
||||
byType: {
|
||||
access: requests.filter(r => r.type === 'access').length,
|
||||
rectification: requests.filter(r => r.type === 'rectification').length,
|
||||
erasure: requests.filter(r => r.type === 'erasure').length,
|
||||
restriction: requests.filter(r => r.type === 'restriction').length,
|
||||
portability: requests.filter(r => r.type === 'portability').length,
|
||||
objection: requests.filter(r => r.type === 'objection').length,
|
||||
},
|
||||
overdue: 0,
|
||||
dueThisWeek: 0,
|
||||
averageProcessingDays: 0,
|
||||
completedThisMonth: 0,
|
||||
}
|
||||
}
|
||||
|
||||
return { requests, statistics }
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new DSR via SDK backend
|
||||
* Create a new DSR via compliance backend
|
||||
*/
|
||||
export async function createSDKDSR(request: DSRCreateRequest): Promise<void> {
|
||||
const body = {
|
||||
request_type: request.type,
|
||||
subject_name: request.requester.name,
|
||||
subject_email: request.requester.email,
|
||||
subject_identifier: request.requester.customerId || '',
|
||||
request_description: request.requestText || '',
|
||||
request_channel: request.source === 'web_form' ? 'form' : request.source,
|
||||
notes: '',
|
||||
requester_name: request.requester.name,
|
||||
requester_email: request.requester.email,
|
||||
requester_phone: request.requester.phone || null,
|
||||
requester_address: request.requester.address || null,
|
||||
requester_customer_id: request.requester.customerId || null,
|
||||
source: request.source,
|
||||
source_details: request.sourceDetails || null,
|
||||
request_text: request.requestText || '',
|
||||
priority: request.priority || 'normal',
|
||||
}
|
||||
const res = await fetch('/api/sdk/v1/dsgvo/dsr', {
|
||||
const res = await fetch('/api/sdk/v1/compliance/dsr', {
|
||||
method: 'POST',
|
||||
headers: getSdkHeaders(),
|
||||
body: JSON.stringify(body),
|
||||
@@ -594,10 +217,10 @@ export async function createSDKDSR(request: DSRCreateRequest): Promise<void> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a single DSR by ID from SDK backend
|
||||
* Fetch a single DSR by ID from compliance backend
|
||||
*/
|
||||
export async function fetchSDKDSR(id: string): Promise<DSRRequest | null> {
|
||||
const res = await fetch(`/api/sdk/v1/dsgvo/dsr/${id}`, {
|
||||
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}`, {
|
||||
headers: getSdkHeaders(),
|
||||
})
|
||||
if (!res.ok) {
|
||||
@@ -609,11 +232,11 @@ export async function fetchSDKDSR(id: string): Promise<DSRRequest | null> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Update DSR status via SDK backend
|
||||
* Update DSR status via compliance backend
|
||||
*/
|
||||
export async function updateSDKDSRStatus(id: string, status: string): Promise<void> {
|
||||
const res = await fetch(`/api/sdk/v1/dsgvo/dsr/${id}`, {
|
||||
method: 'PUT',
|
||||
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/status`, {
|
||||
method: 'POST',
|
||||
headers: getSdkHeaders(),
|
||||
body: JSON.stringify({ status }),
|
||||
})
|
||||
|
||||
@@ -797,18 +797,32 @@ export const SDK_STEPS: SDKStep[] = [
|
||||
prerequisiteSteps: ['vendor-compliance'],
|
||||
isOptional: false,
|
||||
},
|
||||
{
|
||||
id: 'email-templates',
|
||||
seq: 4350,
|
||||
phase: 2,
|
||||
package: 'betrieb',
|
||||
order: 5,
|
||||
name: 'E-Mail-Templates',
|
||||
nameShort: 'E-Mails',
|
||||
description: 'Benachrichtigungs-Vorlagen verwalten',
|
||||
url: '/sdk/email-templates',
|
||||
checkpointId: 'CP-EMAIL',
|
||||
prerequisiteSteps: ['consent-management'],
|
||||
isOptional: false,
|
||||
},
|
||||
{
|
||||
id: 'notfallplan',
|
||||
seq: 4400,
|
||||
phase: 2,
|
||||
package: 'betrieb',
|
||||
order: 5,
|
||||
order: 6,
|
||||
name: 'Notfallplan & Breach Response',
|
||||
nameShort: 'Notfallplan',
|
||||
description: 'Datenpannen-Management nach Art. 33/34 DSGVO',
|
||||
url: '/sdk/notfallplan',
|
||||
checkpointId: 'CP-NOTF',
|
||||
prerequisiteSteps: ['consent-management'],
|
||||
prerequisiteSteps: ['email-templates'],
|
||||
isOptional: false,
|
||||
},
|
||||
{
|
||||
@@ -816,7 +830,7 @@ export const SDK_STEPS: SDKStep[] = [
|
||||
seq: 4500,
|
||||
phase: 2,
|
||||
package: 'betrieb',
|
||||
order: 6,
|
||||
order: 7,
|
||||
name: 'Incident Management',
|
||||
nameShort: 'Incidents',
|
||||
description: 'Datenpannen erfassen, bewerten und melden (Art. 33/34 DSGVO)',
|
||||
@@ -830,7 +844,7 @@ export const SDK_STEPS: SDKStep[] = [
|
||||
seq: 4600,
|
||||
phase: 2,
|
||||
package: 'betrieb',
|
||||
order: 7,
|
||||
order: 8,
|
||||
name: 'Hinweisgebersystem',
|
||||
nameShort: 'Whistleblower',
|
||||
description: 'Anonymes Meldesystem gemaess HinSchG',
|
||||
@@ -844,7 +858,7 @@ export const SDK_STEPS: SDKStep[] = [
|
||||
seq: 4700,
|
||||
phase: 2,
|
||||
package: 'betrieb',
|
||||
order: 8,
|
||||
order: 9,
|
||||
name: 'Compliance Academy',
|
||||
nameShort: 'Academy',
|
||||
description: 'Mitarbeiter-Schulungen & Zertifikate',
|
||||
@@ -858,7 +872,7 @@ export const SDK_STEPS: SDKStep[] = [
|
||||
seq: 4800,
|
||||
phase: 2,
|
||||
package: 'betrieb',
|
||||
order: 9,
|
||||
order: 10,
|
||||
name: 'Training Engine',
|
||||
nameShort: 'Training',
|
||||
description: 'KI-generierte Schulungsinhalte, Quiz & Medien',
|
||||
|
||||
Reference in New Issue
Block a user