refactor(admin): split 9 more oversized lib/ files into focused modules
Files split by agents before rate limit: - dsr/api.ts (669 → barrel + helpers) - einwilligungen/context.tsx (669 → barrel + hooks/reducer) - export.ts (753 → barrel + domain exporters) - incidents/api.ts (845 → barrel + api-helpers) - tom-generator/context.tsx (720 → barrel + hooks/reducer) - vendor-compliance/context.tsx (1010 → 234 provider + hooks/reducer) - api-docs/endpoints.ts — partially split (3 domain files created) - academy/api.ts — partially split (helpers extracted) - whistleblower/api.ts — partially split (helpers extracted) next build passes. api-client.ts (885) deferred to next session. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
165
admin-compliance/lib/sdk/academy/api-helpers.ts
Normal file
165
admin-compliance/lib/sdk/academy/api-helpers.ts
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
/**
|
||||||
|
* Academy API - Shared configuration, helpers, and backend type mapping
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type {
|
||||||
|
Course,
|
||||||
|
CourseCategory,
|
||||||
|
LessonType,
|
||||||
|
} from './types'
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// CONFIGURATION
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export const ACADEMY_API_BASE = '/api/sdk/v1/academy'
|
||||||
|
export const API_TIMEOUT = 30000 // 30 seconds
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// HELPER FUNCTIONS
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
function getTenantId(): string {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
return localStorage.getItem('bp_tenant_id') || 'default-tenant'
|
||||||
|
}
|
||||||
|
return 'default-tenant'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAuthHeaders(): HeadersInit {
|
||||||
|
const headers: HeadersInit = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Tenant-ID': getTenantId()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const token = localStorage.getItem('authToken')
|
||||||
|
if (token) {
|
||||||
|
headers['Authorization'] = `Bearer ${token}`
|
||||||
|
}
|
||||||
|
const userId = localStorage.getItem('bp_user_id')
|
||||||
|
if (userId) {
|
||||||
|
headers['X-User-ID'] = userId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// BACKEND TYPE MAPPING (snake_case -> camelCase)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export interface BackendCourse {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
category: CourseCategory
|
||||||
|
duration_minutes: number
|
||||||
|
required_for_roles: string[]
|
||||||
|
is_active: boolean
|
||||||
|
passing_score?: number
|
||||||
|
status?: string
|
||||||
|
lessons?: BackendLesson[]
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BackendQuizQuestion {
|
||||||
|
id: string
|
||||||
|
question: string
|
||||||
|
options: string[]
|
||||||
|
correct_index: number
|
||||||
|
explanation: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BackendLesson {
|
||||||
|
id: string
|
||||||
|
course_id: string
|
||||||
|
title: string
|
||||||
|
description?: string
|
||||||
|
lesson_type: LessonType
|
||||||
|
content_url?: string
|
||||||
|
duration_minutes: number
|
||||||
|
order_index: number
|
||||||
|
quiz_questions?: BackendQuizQuestion[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapCourseFromBackend(bc: BackendCourse): Course {
|
||||||
|
return {
|
||||||
|
id: bc.id,
|
||||||
|
title: bc.title,
|
||||||
|
description: bc.description || '',
|
||||||
|
category: bc.category,
|
||||||
|
durationMinutes: bc.duration_minutes || 0,
|
||||||
|
passingScore: bc.passing_score ?? 70,
|
||||||
|
isActive: bc.is_active ?? true,
|
||||||
|
status: (bc.status as 'draft' | 'published') ?? 'draft',
|
||||||
|
requiredForRoles: bc.required_for_roles || [],
|
||||||
|
lessons: (bc.lessons || []).map(l => ({
|
||||||
|
id: l.id,
|
||||||
|
courseId: l.course_id,
|
||||||
|
title: l.title,
|
||||||
|
type: l.lesson_type,
|
||||||
|
contentMarkdown: l.content_url || '',
|
||||||
|
durationMinutes: l.duration_minutes || 0,
|
||||||
|
order: l.order_index,
|
||||||
|
quizQuestions: (l.quiz_questions || []).map(q => ({
|
||||||
|
id: q.id || `q-${Math.random().toString(36).slice(2)}`,
|
||||||
|
lessonId: l.id,
|
||||||
|
question: q.question,
|
||||||
|
options: q.options,
|
||||||
|
correctOptionIndex: q.correct_index,
|
||||||
|
explanation: q.explanation,
|
||||||
|
})),
|
||||||
|
})),
|
||||||
|
createdAt: bc.created_at,
|
||||||
|
updatedAt: bc.updated_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapCoursesFromBackend(courses: BackendCourse[]): Course[] {
|
||||||
|
return courses.map(mapCourseFromBackend)
|
||||||
|
}
|
||||||
383
admin-compliance/lib/sdk/api-docs/endpoints-go.ts
Normal file
383
admin-compliance/lib/sdk/api-docs/endpoints-go.ts
Normal file
@@ -0,0 +1,383 @@
|
|||||||
|
/**
|
||||||
|
* Go/Gin endpoints — AI Compliance SDK service modules
|
||||||
|
* (health, rbac, llm, go-audit, ucca, rag, roadmaps, roadmap-items,
|
||||||
|
* workshops, portfolios, academy, training, whistleblower, iace)
|
||||||
|
*/
|
||||||
|
import { ApiModule } from './types'
|
||||||
|
|
||||||
|
export const goModules: ApiModule[] = [
|
||||||
|
{
|
||||||
|
id: 'go-health',
|
||||||
|
name: 'Health — System-Status',
|
||||||
|
service: 'go',
|
||||||
|
basePath: '/sdk/v1',
|
||||||
|
exposure: 'admin',
|
||||||
|
endpoints: [
|
||||||
|
{ method: 'GET', path: '/health', description: 'API Health-Check', service: 'go', exposure: 'admin' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'rbac',
|
||||||
|
name: 'RBAC — Tenant, Rollen & Berechtigungen',
|
||||||
|
service: 'go',
|
||||||
|
basePath: '/sdk/v1',
|
||||||
|
exposure: 'internal',
|
||||||
|
endpoints: [
|
||||||
|
{ method: 'GET', path: '/tenants', description: 'Alle Tenants auflisten', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/tenants/:id', description: 'Tenant laden', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/tenants', description: 'Tenant erstellen', service: 'go' },
|
||||||
|
{ method: 'PUT', path: '/tenants/:id', description: 'Tenant aktualisieren', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/tenants/:id/namespaces', description: 'Namespaces auflisten', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/tenants/:id/namespaces', description: 'Namespace erstellen', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/namespaces/:id', description: 'Namespace laden', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/roles', description: 'Rollen auflisten', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/roles/system', description: 'System-Rollen auflisten', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/roles/:id', description: 'Rolle laden', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/roles', description: 'Rolle erstellen', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/user-roles', description: 'Rolle zuweisen', service: 'go' },
|
||||||
|
{ method: 'DELETE', path: '/user-roles/:userId/:roleId', description: 'Rolle entziehen', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/user-roles/:userId', description: 'Benutzer-Rollen laden', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/permissions/effective', description: 'Effektive Berechtigungen laden', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/permissions/context', description: 'Benutzerkontext laden', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/permissions/check', description: 'Berechtigung pruefen', service: 'go' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'llm',
|
||||||
|
name: 'LLM — KI-Textverarbeitung & Policies',
|
||||||
|
service: 'go',
|
||||||
|
basePath: '/sdk/v1/llm',
|
||||||
|
exposure: 'partner',
|
||||||
|
endpoints: [
|
||||||
|
{ method: 'GET', path: '/policies', description: 'LLM-Policies auflisten', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/policies/:id', description: 'Policy laden', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/policies', description: 'Policy erstellen', service: 'go' },
|
||||||
|
{ method: 'PUT', path: '/policies/:id', description: 'Policy aktualisieren', service: 'go' },
|
||||||
|
{ method: 'DELETE', path: '/policies/:id', description: 'Policy loeschen', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/chat', description: 'Chat Completion', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/complete', description: 'Text Completion', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/models', description: 'Verfuegbare Modelle auflisten', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/providers/status', description: 'Provider-Status laden', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/analyze', description: 'Text analysieren', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/redact', description: 'PII schwaerzen', service: 'go' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'go-audit',
|
||||||
|
name: 'Audit (Go) — LLM-Audit & Compliance-Reports',
|
||||||
|
service: 'go',
|
||||||
|
basePath: '/sdk/v1/audit',
|
||||||
|
exposure: 'internal',
|
||||||
|
endpoints: [
|
||||||
|
{ method: 'GET', path: '/llm', description: 'LLM-Audit-Logs laden', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/general', description: 'Allgemeine Audit-Logs laden', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/llm-operations', description: 'LLM-Operationen laden (Alias)', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/trail', description: 'Audit-Trail laden (Alias)', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/usage', description: 'Nutzungsstatistiken laden', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/compliance-report', description: 'Compliance-Report laden', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/export/llm', description: 'LLM-Audit exportieren', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/export/general', description: 'Allgemeines Audit exportieren', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/export/compliance', description: 'Compliance-Report exportieren', service: 'go' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'ucca',
|
||||||
|
name: 'UCCA — Use-Case Compliance Advisor',
|
||||||
|
service: 'go',
|
||||||
|
basePath: '/sdk/v1/ucca',
|
||||||
|
exposure: 'internal',
|
||||||
|
endpoints: [
|
||||||
|
{ method: 'POST', path: '/assess', description: 'Compliance-Bewertung durchfuehren', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/assessments', description: 'Bewertungen auflisten', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/assessments/:id', description: 'Bewertung laden', service: 'go' },
|
||||||
|
{ method: 'PUT', path: '/assessments/:id', description: 'Bewertung aktualisieren', service: 'go' },
|
||||||
|
{ method: 'DELETE', path: '/assessments/:id', description: 'Bewertung loeschen', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/assessments/:id/explain', description: 'KI-Erklaerung generieren', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/patterns', description: 'Compliance-Muster laden', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/examples', description: 'Beispiele laden', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/rules', description: 'Compliance-Regeln laden', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/controls', description: 'Controls laden', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/problem-solutions', description: 'Problem-Loesungs-Paare laden', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/export/:id', description: 'Bewertung exportieren', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/escalations', description: 'Eskalationen auflisten', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/escalations/stats', description: 'Eskalations-Statistiken laden', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/escalations/:id', description: 'Eskalation laden', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/escalations', description: 'Eskalation erstellen', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/escalations/:id/assign', description: 'Eskalation zuweisen', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/escalations/:id/review', description: 'Review starten', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/escalations/:id/decide', description: 'Entscheidung treffen', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/obligations/assess', description: 'Pflichten bewerten', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/obligations/:assessmentId', description: 'Bewertungsergebnis laden', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/obligations/:assessmentId/by-regulation', description: 'Nach Regulierung gruppiert', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/obligations/:assessmentId/by-deadline', description: 'Nach Frist gruppiert', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/obligations/:assessmentId/by-responsible', description: 'Nach Verantwortlichem gruppiert', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/obligations/export/memo', description: 'C-Level-Memo exportieren', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/obligations/export/direct', description: 'Uebersicht direkt exportieren', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/obligations/regulations', description: 'Regulierungen laden', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/obligations/regulations/:regulationId/decision-tree', description: 'Entscheidungsbaum laden', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/obligations/quick-check', description: 'Schnell-Check durchfuehren', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/obligations/assess-from-scope', description: 'Aus Scope bewerten', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/obligations/tom-controls/for-obligation/:obligationId', description: 'TOM-Controls fuer Pflicht laden', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/obligations/gap-analysis', description: 'TOM-Gap-Analyse durchfuehren', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/obligations/tom-controls/:controlId/obligations', description: 'Pflichten fuer TOM-Control laden', service: 'go' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'rag',
|
||||||
|
name: 'RAG — Legal Corpus & Vektorsuche',
|
||||||
|
service: 'go',
|
||||||
|
basePath: '/sdk/v1/rag',
|
||||||
|
exposure: 'partner',
|
||||||
|
endpoints: [
|
||||||
|
{ method: 'POST', path: '/search', description: 'Rechtskorpus durchsuchen', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/regulations', description: 'Regulierungen auflisten', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/corpus-status', description: 'Indexierungsstatus laden', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/corpus-versions/:collection', description: 'Versionshistorie laden', service: 'go' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'roadmaps',
|
||||||
|
name: 'Roadmaps — Compliance-Implementierungsplaene',
|
||||||
|
service: 'go',
|
||||||
|
basePath: '/sdk/v1/roadmaps',
|
||||||
|
exposure: 'internal',
|
||||||
|
endpoints: [
|
||||||
|
{ method: 'POST', path: '/', description: 'Roadmap erstellen', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/', description: 'Roadmaps auflisten', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/:id', description: 'Roadmap laden', service: 'go' },
|
||||||
|
{ method: 'PUT', path: '/:id', description: 'Roadmap aktualisieren', service: 'go' },
|
||||||
|
{ method: 'DELETE', path: '/:id', description: 'Roadmap loeschen', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/:id/stats', description: 'Roadmap-Statistiken laden', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/:id/items', description: 'Item erstellen', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/:id/items', description: 'Items auflisten', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/import/upload', description: 'Import hochladen', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/import/:jobId', description: 'Import-Status laden', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/import/:jobId/confirm', description: 'Import bestaetigen', service: 'go' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'roadmap-items',
|
||||||
|
name: 'Roadmap Items — Einzelne Massnahmen',
|
||||||
|
service: 'go',
|
||||||
|
basePath: '/sdk/v1/roadmap-items',
|
||||||
|
exposure: 'internal',
|
||||||
|
endpoints: [
|
||||||
|
{ method: 'GET', path: '/:id', description: 'Item laden', service: 'go' },
|
||||||
|
{ method: 'PUT', path: '/:id', description: 'Item aktualisieren', service: 'go' },
|
||||||
|
{ method: 'PATCH', path: '/:id/status', description: 'Item-Status aendern', service: 'go' },
|
||||||
|
{ method: 'DELETE', path: '/:id', description: 'Item loeschen', service: 'go' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'workshops',
|
||||||
|
name: 'Workshops — Kollaborative Compliance-Workshops',
|
||||||
|
service: 'go',
|
||||||
|
basePath: '/sdk/v1/workshops',
|
||||||
|
exposure: 'internal',
|
||||||
|
endpoints: [
|
||||||
|
{ method: 'POST', path: '/', description: 'Workshop erstellen', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/', description: 'Workshops auflisten', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/:id', description: 'Workshop laden', service: 'go' },
|
||||||
|
{ method: 'PUT', path: '/:id', description: 'Workshop aktualisieren', service: 'go' },
|
||||||
|
{ method: 'DELETE', path: '/:id', description: 'Workshop loeschen', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/:id/start', description: 'Workshop starten', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/:id/pause', description: 'Workshop pausieren', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/:id/complete', description: 'Workshop abschliessen', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/:id/participants', description: 'Teilnehmer auflisten', service: 'go' },
|
||||||
|
{ method: 'PUT', path: '/:id/participants/:participantId', description: 'Teilnehmer aktualisieren', service: 'go' },
|
||||||
|
{ method: 'DELETE', path: '/:id/participants/:participantId', description: 'Teilnehmer entfernen', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/:id/responses', description: 'Antwort einreichen', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/:id/responses', description: 'Antworten laden', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/:id/comments', description: 'Kommentar hinzufuegen', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/:id/comments', description: 'Kommentare laden', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/:id/advance', description: 'Zum naechsten Schritt', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/:id/goto', description: 'Zu bestimmtem Schritt springen', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/:id/stats', description: 'Workshop-Statistiken laden', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/:id/summary', description: 'Zusammenfassung laden', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/:id/export', description: 'Workshop exportieren', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/join/:code', description: 'Per Zugangscode beitreten', service: 'go' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'portfolios',
|
||||||
|
name: 'Portfolios — KI-Use-Case-Portfolio',
|
||||||
|
service: 'go',
|
||||||
|
basePath: '/sdk/v1/portfolios',
|
||||||
|
exposure: 'internal',
|
||||||
|
endpoints: [
|
||||||
|
{ method: 'POST', path: '/', description: 'Portfolio erstellen', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/', description: 'Portfolios auflisten', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/:id', description: 'Portfolio laden', service: 'go' },
|
||||||
|
{ method: 'PUT', path: '/:id', description: 'Portfolio aktualisieren', service: 'go' },
|
||||||
|
{ method: 'DELETE', path: '/:id', description: 'Portfolio loeschen', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/:id/items', description: 'Item hinzufuegen', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/:id/items', description: 'Items auflisten', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/:id/items/bulk', description: 'Items Bulk-Import', service: 'go' },
|
||||||
|
{ method: 'DELETE', path: '/:id/items/:itemId', description: 'Item entfernen', service: 'go' },
|
||||||
|
{ method: 'PUT', path: '/:id/items/order', description: 'Items sortieren', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/:id/stats', description: 'Portfolio-Statistiken laden', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/:id/activity', description: 'Aktivitaets-Log laden', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/:id/recalculate', description: 'Metriken neu berechnen', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/:id/submit-review', description: 'Zur Pruefung einreichen', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/:id/approve', description: 'Portfolio genehmigen', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/merge', description: 'Portfolios zusammenfuehren', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/compare', description: 'Portfolios vergleichen', service: 'go' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'academy',
|
||||||
|
name: 'Academy — E-Learning & Zertifikate',
|
||||||
|
service: 'go',
|
||||||
|
basePath: '/sdk/v1/academy',
|
||||||
|
exposure: 'internal',
|
||||||
|
endpoints: [
|
||||||
|
{ method: 'POST', path: '/courses', description: 'Kurs erstellen', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/courses', description: 'Kurse auflisten', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/courses/:id', description: 'Kurs laden', service: 'go' },
|
||||||
|
{ method: 'PUT', path: '/courses/:id', description: 'Kurs aktualisieren', service: 'go' },
|
||||||
|
{ method: 'DELETE', path: '/courses/:id', description: 'Kurs loeschen', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/enrollments', description: 'Einschreibung erstellen', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/enrollments', description: 'Einschreibungen auflisten', service: 'go' },
|
||||||
|
{ method: 'PUT', path: '/enrollments/:id/progress', description: 'Fortschritt aktualisieren', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/enrollments/:id/complete', description: 'Einschreibung abschliessen', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/certificates/:id', description: 'Zertifikat laden', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/enrollments/:id/certificate', description: 'Zertifikat generieren', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/certificates/:id/pdf', description: 'Zertifikat-PDF herunterladen', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/courses/:id/quiz', description: 'Quiz einreichen', service: 'go' },
|
||||||
|
{ method: 'PUT', path: '/lessons/:id', description: 'Lektion aktualisieren', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/lessons/:id/quiz-test', description: 'Quiz testen', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/stats', description: 'Academy-Statistiken laden', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/courses/generate', description: 'Kurs aus Modul generieren', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/courses/generate-all', description: 'Alle Kurse generieren', service: 'go' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'training',
|
||||||
|
name: 'Training — Schulungsmodule & Content-Pipeline',
|
||||||
|
service: 'go',
|
||||||
|
basePath: '/sdk/v1/training',
|
||||||
|
exposure: 'internal',
|
||||||
|
endpoints: [
|
||||||
|
{ method: 'GET', path: '/modules', description: 'Schulungsmodule auflisten', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/modules/:id', description: 'Modul laden', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/modules', description: 'Modul erstellen', service: 'go' },
|
||||||
|
{ method: 'PUT', path: '/modules/:id', description: 'Modul aktualisieren', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/matrix', description: 'Schulungsmatrix laden', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/matrix/:role', description: 'Matrix fuer Rolle laden', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/matrix', description: 'Matrix-Eintrag setzen', service: 'go' },
|
||||||
|
{ method: 'DELETE', path: '/matrix/:role/:moduleId', description: 'Matrix-Eintrag loeschen', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/assignments/compute', description: 'Zuweisungen berechnen', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/assignments', description: 'Zuweisungen auflisten', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/assignments/:id', description: 'Zuweisung laden', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/assignments/:id/start', description: 'Zuweisung starten', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/assignments/:id/progress', description: 'Fortschritt aktualisieren', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/assignments/:id/complete', description: 'Zuweisung abschliessen', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/quiz/:moduleId', description: 'Quiz laden', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/quiz/:moduleId/submit', description: 'Quiz einreichen', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/quiz/attempts/:assignmentId', description: 'Quiz-Versuche laden', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/content/generate', description: 'Inhalt generieren', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/content/generate-quiz', description: 'Quiz generieren', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/content/generate-all', description: 'Alle Inhalte generieren', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/content/generate-all-quiz', description: 'Alle Quizze generieren', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/content/:moduleId', description: 'Modul-Inhalt laden', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/content/:moduleId/publish', description: 'Inhalt veroeffentlichen', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/content/:moduleId/generate-audio', description: 'Audio generieren', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/content/:moduleId/generate-video', description: 'Video generieren', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/content/:moduleId/preview-script', description: 'Video-Script Vorschau', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/media/module/:moduleId', description: 'Medien fuer Modul laden', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/media/:mediaId/url', description: 'Medien-URL laden', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/media/:mediaId/publish', description: 'Medium veroeffentlichen', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/deadlines', description: 'Fristen laden', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/deadlines/overdue', description: 'Ueberfaellige Fristen laden', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/escalation/check', description: 'Eskalation pruefen', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/audit-log', description: 'Schulungs-Audit-Log laden', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/stats', description: 'Schulungs-Statistiken laden', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/certificates/:id/verify', description: 'Zertifikat verifizieren', service: 'go', exposure: 'partner' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'whistleblower',
|
||||||
|
name: 'Whistleblower — Hinweisgebersystem (HinSchG)',
|
||||||
|
service: 'go',
|
||||||
|
basePath: '/sdk/v1/whistleblower',
|
||||||
|
exposure: 'internal',
|
||||||
|
endpoints: [
|
||||||
|
{ method: 'POST', path: '/reports/submit', description: 'Anonymen Hinweis einreichen', service: 'go', exposure: 'public' },
|
||||||
|
{ method: 'GET', path: '/reports/access/:accessKey', description: 'Hinweis per Zugangscode laden', service: 'go', exposure: 'public' },
|
||||||
|
{ method: 'POST', path: '/reports/access/:accessKey/messages', description: 'Nachricht senden (anonym)', service: 'go', exposure: 'public' },
|
||||||
|
{ method: 'GET', path: '/reports', description: 'Alle Hinweise auflisten', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/reports/:id', description: 'Hinweis laden', service: 'go' },
|
||||||
|
{ method: 'PUT', path: '/reports/:id', description: 'Hinweis aktualisieren', service: 'go' },
|
||||||
|
{ method: 'DELETE', path: '/reports/:id', description: 'Hinweis loeschen', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/reports/:id/acknowledge', description: 'Eingangsbestaetigung senden', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/reports/:id/investigate', description: 'Untersuchung starten', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/reports/:id/measures', description: 'Abhilfemassnahme hinzufuegen', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/reports/:id/close', description: 'Hinweis schliessen', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/reports/:id/messages', description: 'Admin-Nachricht senden', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/reports/:id/messages', description: 'Nachrichten laden', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/stats', description: 'Whistleblower-Statistiken laden', service: 'go' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'iace',
|
||||||
|
name: 'IACE — Industrial AI / CE-Compliance Engine',
|
||||||
|
service: 'go',
|
||||||
|
basePath: '/sdk/v1/iace',
|
||||||
|
exposure: 'internal',
|
||||||
|
endpoints: [
|
||||||
|
{ method: 'GET', path: '/hazard-library', description: 'Gefahrenbibliothek laden', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/controls-library', description: 'Controls-Bibliothek laden', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/projects', description: 'Projekt erstellen', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/projects', description: 'Projekte auflisten', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/projects/:id', description: 'Projekt laden', service: 'go' },
|
||||||
|
{ method: 'PUT', path: '/projects/:id', description: 'Projekt aktualisieren', service: 'go' },
|
||||||
|
{ method: 'DELETE', path: '/projects/:id', description: 'Projekt archivieren', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/projects/:id/init-from-profile', description: 'Aus Unternehmensprofil initialisieren', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/projects/:id/completeness-check', description: 'Vollstaendigkeits-Check durchfuehren', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/projects/:id/components', description: 'Komponente erstellen', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/projects/:id/components', description: 'Komponenten auflisten', service: 'go' },
|
||||||
|
{ method: 'PUT', path: '/projects/:id/components/:cid', description: 'Komponente aktualisieren', service: 'go' },
|
||||||
|
{ method: 'DELETE', path: '/projects/:id/components/:cid', description: 'Komponente loeschen', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/projects/:id/classify', description: 'Regulatorisch klassifizieren', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/projects/:id/classifications', description: 'Klassifizierungen laden', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/projects/:id/classify/:regulation', description: 'Fuer einzelne Regulierung klassifizieren', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/projects/:id/hazards', description: 'Gefaehrdung erstellen', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/projects/:id/hazards', description: 'Gefaehrdungen auflisten', service: 'go' },
|
||||||
|
{ method: 'PUT', path: '/projects/:id/hazards/:hid', description: 'Gefaehrdung aktualisieren', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/projects/:id/hazards/suggest', description: 'KI-Gefaehrdungsvorschlaege generieren', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/projects/:id/hazards/:hid/assess', description: 'Risiko bewerten', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/projects/:id/risk-summary', description: 'Risiko-Zusammenfassung laden', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/projects/:id/hazards/:hid/reassess', description: 'Risiko neu bewerten', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/projects/:id/hazards/:hid/mitigations', description: 'Risikominderung erstellen', service: 'go' },
|
||||||
|
{ method: 'PUT', path: '/mitigations/:mid', description: 'Risikominderung aktualisieren', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/mitigations/:mid/verify', description: 'Risikominderung verifizieren', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/projects/:id/evidence', description: 'Nachweis hochladen', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/projects/:id/evidence', description: 'Nachweise auflisten', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/projects/:id/verification-plan', description: 'Verifizierungsplan erstellen', service: 'go' },
|
||||||
|
{ method: 'PUT', path: '/verification-plan/:vid', description: 'Plan aktualisieren', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/verification-plan/:vid/complete', description: 'Verifizierung abschliessen', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/projects/:id/tech-file/generate', description: 'Technische Akte generieren', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/projects/:id/tech-file', description: 'Akte-Abschnitte laden', service: 'go' },
|
||||||
|
{ method: 'PUT', path: '/projects/:id/tech-file/:section', description: 'Abschnitt aktualisieren', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/projects/:id/tech-file/:section/approve', description: 'Abschnitt genehmigen', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/projects/:id/tech-file/export', description: 'Technische Akte exportieren', service: 'go' },
|
||||||
|
{ method: 'POST', path: '/projects/:id/monitoring', description: 'Monitoring-Event erstellen', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/projects/:id/monitoring', description: 'Monitoring-Events laden', service: 'go' },
|
||||||
|
{ method: 'PUT', path: '/projects/:id/monitoring/:eid', description: 'Event aktualisieren', service: 'go' },
|
||||||
|
{ method: 'GET', path: '/projects/:id/audit-trail', description: 'Projekt-Audit-Trail laden', service: 'go' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
191
admin-compliance/lib/sdk/api-docs/endpoints-python-core.ts
Normal file
191
admin-compliance/lib/sdk/api-docs/endpoints-python-core.ts
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
/**
|
||||||
|
* Python/FastAPI endpoints — Core compliance modules
|
||||||
|
* (framework, audit, change-requests, company-profile, projects,
|
||||||
|
* compliance-scope, dashboard, generation, extraction, modules)
|
||||||
|
*/
|
||||||
|
import { ApiModule } from './types'
|
||||||
|
|
||||||
|
export const pythonCoreModules: ApiModule[] = [
|
||||||
|
{
|
||||||
|
id: 'compliance-framework',
|
||||||
|
name: 'Compliance Framework — Regulierungen, Anforderungen & Controls',
|
||||||
|
service: 'python',
|
||||||
|
basePath: '/api/compliance',
|
||||||
|
exposure: 'internal',
|
||||||
|
endpoints: [
|
||||||
|
{ method: 'GET', path: '/regulations', description: 'Alle Regulierungen auflisten', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/regulations/{code}', description: 'Regulierung nach Code laden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/regulations/{code}/requirements', description: 'Anforderungen einer Regulierung', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/requirements', description: 'Anforderungen auflisten (paginiert)', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/requirements/{requirement_id}', description: 'Einzelne Anforderung laden', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/requirements', description: 'Anforderung erstellen', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/requirements/{requirement_id}', description: 'Anforderung aktualisieren', service: 'python' },
|
||||||
|
{ method: 'DELETE', path: '/requirements/{requirement_id}', description: 'Anforderung loeschen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/controls', description: 'Alle Controls auflisten', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/controls/paginated', description: 'Controls paginiert laden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/controls/{control_id}', description: 'Einzelnes Control laden', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/controls/{control_id}', description: 'Control aktualisieren', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/controls/{control_id}/review', description: 'Control-Review durchfuehren', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/controls/by-domain/{domain}', description: 'Controls nach Domain filtern', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/export', description: 'Audit-Export erstellen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/export/{export_id}', description: 'Export-Status abfragen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/export/{export_id}/download', description: 'Export-Datei herunterladen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/exports', description: 'Alle Exports auflisten', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/init-tables', description: 'Datenbanktabellen initialisieren', service: 'python', exposure: 'admin' },
|
||||||
|
{ method: 'POST', path: '/create-indexes', description: 'Datenbank-Indizes erstellen', service: 'python', exposure: 'admin' },
|
||||||
|
{ method: 'POST', path: '/seed-risks', description: 'Risikodaten einspielen', service: 'python', exposure: 'admin' },
|
||||||
|
{ method: 'POST', path: '/seed', description: 'Systemdaten einspielen', service: 'python', exposure: 'admin' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'audit',
|
||||||
|
name: 'Audit — Sitzungen & Checklisten',
|
||||||
|
service: 'python',
|
||||||
|
basePath: '/api/compliance/audit',
|
||||||
|
exposure: 'internal',
|
||||||
|
endpoints: [
|
||||||
|
{ method: 'POST', path: '/sessions', description: 'Audit-Sitzung erstellen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/sessions', description: 'Alle Audit-Sitzungen auflisten', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/sessions/{session_id}', description: 'Sitzung laden', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/sessions/{session_id}/start', description: 'Sitzung starten', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/sessions/{session_id}/complete', description: 'Sitzung abschliessen', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/sessions/{session_id}/archive', description: 'Sitzung archivieren', service: 'python' },
|
||||||
|
{ method: 'DELETE', path: '/sessions/{session_id}', description: 'Sitzung loeschen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/sessions/{session_id}/report/pdf', description: 'Sitzungsbericht als PDF exportieren', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/checklist/{session_id}', description: 'Checkliste einer Sitzung laden', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/checklist/{session_id}/items/{requirement_id}/sign-off', description: 'Anforderung abzeichnen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/checklist/{session_id}/items/{requirement_id}', description: 'Abzeichnung-Details laden', service: 'python' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'ai-systems',
|
||||||
|
name: 'AI Act — KI-Systeme & Risikobewertung',
|
||||||
|
service: 'python',
|
||||||
|
basePath: '/api/compliance/ai',
|
||||||
|
exposure: 'internal',
|
||||||
|
endpoints: [
|
||||||
|
{ method: 'GET', path: '/systems', description: 'KI-Systeme auflisten', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/systems', description: 'KI-System erstellen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/systems/{system_id}', description: 'KI-System laden', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/systems/{system_id}', description: 'KI-System aktualisieren', service: 'python' },
|
||||||
|
{ method: 'DELETE', path: '/systems/{system_id}', description: 'KI-System loeschen', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/systems/{system_id}/assess', description: 'KI-Compliance bewerten', service: 'python' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'change-requests',
|
||||||
|
name: 'Change Requests — Aenderungsantraege',
|
||||||
|
service: 'python',
|
||||||
|
basePath: '/api/compliance/change-requests',
|
||||||
|
exposure: 'internal',
|
||||||
|
endpoints: [
|
||||||
|
{ method: 'GET', path: '/stats', description: 'CR-Statistiken laden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/{cr_id}', description: 'Einzelnen CR laden', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/{cr_id}/accept', description: 'CR akzeptieren', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/{cr_id}/reject', description: 'CR ablehnen', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/{cr_id}/edit', description: 'CR bearbeiten', service: 'python' },
|
||||||
|
{ method: 'DELETE', path: '/{cr_id}', description: 'CR loeschen', service: 'python' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'company-profile',
|
||||||
|
name: 'Stammdaten — Unternehmensprofil',
|
||||||
|
service: 'python',
|
||||||
|
basePath: '/api/v1/company-profile',
|
||||||
|
exposure: 'internal',
|
||||||
|
endpoints: [
|
||||||
|
{ method: 'GET', path: '/', description: 'Unternehmensprofil laden', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/', description: 'Profil erstellen/aktualisieren', service: 'python' },
|
||||||
|
{ method: 'DELETE', path: '/', description: 'Profil loeschen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/template-context', description: 'Profil als Template-Kontext (flach)', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/audit', description: 'Profil-Aenderungsprotokoll laden', service: 'python' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'projects',
|
||||||
|
name: 'Projekte — Multi-Projekt-Verwaltung',
|
||||||
|
service: 'python',
|
||||||
|
basePath: '/api/compliance/v1/projects',
|
||||||
|
exposure: 'internal',
|
||||||
|
endpoints: [
|
||||||
|
{ method: 'GET', path: '/', description: 'Alle Projekte des Tenants auflisten', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/', description: 'Neues Projekt erstellen (optional mit Stammdaten-Kopie)', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/{project_id}', description: 'Einzelnes Projekt laden', service: 'python' },
|
||||||
|
{ method: 'PATCH', path: '/{project_id}', description: 'Projekt aktualisieren (Name, Beschreibung)', service: 'python' },
|
||||||
|
{ method: 'DELETE', path: '/{project_id}', description: 'Projekt archivieren (Soft Delete)', service: 'python' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'compliance-scope',
|
||||||
|
name: 'Compliance Scope — Geltungsbereich',
|
||||||
|
service: 'python',
|
||||||
|
basePath: '/api/v1/compliance-scope',
|
||||||
|
exposure: 'internal',
|
||||||
|
endpoints: [
|
||||||
|
{ method: 'GET', path: '/', description: 'Compliance-Scope laden', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/', description: 'Compliance-Scope erstellen/aktualisieren', service: 'python' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'dashboard',
|
||||||
|
name: 'Dashboard — Compliance-Uebersicht & Reports',
|
||||||
|
service: 'python',
|
||||||
|
basePath: '/api/compliance/dashboard',
|
||||||
|
exposure: 'internal',
|
||||||
|
endpoints: [
|
||||||
|
{ method: 'GET', path: '/dashboard', description: 'Haupt-Dashboard laden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/score', description: 'Compliance-Score berechnen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/dashboard/executive', description: 'Executive-Dashboard laden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/dashboard/trend', description: 'Compliance-Trendverlauf laden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/reports/summary', description: 'Zusammenfassungsbericht laden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/reports/{period}', description: 'Periodenbericht generieren', service: 'python' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'generation',
|
||||||
|
name: 'Dokumentengenerierung — Automatische Erstellung',
|
||||||
|
service: 'python',
|
||||||
|
basePath: '/api/compliance/generation',
|
||||||
|
exposure: 'internal',
|
||||||
|
endpoints: [
|
||||||
|
{ method: 'GET', path: '/preview/{doc_type}', description: 'Generierungs-Vorschau laden', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/apply/{doc_type}', description: 'Dokument generieren und anwenden', service: 'python' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'extraction',
|
||||||
|
name: 'Extraktion — Anforderungen aus RAG',
|
||||||
|
service: 'python',
|
||||||
|
basePath: '/api/compliance',
|
||||||
|
exposure: 'internal',
|
||||||
|
endpoints: [
|
||||||
|
{ method: 'POST', path: '/extract-requirements-from-rag', description: 'Anforderungen aus RAG-Korpus extrahieren', service: 'python' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'modules',
|
||||||
|
name: 'Module — Compliance-Modul-Verwaltung',
|
||||||
|
service: 'python',
|
||||||
|
basePath: '/api/compliance/modules',
|
||||||
|
exposure: 'internal',
|
||||||
|
endpoints: [
|
||||||
|
{ method: 'GET', path: '/modules', description: 'Module auflisten', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/modules/overview', description: 'Modul-Uebersicht laden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/modules/{module_id}', description: 'Modul laden', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/modules/seed', description: 'Module einspielen', service: 'python', exposure: 'admin' },
|
||||||
|
{ method: 'POST', path: '/modules/{module_id}/activate', description: 'Modul aktivieren', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/modules/{module_id}/deactivate', description: 'Modul deaktivieren', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/modules/{module_id}/regulations', description: 'Regulierungs-Zuordnung hinzufuegen', service: 'python' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
262
admin-compliance/lib/sdk/api-docs/endpoints-python-gdpr.ts
Normal file
262
admin-compliance/lib/sdk/api-docs/endpoints-python-gdpr.ts
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
/**
|
||||||
|
* Python/FastAPI endpoints — GDPR, DSR, consent, and data-subject modules
|
||||||
|
* (banner, consent-templates, dsfa, dsr, einwilligungen, loeschfristen,
|
||||||
|
* consent-user, consent-admin, dsr-user, dsr-admin, gdpr)
|
||||||
|
*/
|
||||||
|
import { ApiModule } from './types'
|
||||||
|
|
||||||
|
export const pythonGdprModules: ApiModule[] = [
|
||||||
|
{
|
||||||
|
id: 'banner',
|
||||||
|
name: 'Cookie-Banner & Consent Management',
|
||||||
|
service: 'python',
|
||||||
|
basePath: '/api/compliance/consent',
|
||||||
|
exposure: 'internal',
|
||||||
|
endpoints: [
|
||||||
|
{ method: 'POST', path: '/consent', description: 'Einwilligung erfassen', service: 'python', exposure: 'public' },
|
||||||
|
{ method: 'GET', path: '/consent', description: 'Einwilligungen auflisten', service: 'python' },
|
||||||
|
{ method: 'DELETE', path: '/consent/{consent_id}', description: 'Einwilligung loeschen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/consent/export', description: 'Einwilligungsdaten exportieren', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/config/{site_id}', description: 'Seitenkonfiguration laden', service: 'python', exposure: 'public' },
|
||||||
|
{ method: 'GET', path: '/admin/sites', description: 'Alle Seiten auflisten', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/admin/sites', description: 'Seite erstellen', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/admin/sites/{site_id}', description: 'Seite aktualisieren', service: 'python' },
|
||||||
|
{ method: 'DELETE', path: '/admin/sites/{site_id}', description: 'Seite loeschen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/admin/sites/{site_id}/categories', description: 'Cookie-Kategorien auflisten', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/admin/sites/{site_id}/categories', description: 'Cookie-Kategorie erstellen', service: 'python' },
|
||||||
|
{ method: 'DELETE', path: '/admin/categories/{category_id}', description: 'Cookie-Kategorie loeschen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/admin/sites/{site_id}/vendors', description: 'Anbieter auflisten', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/admin/sites/{site_id}/vendors', description: 'Anbieter hinzufuegen', service: 'python' },
|
||||||
|
{ method: 'DELETE', path: '/admin/vendors/{vendor_id}', description: 'Anbieter loeschen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/admin/stats/{site_id}', description: 'Seiten-Statistiken laden', service: 'python' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'consent-templates',
|
||||||
|
name: 'Einwilligungsvorlagen — Consent Templates',
|
||||||
|
service: 'python',
|
||||||
|
basePath: '/api/compliance/consent-templates',
|
||||||
|
exposure: 'internal',
|
||||||
|
endpoints: [
|
||||||
|
{ method: 'GET', path: '/consent-templates', description: 'Vorlagen auflisten', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/consent-templates', description: 'Vorlage erstellen', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/consent-templates/{template_id}', description: 'Vorlage aktualisieren', service: 'python' },
|
||||||
|
{ method: 'DELETE', path: '/consent-templates/{template_id}', description: 'Vorlage loeschen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/gdpr-processes', description: 'DSGVO-Prozesse auflisten', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/gdpr-processes/{process_id}', description: 'DSGVO-Prozess aktualisieren', service: 'python' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'dsfa',
|
||||||
|
name: 'DSFA — Datenschutz-Folgenabschaetzung',
|
||||||
|
service: 'python',
|
||||||
|
basePath: '/api/compliance/dsfa',
|
||||||
|
exposure: 'internal',
|
||||||
|
endpoints: [
|
||||||
|
{ method: 'GET', path: '/', description: 'DSFAs auflisten', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/', description: 'DSFA erstellen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/{dsfa_id}', description: 'DSFA laden', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/{dsfa_id}', description: 'DSFA aktualisieren', service: 'python' },
|
||||||
|
{ method: 'DELETE', path: '/{dsfa_id}', description: 'DSFA loeschen', service: 'python' },
|
||||||
|
{ method: 'PATCH', path: '/{dsfa_id}/status', description: 'DSFA-Status aendern', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/{dsfa_id}/sections/{section_number}', description: 'DSFA-Abschnitt aktualisieren', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/{dsfa_id}/submit-for-review', description: 'Zur Pruefung einreichen', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/{dsfa_id}/approve', description: 'DSFA genehmigen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/{dsfa_id}/export', description: 'DSFA als JSON exportieren', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/{dsfa_id}/versions', description: 'Versionshistorie laden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/{dsfa_id}/versions/{version_number}', description: 'Bestimmte Version laden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/stats', description: 'DSFA-Statistiken laden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/audit-log', description: 'DSFA-Audit-Log laden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/export/csv', description: 'Alle DSFAs als CSV exportieren', service: 'python' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'dsr',
|
||||||
|
name: 'DSR — Betroffenenrechte (Admin)',
|
||||||
|
service: 'python',
|
||||||
|
basePath: '/api/compliance/dsr',
|
||||||
|
exposure: 'internal',
|
||||||
|
endpoints: [
|
||||||
|
{ method: 'POST', path: '/', description: 'DSR erstellen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/', description: 'DSRs auflisten', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/{dsr_id}', description: 'DSR laden', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/{dsr_id}', description: 'DSR aktualisieren', service: 'python' },
|
||||||
|
{ method: 'DELETE', path: '/{dsr_id}', description: 'DSR loeschen', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/{dsr_id}/status', description: 'Status aendern', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/{dsr_id}/verify-identity', description: 'Identitaet verifizieren', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/{dsr_id}/assign', description: 'DSR zuweisen', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/{dsr_id}/extend', description: 'Frist verlaengern', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/{dsr_id}/complete', description: 'DSR abschliessen', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/{dsr_id}/reject', description: 'DSR ablehnen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/{dsr_id}/history', description: 'Antragshistorie laden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/{dsr_id}/communications', description: 'Kommunikation laden', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/{dsr_id}/communicate', description: 'Nachricht senden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/{dsr_id}/exception-checks', description: 'Ausnahme-Checks laden', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/{dsr_id}/exception-checks/init', description: 'Ausnahme-Checks initialisieren', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/{dsr_id}/exception-checks/{check_id}', description: 'Ausnahme-Check aktualisieren', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/stats', description: 'DSR-Statistiken laden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/export', description: 'DSRs exportieren', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/deadlines/process', description: 'Fristen verarbeiten', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/templates', description: 'DSR-Vorlagen laden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/templates/published', description: 'Veroeffentlichte Vorlagen laden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/templates/{template_id}/versions', description: 'Vorlagen-Versionen laden', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/templates/{template_id}/versions', description: 'Vorlagen-Version erstellen', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/template-versions/{version_id}/publish', description: 'Vorlagen-Version veroeffentlichen', service: 'python' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'einwilligungen',
|
||||||
|
name: 'Einwilligungen — DSGVO-Einwilligungsverwaltung',
|
||||||
|
service: 'python',
|
||||||
|
basePath: '/api/compliance/einwilligungen',
|
||||||
|
exposure: 'internal',
|
||||||
|
endpoints: [
|
||||||
|
{ method: 'GET', path: '/catalog', description: 'Einwilligungskatalog laden', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/catalog', description: 'Katalog aktualisieren', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/company', description: 'Unternehmens-Consent-Einstellungen laden', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/company', description: 'Einstellungen aktualisieren', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/cookies', description: 'Cookie-Einwilligungen laden', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/cookies', description: 'Cookie-Einwilligungen aktualisieren', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/consents/stats', description: 'Statistiken laden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/consents', description: 'Einwilligungen auflisten (paginiert)', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/consents', description: 'Einwilligung erstellen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/consents/{consent_id}/history', description: 'Einwilligungshistorie laden', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/consents/{consent_id}/revoke', description: 'Einwilligung widerrufen', service: 'python' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'loeschfristen',
|
||||||
|
name: 'Loeschfristen — Aufbewahrung & Loeschung',
|
||||||
|
service: 'python',
|
||||||
|
basePath: '/api/compliance/loeschfristen',
|
||||||
|
exposure: 'internal',
|
||||||
|
endpoints: [
|
||||||
|
{ method: 'GET', path: '/', description: 'Loeschrichtlinien auflisten', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/', description: 'Richtlinie erstellen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/stats', description: 'Loeschfristen-Statistiken laden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/{policy_id}', description: 'Richtlinie laden', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/{policy_id}', description: 'Richtlinie aktualisieren', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/{policy_id}/status', description: 'Richtlinien-Status aendern', service: 'python' },
|
||||||
|
{ method: 'DELETE', path: '/{policy_id}', description: 'Richtlinie loeschen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/{policy_id}/versions', description: 'Versionshistorie laden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/{policy_id}/versions/{version_number}', description: 'Bestimmte Version laden', service: 'python' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'consent-user',
|
||||||
|
name: 'Consent API — Nutzer-Einwilligungen',
|
||||||
|
service: 'python',
|
||||||
|
basePath: '/api/consents',
|
||||||
|
exposure: 'public',
|
||||||
|
endpoints: [
|
||||||
|
{ method: 'GET', path: '/token/demo', description: 'Demo-Token laden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/check/{document_type}', description: 'Einwilligungsstatus pruefen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/pending', description: 'Offene Einwilligungen laden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/documents/{document_type}/latest', description: 'Aktuellstes Dokument laden', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/give', description: 'Einwilligung erteilen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/cookies/categories', description: 'Cookie-Kategorien laden', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/cookies', description: 'Cookie-Einwilligung setzen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/privacy/my-data', description: 'Eigene Daten laden', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/privacy/export', description: 'Datenexport anfordern', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/privacy/delete', description: 'Datenlöschung anfordern', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/health', description: 'Health-Check', service: 'python' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'consent-admin',
|
||||||
|
name: 'Consent Admin — Dokumenten- & Versionsverwaltung',
|
||||||
|
service: 'python',
|
||||||
|
basePath: '/api/admin/consents',
|
||||||
|
exposure: 'internal',
|
||||||
|
endpoints: [
|
||||||
|
{ method: 'GET', path: '/documents', description: 'Dokumente auflisten', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/documents', description: 'Dokument erstellen', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/documents/{doc_id}', description: 'Dokument aktualisieren', service: 'python' },
|
||||||
|
{ method: 'DELETE', path: '/documents/{doc_id}', description: 'Dokument loeschen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/documents/{doc_id}/versions', description: 'Versionen laden', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/versions', description: 'Version erstellen', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/versions/{version_id}', description: 'Version aktualisieren', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/versions/{version_id}/publish', description: 'Version veroeffentlichen', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/versions/{version_id}/archive', description: 'Version archivieren', service: 'python' },
|
||||||
|
{ method: 'DELETE', path: '/versions/{version_id}', description: 'Version loeschen', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/versions/{version_id}/submit-review', description: 'Zur Pruefung einreichen', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/versions/{version_id}/approve', description: 'Version genehmigen', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/versions/{version_id}/reject', description: 'Version ablehnen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/versions/{version_id}/compare', description: 'Versionen vergleichen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/versions/{version_id}/approval-history', description: 'Genehmigungshistorie laden', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/versions/upload-word', description: 'Word-Dokument hochladen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/scheduled-versions', description: 'Geplante Versionen laden', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/scheduled-publishing/process', description: 'Geplante Veroeffentlichungen verarbeiten', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/cookies/categories', description: 'Cookie-Kategorien laden', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/cookies/categories', description: 'Kategorie erstellen', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/cookies/categories/{cat_id}', description: 'Kategorie aktualisieren', service: 'python' },
|
||||||
|
{ method: 'DELETE', path: '/cookies/categories/{cat_id}', description: 'Kategorie loeschen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/statistics', description: 'Admin-Statistiken laden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/audit-log', description: 'Audit-Log laden', service: 'python' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'dsr-user',
|
||||||
|
name: 'DSR API — Nutzer-Betroffenenrechte',
|
||||||
|
service: 'python',
|
||||||
|
basePath: '/api/dsr',
|
||||||
|
exposure: 'public',
|
||||||
|
endpoints: [
|
||||||
|
{ method: 'POST', path: '/', description: 'Antrag stellen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/', description: 'Eigene Antraege laden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/{dsr_id}', description: 'Antrag laden', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/{dsr_id}/cancel', description: 'Antrag stornieren', service: 'python' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'dsr-admin',
|
||||||
|
name: 'DSR Admin — Antrags-Verwaltung',
|
||||||
|
service: 'python',
|
||||||
|
basePath: '/api/admin/dsr',
|
||||||
|
exposure: 'internal',
|
||||||
|
endpoints: [
|
||||||
|
{ method: 'GET', path: '/', description: 'Alle Antraege laden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/stats', description: 'DSR-Statistiken laden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/{dsr_id}', description: 'Antrag laden', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/', description: 'Antrag erstellen', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/{dsr_id}', description: 'Antrag aktualisieren', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/{dsr_id}/status', description: 'Status aendern', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/{dsr_id}/verify-identity', description: 'Identitaet verifizieren', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/{dsr_id}/assign', description: 'Zuweisen', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/{dsr_id}/extend', description: 'Frist verlaengern', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/{dsr_id}/complete', description: 'Abschliessen', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/{dsr_id}/reject', description: 'Ablehnen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/{dsr_id}/history', description: 'Historie laden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/{dsr_id}/communications', description: 'Kommunikation laden', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/{dsr_id}/communicate', description: 'Nachricht senden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/{dsr_id}/exception-checks', description: 'Ausnahme-Checks laden', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/{dsr_id}/exception-checks/init', description: 'Checks initialisieren', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/{dsr_id}/exception-checks/{check_id}', description: 'Check aktualisieren', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/deadlines/process', description: 'Fristen verarbeiten', service: 'python' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'gdpr',
|
||||||
|
name: 'GDPR / Datenschutz — Nutzerdaten & Export',
|
||||||
|
service: 'python',
|
||||||
|
basePath: '/api/gdpr',
|
||||||
|
exposure: 'public',
|
||||||
|
endpoints: [
|
||||||
|
{ method: 'POST', path: '/export-pdf', description: 'Nutzerdaten als PDF exportieren', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/export-html', description: 'Nutzerdaten als HTML exportieren', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/data-categories', description: 'Datenkategorien laden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/data-categories/{category}', description: 'Kategorie-Details laden', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/request-deletion', description: 'Datenlöschung beantragen', service: 'python' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
449
admin-compliance/lib/sdk/api-docs/endpoints-python-ops.ts
Normal file
449
admin-compliance/lib/sdk/api-docs/endpoints-python-ops.ts
Normal file
@@ -0,0 +1,449 @@
|
|||||||
|
/**
|
||||||
|
* Python/FastAPI endpoints — Operational compliance modules
|
||||||
|
* (tom, vvt, vendor-compliance, risks, evidence, incidents, escalations,
|
||||||
|
* email-templates, legal-documents, legal-templates, import, screening,
|
||||||
|
* scraper, source-policy, security-backlog, notfallplan, obligations,
|
||||||
|
* isms, quality)
|
||||||
|
*/
|
||||||
|
import { ApiModule } from './types'
|
||||||
|
|
||||||
|
export const pythonOpsModules: ApiModule[] = [
|
||||||
|
{
|
||||||
|
id: 'email-templates',
|
||||||
|
name: 'E-Mail-Vorlagen — Template-Verwaltung',
|
||||||
|
service: 'python',
|
||||||
|
basePath: '/api/compliance/email-templates',
|
||||||
|
exposure: 'internal',
|
||||||
|
endpoints: [
|
||||||
|
{ method: 'GET', path: '/types', description: 'Vorlagentypen laden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/stats', description: 'E-Mail-Statistiken laden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/settings', description: 'E-Mail-Einstellungen laden', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/settings', description: 'E-Mail-Einstellungen aktualisieren', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/logs', description: 'Versandprotokoll laden', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/initialize', description: 'Standard-Vorlagen initialisieren', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/', description: 'Vorlagen auflisten', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/', description: 'Vorlage erstellen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/{template_id}', description: 'Vorlage laden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/{template_id}/versions', description: 'Vorlagen-Versionen laden', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/{template_id}/versions', description: 'Version erstellen', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/versions', description: 'Version erstellen (alternativ)', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/versions/{version_id}', description: 'Version laden', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/versions/{version_id}', description: 'Version aktualisieren', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/versions/{version_id}/submit', description: 'Version einreichen', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/versions/{version_id}/approve', description: 'Version genehmigen', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/versions/{version_id}/reject', description: 'Version ablehnen', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/versions/{version_id}/publish', description: 'Version veroeffentlichen', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/versions/{version_id}/preview', description: 'Version-Vorschau generieren', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/versions/{version_id}/send-test', description: 'Test-E-Mail senden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/default/{template_type}', description: 'Standard-Vorlage laden', service: 'python' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'escalations',
|
||||||
|
name: 'Eskalationen — Eskalationsmanagement',
|
||||||
|
service: 'python',
|
||||||
|
basePath: '/api/compliance/escalations',
|
||||||
|
exposure: 'internal',
|
||||||
|
endpoints: [
|
||||||
|
{ method: 'GET', path: '/', description: 'Eskalationen auflisten', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/', description: 'Eskalation erstellen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/stats', description: 'Eskalations-Statistiken laden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/{escalation_id}', description: 'Eskalation laden', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/{escalation_id}', description: 'Eskalation aktualisieren', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/{escalation_id}/status', description: 'Eskalations-Status aendern', service: 'python' },
|
||||||
|
{ method: 'DELETE', path: '/{escalation_id}', description: 'Eskalation loeschen', service: 'python' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'evidence',
|
||||||
|
name: 'Nachweise — Evidence Management',
|
||||||
|
service: 'python',
|
||||||
|
basePath: '/api/compliance/evidence',
|
||||||
|
exposure: 'internal',
|
||||||
|
endpoints: [
|
||||||
|
{ method: 'GET', path: '/evidence', description: 'Nachweise auflisten', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/evidence', description: 'Nachweis erstellen', service: 'python' },
|
||||||
|
{ method: 'DELETE', path: '/evidence/{evidence_id}', description: 'Nachweis loeschen', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/evidence/upload', description: 'Nachweis-Datei hochladen', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/evidence/collect', description: 'CI-Nachweis sammeln', service: 'python', exposure: 'partner' },
|
||||||
|
{ method: 'GET', path: '/evidence/ci-status', description: 'CI-Nachweis-Status laden', service: 'python', exposure: 'partner' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'import',
|
||||||
|
name: 'Dokument-Import & Gap-Analyse',
|
||||||
|
service: 'python',
|
||||||
|
basePath: '/api/import',
|
||||||
|
exposure: 'internal',
|
||||||
|
endpoints: [
|
||||||
|
{ method: 'POST', path: '/analyze', description: 'Dokument analysieren', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/gap-analysis/{document_id}', description: 'Gap-Analyse laden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/documents', description: 'Importierte Dokumente auflisten', service: 'python' },
|
||||||
|
{ method: 'DELETE', path: '/{document_id}', description: 'Dokument loeschen', service: 'python' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'incidents',
|
||||||
|
name: 'Datenschutz-Vorfaelle — Incident Management',
|
||||||
|
service: 'python',
|
||||||
|
basePath: '/api/compliance/incidents',
|
||||||
|
exposure: 'internal',
|
||||||
|
endpoints: [
|
||||||
|
{ method: 'POST', path: '/', description: 'Vorfall erstellen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/', description: 'Vorfaelle auflisten', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/stats', description: 'Vorfall-Statistiken laden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/{incident_id}', description: 'Vorfall laden', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/{incident_id}', description: 'Vorfall aktualisieren', service: 'python' },
|
||||||
|
{ method: 'DELETE', path: '/{incident_id}', description: 'Vorfall loeschen', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/{incident_id}/status', description: 'Vorfall-Status aendern', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/{incident_id}/assess-risk', description: 'Risikobewertung durchfuehren', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/{incident_id}/notify-authority', description: 'Behoerde benachrichtigen', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/{incident_id}/notify-subjects', description: 'Betroffene benachrichtigen', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/{incident_id}/measures', description: 'Massnahme hinzufuegen', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/{incident_id}/measures/{measure_id}', description: 'Massnahme aktualisieren', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/{incident_id}/measures/{measure_id}/complete', description: 'Massnahme abschliessen', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/{incident_id}/timeline', description: 'Zeitachsen-Eintrag hinzufuegen', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/{incident_id}/close', description: 'Vorfall schliessen', service: 'python' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'isms',
|
||||||
|
name: 'ISMS — ISO 27001 Managementsystem',
|
||||||
|
service: 'python',
|
||||||
|
basePath: '/api/compliance/isms',
|
||||||
|
exposure: 'internal',
|
||||||
|
endpoints: [
|
||||||
|
{ method: 'GET', path: '/scope', description: 'ISMS-Scope laden', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/scope', description: 'ISMS-Scope erstellen', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/scope/{scope_id}', description: 'ISMS-Scope aktualisieren', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/scope/{scope_id}/approve', description: 'ISMS-Scope genehmigen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/context', description: 'ISMS-Kontext laden', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/context', description: 'ISMS-Kontext erstellen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/policies', description: 'Richtlinien auflisten', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/policies', description: 'Richtlinie erstellen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/policies/{policy_id}', description: 'Richtlinie laden', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/policies/{policy_id}', description: 'Richtlinie aktualisieren', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/policies/{policy_id}/approve', description: 'Richtlinie genehmigen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/objectives', description: 'Sicherheitsziele laden', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/objectives', description: 'Sicherheitsziel erstellen', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/objectives/{objective_id}', description: 'Sicherheitsziel aktualisieren', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/soa', description: 'Statement of Applicability laden', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/soa', description: 'SoA-Eintrag erstellen', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/soa/{entry_id}', description: 'SoA-Eintrag aktualisieren', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/soa/{entry_id}/approve', description: 'SoA-Eintrag genehmigen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/findings', description: 'Audit-Feststellungen laden', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/findings', description: 'Feststellung erstellen', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/findings/{finding_id}', description: 'Feststellung aktualisieren', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/findings/{finding_id}/close', description: 'Feststellung schliessen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/capa', description: 'Korrekturmassnahmen laden', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/capa', description: 'CAPA erstellen', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/capa/{capa_id}', description: 'CAPA aktualisieren', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/capa/{capa_id}/verify', description: 'CAPA verifizieren', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/management-reviews', description: 'Management-Reviews laden', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/management-reviews', description: 'Review erstellen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/management-reviews/{review_id}', description: 'Review laden', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/management-reviews/{review_id}', description: 'Review aktualisieren', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/management-reviews/{review_id}/approve', description: 'Review genehmigen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/internal-audits', description: 'Interne Audits laden', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/internal-audits', description: 'Internes Audit erstellen', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/internal-audits/{audit_id}', description: 'Audit aktualisieren', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/internal-audits/{audit_id}/complete', description: 'Audit abschliessen', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/readiness-check', description: 'Bereitschafts-Check ausfuehren', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/readiness-check/latest', description: 'Letzten Check laden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/audit-trail', description: 'Audit-Trail laden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/overview', description: 'ISO 27001 Uebersicht laden', service: 'python' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'legal-documents',
|
||||||
|
name: 'Rechtliche Dokumente — Verwaltung & Versionen',
|
||||||
|
service: 'python',
|
||||||
|
basePath: '/api/compliance/legal-documents',
|
||||||
|
exposure: 'internal',
|
||||||
|
endpoints: [
|
||||||
|
{ method: 'GET', path: '/documents', description: 'Dokumente auflisten', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/documents', description: 'Dokument erstellen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/documents/{document_id}', description: 'Dokument laden', service: 'python' },
|
||||||
|
{ method: 'DELETE', path: '/documents/{document_id}', description: 'Dokument loeschen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/documents/{document_id}/versions', description: 'Versionen laden', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/versions', description: 'Version erstellen', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/versions/{version_id}', description: 'Version aktualisieren', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/versions/{version_id}', description: 'Version laden', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/versions/upload-word', description: 'Word-Dokument hochladen', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/versions/{version_id}/submit-review', description: 'Zur Pruefung einreichen', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/versions/{version_id}/approve', description: 'Version genehmigen', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/versions/{version_id}/reject', description: 'Version ablehnen', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/versions/{version_id}/publish', description: 'Version veroeffentlichen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/versions/{version_id}/approval-history', description: 'Genehmigungshistorie laden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/public', description: 'Oeffentliche Dokumente laden', service: 'python', exposure: 'public' },
|
||||||
|
{ method: 'GET', path: '/public/{document_type}/latest', description: 'Aktuellstes Dokument laden', service: 'python', exposure: 'public' },
|
||||||
|
{ method: 'POST', path: '/consents', description: 'Einwilligung erfassen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/consents/my', description: 'Eigene Einwilligungen laden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/consents/check/{document_type}', description: 'Einwilligungsstatus pruefen', service: 'python' },
|
||||||
|
{ method: 'DELETE', path: '/consents/{consent_id}', description: 'Einwilligung widerrufen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/stats/consents', description: 'Einwilligungs-Statistiken laden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/audit-log', description: 'Audit-Log laden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/cookie-categories', description: 'Cookie-Kategorien auflisten', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/cookie-categories', description: 'Cookie-Kategorie erstellen', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/cookie-categories/{category_id}', description: 'Kategorie aktualisieren', service: 'python' },
|
||||||
|
{ method: 'DELETE', path: '/cookie-categories/{category_id}', description: 'Kategorie loeschen', service: 'python' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'legal-templates',
|
||||||
|
name: 'Dokumentvorlagen — DSGVO-Generatoren',
|
||||||
|
service: 'python',
|
||||||
|
basePath: '/api/compliance/legal-templates',
|
||||||
|
exposure: 'internal',
|
||||||
|
endpoints: [
|
||||||
|
{ method: 'GET', path: '/', description: 'Vorlagen auflisten', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/status', description: 'Vorlagenstatus laden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/sources', description: 'Vorlagenquellen laden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/{template_id}', description: 'Vorlage laden', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/', description: 'Vorlage erstellen', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/{template_id}', description: 'Vorlage aktualisieren', service: 'python' },
|
||||||
|
{ method: 'DELETE', path: '/{template_id}', description: 'Vorlage loeschen', service: 'python' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'notfallplan',
|
||||||
|
name: 'Notfallplan — Kontakte, Szenarien & Uebungen',
|
||||||
|
service: 'python',
|
||||||
|
basePath: '/api/compliance/notfallplan',
|
||||||
|
exposure: 'internal',
|
||||||
|
endpoints: [
|
||||||
|
{ method: 'GET', path: '/contacts', description: 'Notfallkontakte laden', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/contacts', description: 'Kontakt erstellen', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/contacts/{contact_id}', description: 'Kontakt aktualisieren', service: 'python' },
|
||||||
|
{ method: 'DELETE', path: '/contacts/{contact_id}', description: 'Kontakt loeschen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/scenarios', description: 'Notfallszenarien laden', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/scenarios', description: 'Szenario erstellen', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/scenarios/{scenario_id}', description: 'Szenario aktualisieren', service: 'python' },
|
||||||
|
{ method: 'DELETE', path: '/scenarios/{scenario_id}', description: 'Szenario loeschen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/checklists', description: 'Checklisten laden', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/checklists', description: 'Checkliste erstellen', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/checklists/{checklist_id}', description: 'Checkliste aktualisieren', service: 'python' },
|
||||||
|
{ method: 'DELETE', path: '/checklists/{checklist_id}', description: 'Checkliste loeschen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/exercises', description: 'Uebungen laden', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/exercises', description: 'Uebung erstellen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/incidents', description: 'Notfall-Vorfaelle laden', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/incidents', description: 'Vorfall erstellen', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/incidents/{incident_id}', description: 'Vorfall aktualisieren', service: 'python' },
|
||||||
|
{ method: 'DELETE', path: '/incidents/{incident_id}', description: 'Vorfall loeschen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/templates', description: 'Vorlagen laden', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/templates', description: 'Vorlage erstellen', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/templates/{template_id}', description: 'Vorlage aktualisieren', service: 'python' },
|
||||||
|
{ method: 'DELETE', path: '/templates/{template_id}', description: 'Vorlage loeschen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/stats', description: 'Notfallplan-Statistiken laden', service: 'python' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'obligations',
|
||||||
|
name: 'Pflichten — Compliance-Obligations',
|
||||||
|
service: 'python',
|
||||||
|
basePath: '/api/compliance/obligations',
|
||||||
|
exposure: 'internal',
|
||||||
|
endpoints: [
|
||||||
|
{ method: 'GET', path: '/', description: 'Pflichten auflisten', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/', description: 'Pflicht erstellen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/stats', description: 'Pflichten-Statistiken laden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/{obligation_id}', description: 'Pflicht laden', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/{obligation_id}', description: 'Pflicht aktualisieren', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/{obligation_id}/status', description: 'Pflicht-Status aendern', service: 'python' },
|
||||||
|
{ method: 'DELETE', path: '/{obligation_id}', description: 'Pflicht loeschen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/{obligation_id}/versions', description: 'Versionshistorie laden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/{obligation_id}/versions/{version_number}', description: 'Version laden', service: 'python' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'quality',
|
||||||
|
name: 'Quality — KI-Qualitaetsmetriken & Tests',
|
||||||
|
service: 'python',
|
||||||
|
basePath: '/api/compliance/quality',
|
||||||
|
exposure: 'internal',
|
||||||
|
endpoints: [
|
||||||
|
{ method: 'GET', path: '/stats', description: 'Qualitaets-Statistiken laden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/metrics', description: 'Metriken auflisten', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/metrics', description: 'Metrik erstellen', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/metrics/{metric_id}', description: 'Metrik aktualisieren', service: 'python' },
|
||||||
|
{ method: 'DELETE', path: '/metrics/{metric_id}', description: 'Metrik loeschen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/tests', description: 'Tests auflisten', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/tests', description: 'Test erstellen', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/tests/{test_id}', description: 'Test aktualisieren', service: 'python' },
|
||||||
|
{ method: 'DELETE', path: '/tests/{test_id}', description: 'Test loeschen', service: 'python' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'risks',
|
||||||
|
name: 'Risikomanagement — Bewertung & Matrix',
|
||||||
|
service: 'python',
|
||||||
|
basePath: '/api/compliance/risks',
|
||||||
|
exposure: 'internal',
|
||||||
|
endpoints: [
|
||||||
|
{ method: 'GET', path: '/risks', description: 'Risiken auflisten', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/risks', description: 'Risiko erstellen', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/risks/{risk_id}', description: 'Risiko aktualisieren', service: 'python' },
|
||||||
|
{ method: 'DELETE', path: '/risks/{risk_id}', description: 'Risiko loeschen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/risks/matrix', description: 'Risikomatrix laden', service: 'python' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'screening',
|
||||||
|
name: 'Screening — Abhaengigkeiten-Pruefung',
|
||||||
|
service: 'python',
|
||||||
|
basePath: '/api/compliance/screening',
|
||||||
|
exposure: 'internal',
|
||||||
|
endpoints: [
|
||||||
|
{ method: 'POST', path: '/scan', description: 'Abhaengigkeiten scannen', service: 'python', exposure: 'partner' },
|
||||||
|
{ method: 'GET', path: '/{screening_id}', description: 'Screening-Ergebnis laden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/', description: 'Screenings auflisten', service: 'python' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'scraper',
|
||||||
|
name: 'Scraper — Rechtsquellen-Aktualisierung',
|
||||||
|
service: 'python',
|
||||||
|
basePath: '/api/compliance/scraper',
|
||||||
|
exposure: 'partner',
|
||||||
|
endpoints: [
|
||||||
|
{ method: 'GET', path: '/scraper/status', description: 'Scraper-Status laden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/scraper/sources', description: 'Quellen auflisten', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/scraper/scrape-all', description: 'Alle Quellen scrapen', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/scraper/scrape/{code}', description: 'Einzelne Quelle scrapen', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/scraper/extract-bsi', description: 'BSI-Anforderungen extrahieren', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/scraper/extract-pdf', description: 'PDF-Anforderungen extrahieren', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/scraper/pdf-documents', description: 'PDF-Dokumente auflisten', service: 'python' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'security-backlog',
|
||||||
|
name: 'Security Backlog — Sicherheitsmassnahmen',
|
||||||
|
service: 'python',
|
||||||
|
basePath: '/api/compliance/security-backlog',
|
||||||
|
exposure: 'internal',
|
||||||
|
endpoints: [
|
||||||
|
{ method: 'GET', path: '/', description: 'Backlog-Eintraege auflisten', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/', description: 'Eintrag erstellen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/stats', description: 'Backlog-Statistiken laden', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/{item_id}', description: 'Eintrag aktualisieren', service: 'python' },
|
||||||
|
{ method: 'DELETE', path: '/{item_id}', description: 'Eintrag loeschen', service: 'python' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'source-policy',
|
||||||
|
name: 'Source Policy — Datenquellen & PII-Regeln',
|
||||||
|
service: 'python',
|
||||||
|
basePath: '/api/compliance/source-policy',
|
||||||
|
exposure: 'internal',
|
||||||
|
endpoints: [
|
||||||
|
{ method: 'GET', path: '/sources', description: 'Datenquellen auflisten', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/sources', description: 'Quelle erstellen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/sources/{source_id}', description: 'Quelle laden', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/sources/{source_id}', description: 'Quelle aktualisieren', service: 'python' },
|
||||||
|
{ method: 'DELETE', path: '/sources/{source_id}', description: 'Quelle loeschen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/operations-matrix', description: 'Operationsmatrix laden', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/operations/{operation_id}', description: 'Operation aktualisieren', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/pii-rules', description: 'PII-Regeln auflisten', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/pii-rules', description: 'PII-Regel erstellen', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/pii-rules/{rule_id}', description: 'PII-Regel aktualisieren', service: 'python' },
|
||||||
|
{ method: 'DELETE', path: '/pii-rules/{rule_id}', description: 'PII-Regel loeschen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/blocked-content', description: 'Gesperrte Inhalte laden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/policy-audit', description: 'Richtlinien-Audit-Log laden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/policy-stats', description: 'Richtlinien-Statistiken laden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/compliance-report', description: 'Compliance-Bericht laden', service: 'python' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'tom',
|
||||||
|
name: 'TOM — Technisch-Organisatorische Massnahmen',
|
||||||
|
service: 'python',
|
||||||
|
basePath: '/api/compliance/tom',
|
||||||
|
exposure: 'internal',
|
||||||
|
endpoints: [
|
||||||
|
{ method: 'GET', path: '/state', description: 'TOM-Zustand laden', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/state', description: 'TOM-Zustand speichern', service: 'python' },
|
||||||
|
{ method: 'DELETE', path: '/state', description: 'TOM-Zustand loeschen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/measures', description: 'Massnahmen auflisten', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/measures', description: 'Massnahme erstellen', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/measures/{measure_id}', description: 'Massnahme aktualisieren', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/measures/bulk', description: 'Massnahmen Bulk-Upsert', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/stats', description: 'TOM-Statistiken laden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/export', description: 'Massnahmen exportieren', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/measures/{measure_id}/versions', description: 'Versionshistorie laden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/measures/{measure_id}/versions/{version_number}', description: 'Version laden', service: 'python' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'vendor-compliance',
|
||||||
|
name: 'Vendor Compliance — Auftragsverarbeitung',
|
||||||
|
service: 'python',
|
||||||
|
basePath: '/api/compliance/vendors',
|
||||||
|
exposure: 'internal',
|
||||||
|
endpoints: [
|
||||||
|
{ method: 'GET', path: '/vendors/stats', description: 'Anbieter-Statistiken laden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/vendors', description: 'Anbieter auflisten', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/vendors/{vendor_id}', description: 'Anbieter laden', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/vendors', description: 'Anbieter erstellen', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/vendors/{vendor_id}', description: 'Anbieter aktualisieren', service: 'python' },
|
||||||
|
{ method: 'DELETE', path: '/vendors/{vendor_id}', description: 'Anbieter loeschen', service: 'python' },
|
||||||
|
{ method: 'PATCH', path: '/vendors/{vendor_id}/status', description: 'Anbieter-Status aendern', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/contracts', description: 'Vertraege auflisten', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/contracts/{contract_id}', description: 'Vertrag laden', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/contracts', description: 'Vertrag erstellen', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/contracts/{contract_id}', description: 'Vertrag aktualisieren', service: 'python' },
|
||||||
|
{ method: 'DELETE', path: '/contracts/{contract_id}', description: 'Vertrag loeschen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/findings', description: 'Feststellungen auflisten', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/findings/{finding_id}', description: 'Feststellung laden', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/findings', description: 'Feststellung erstellen', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/findings/{finding_id}', description: 'Feststellung aktualisieren', service: 'python' },
|
||||||
|
{ method: 'DELETE', path: '/findings/{finding_id}', description: 'Feststellung loeschen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/control-instances', description: 'Kontroll-Instanzen auflisten', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/control-instances/{instance_id}', description: 'Instanz laden', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/control-instances', description: 'Instanz erstellen', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/control-instances/{instance_id}', description: 'Instanz aktualisieren', service: 'python' },
|
||||||
|
{ method: 'DELETE', path: '/control-instances/{instance_id}', description: 'Instanz loeschen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/controls', description: 'Controls auflisten', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/controls', description: 'Control erstellen', service: 'python' },
|
||||||
|
{ method: 'DELETE', path: '/controls/{control_id}', description: 'Control loeschen', service: 'python' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'vvt',
|
||||||
|
name: 'VVT — Verarbeitungsverzeichnis (Art. 30 DSGVO)',
|
||||||
|
service: 'python',
|
||||||
|
basePath: '/api/compliance/vvt',
|
||||||
|
exposure: 'internal',
|
||||||
|
endpoints: [
|
||||||
|
{ method: 'GET', path: '/organization', description: 'Organisationskopf laden', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/organization', description: 'Organisationskopf speichern', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/activities', description: 'Verarbeitungstaetigkeiten auflisten', service: 'python' },
|
||||||
|
{ method: 'POST', path: '/activities', description: 'Taetigkeit erstellen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/activities/{activity_id}', description: 'Taetigkeit laden', service: 'python' },
|
||||||
|
{ method: 'PUT', path: '/activities/{activity_id}', description: 'Taetigkeit aktualisieren', service: 'python' },
|
||||||
|
{ method: 'DELETE', path: '/activities/{activity_id}', description: 'Taetigkeit loeschen', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/audit-log', description: 'VVT-Audit-Log laden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/export', description: 'VVT exportieren', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/stats', description: 'VVT-Statistiken laden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/activities/{activity_id}/versions', description: 'Versionshistorie laden', service: 'python' },
|
||||||
|
{ method: 'GET', path: '/activities/{activity_id}/versions/{version_number}', description: 'Version laden', service: 'python' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
146
admin-compliance/lib/sdk/dsr/api-crud.ts
Normal file
146
admin-compliance/lib/sdk/dsr/api-crud.ts
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
/**
|
||||||
|
* DSR API CRUD Operations
|
||||||
|
*
|
||||||
|
* List, create, read, update operations for DSR requests.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
DSRRequest,
|
||||||
|
DSRCreateRequest,
|
||||||
|
DSRStatistics,
|
||||||
|
} from './types'
|
||||||
|
import { BackendDSR, transformBackendDSR, getSdkHeaders } from './api-types'
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// LIST & STATISTICS
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch DSR list from compliance backend via proxy
|
||||||
|
*/
|
||||||
|
export async function fetchSDKDSRList(): Promise<{ requests: DSRRequest[]; statistics: DSRStatistics }> {
|
||||||
|
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 listData = await listRes.json()
|
||||||
|
const backendDSRs: BackendDSR[] = listData.requests || []
|
||||||
|
const requests = backendDSRs.map(transformBackendDSR)
|
||||||
|
|
||||||
|
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 {
|
||||||
|
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 }
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// SINGLE RESOURCE OPERATIONS
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new DSR via compliance backend
|
||||||
|
*/
|
||||||
|
export async function createSDKDSR(request: DSRCreateRequest): Promise<void> {
|
||||||
|
const body = {
|
||||||
|
request_type: request.type,
|
||||||
|
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/compliance/dsr', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getSdkHeaders(),
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
})
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`HTTP ${res.status}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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/compliance/dsr/${id}`, {
|
||||||
|
headers: getSdkHeaders(),
|
||||||
|
})
|
||||||
|
if (!res.ok) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const data = await res.json()
|
||||||
|
if (!data || !data.id) return null
|
||||||
|
return transformBackendDSR(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update DSR status via compliance backend
|
||||||
|
*/
|
||||||
|
export async function updateSDKDSRStatus(id: string, status: string): Promise<void> {
|
||||||
|
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/status`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getSdkHeaders(),
|
||||||
|
body: JSON.stringify({ status }),
|
||||||
|
})
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`HTTP ${res.status}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update DSR fields (priority, notes, etc.)
|
||||||
|
*/
|
||||||
|
export async function updateDSR(id: string, data: Record<string, any>): Promise<DSRRequest> {
|
||||||
|
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: getSdkHeaders(),
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
})
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||||
|
return transformBackendDSR(await res.json())
|
||||||
|
}
|
||||||
259
admin-compliance/lib/sdk/dsr/api-mock.ts
Normal file
259
admin-compliance/lib/sdk/dsr/api-mock.ts
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
/**
|
||||||
|
* DSR Mock Data
|
||||||
|
*
|
||||||
|
* Mock DSR requests and statistics for development/testing fallback.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { DSRRequest, DSRStatistics } from './types'
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// MOCK DATA FUNCTIONS
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export function createMockDSRList(): DSRRequest[] {
|
||||||
|
const now = new Date()
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'dsr-001',
|
||||||
|
referenceNumber: 'DSR-2025-000001',
|
||||||
|
type: 'access',
|
||||||
|
status: 'intake',
|
||||||
|
priority: 'high',
|
||||||
|
requester: {
|
||||||
|
name: 'Max Mustermann',
|
||||||
|
email: 'max.mustermann@example.de'
|
||||||
|
},
|
||||||
|
source: 'web_form',
|
||||||
|
sourceDetails: 'Kontaktformular auf breakpilot.de',
|
||||||
|
receivedAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
deadline: {
|
||||||
|
originalDeadline: new Date(now.getTime() + 28 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
currentDeadline: new Date(now.getTime() + 28 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
extended: false
|
||||||
|
},
|
||||||
|
identityVerification: { verified: false },
|
||||||
|
assignment: { assignedTo: null },
|
||||||
|
createdAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
createdBy: 'system',
|
||||||
|
updatedAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
tenantId: 'default-tenant'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'dsr-002',
|
||||||
|
referenceNumber: 'DSR-2025-000002',
|
||||||
|
type: 'erasure',
|
||||||
|
status: 'identity_verification',
|
||||||
|
priority: 'high',
|
||||||
|
requester: {
|
||||||
|
name: 'Anna Schmidt',
|
||||||
|
email: 'anna.schmidt@example.de',
|
||||||
|
phone: '+49 170 1234567'
|
||||||
|
},
|
||||||
|
source: 'email',
|
||||||
|
requestText: 'Ich moechte, dass alle meine Daten geloescht werden.',
|
||||||
|
receivedAt: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
deadline: {
|
||||||
|
originalDeadline: new Date(now.getTime() + 9 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
currentDeadline: new Date(now.getTime() + 9 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
extended: false
|
||||||
|
},
|
||||||
|
identityVerification: { verified: false },
|
||||||
|
assignment: {
|
||||||
|
assignedTo: 'DSB Mueller',
|
||||||
|
assignedAt: new Date(now.getTime() - 4 * 24 * 60 * 60 * 1000).toISOString()
|
||||||
|
},
|
||||||
|
createdAt: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
createdBy: 'system',
|
||||||
|
updatedAt: new Date(now.getTime() - 4 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
tenantId: 'default-tenant'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'dsr-003',
|
||||||
|
referenceNumber: 'DSR-2025-000003',
|
||||||
|
type: 'rectification',
|
||||||
|
status: 'processing',
|
||||||
|
priority: 'normal',
|
||||||
|
requester: {
|
||||||
|
name: 'Peter Meier',
|
||||||
|
email: 'peter.meier@example.de'
|
||||||
|
},
|
||||||
|
source: 'email',
|
||||||
|
requestText: 'Meine Adresse ist falsch gespeichert.',
|
||||||
|
receivedAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
deadline: {
|
||||||
|
originalDeadline: new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
currentDeadline: new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
extended: false
|
||||||
|
},
|
||||||
|
identityVerification: {
|
||||||
|
verified: true,
|
||||||
|
method: 'existing_account',
|
||||||
|
verifiedAt: new Date(now.getTime() - 6 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
verifiedBy: 'DSB Mueller'
|
||||||
|
},
|
||||||
|
assignment: {
|
||||||
|
assignedTo: 'DSB Mueller',
|
||||||
|
assignedAt: new Date(now.getTime() - 6 * 24 * 60 * 60 * 1000).toISOString()
|
||||||
|
},
|
||||||
|
rectificationDetails: {
|
||||||
|
fieldsToCorrect: [
|
||||||
|
{
|
||||||
|
field: 'Adresse',
|
||||||
|
currentValue: 'Musterstr. 1, 12345 Berlin',
|
||||||
|
requestedValue: 'Musterstr. 10, 12345 Berlin',
|
||||||
|
corrected: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
createdAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
createdBy: 'system',
|
||||||
|
updatedAt: new Date(now.getTime() - 1 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
tenantId: 'default-tenant'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'dsr-004',
|
||||||
|
referenceNumber: 'DSR-2025-000004',
|
||||||
|
type: 'portability',
|
||||||
|
status: 'processing',
|
||||||
|
priority: 'normal',
|
||||||
|
requester: {
|
||||||
|
name: 'Lisa Weber',
|
||||||
|
email: 'lisa.weber@example.de'
|
||||||
|
},
|
||||||
|
source: 'web_form',
|
||||||
|
receivedAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
deadline: {
|
||||||
|
originalDeadline: new Date(now.getTime() + 20 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
currentDeadline: new Date(now.getTime() + 20 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
extended: false
|
||||||
|
},
|
||||||
|
identityVerification: {
|
||||||
|
verified: true,
|
||||||
|
method: 'id_document',
|
||||||
|
verifiedAt: new Date(now.getTime() - 8 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
verifiedBy: 'DSB Mueller'
|
||||||
|
},
|
||||||
|
assignment: {
|
||||||
|
assignedTo: 'IT Team',
|
||||||
|
assignedAt: new Date(now.getTime() - 8 * 24 * 60 * 60 * 1000).toISOString()
|
||||||
|
},
|
||||||
|
notes: 'JSON-Export wird vorbereitet',
|
||||||
|
createdAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
createdBy: 'system',
|
||||||
|
updatedAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
tenantId: 'default-tenant'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'dsr-005',
|
||||||
|
referenceNumber: 'DSR-2025-000005',
|
||||||
|
type: 'objection',
|
||||||
|
status: 'rejected',
|
||||||
|
priority: 'low',
|
||||||
|
requester: {
|
||||||
|
name: 'Thomas Klein',
|
||||||
|
email: 'thomas.klein@example.de'
|
||||||
|
},
|
||||||
|
source: 'letter',
|
||||||
|
requestText: 'Ich widerspreche der Verarbeitung meiner Daten fuer Marketingzwecke.',
|
||||||
|
receivedAt: new Date(now.getTime() - 35 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
deadline: {
|
||||||
|
originalDeadline: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
currentDeadline: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
extended: false
|
||||||
|
},
|
||||||
|
completedAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
identityVerification: {
|
||||||
|
verified: true,
|
||||||
|
method: 'postal',
|
||||||
|
verifiedAt: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
verifiedBy: 'DSB Mueller'
|
||||||
|
},
|
||||||
|
assignment: {
|
||||||
|
assignedTo: 'Rechtsabteilung',
|
||||||
|
assignedAt: new Date(now.getTime() - 28 * 24 * 60 * 60 * 1000).toISOString()
|
||||||
|
},
|
||||||
|
objectionDetails: {
|
||||||
|
processingPurpose: 'Marketing',
|
||||||
|
legalBasis: 'Berechtigtes Interesse (Art. 6(1)(f))',
|
||||||
|
objectionGrounds: 'Keine konkreten Gruende genannt',
|
||||||
|
decision: 'rejected',
|
||||||
|
decisionReason: 'Zwingende schutzwuerdige Gruende fuer die Verarbeitung ueberwiegen',
|
||||||
|
decisionBy: 'Rechtsabteilung',
|
||||||
|
decisionAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString()
|
||||||
|
},
|
||||||
|
notes: 'Widerspruch unberechtigt - zwingende schutzwuerdige Gruende',
|
||||||
|
createdAt: new Date(now.getTime() - 35 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
createdBy: 'system',
|
||||||
|
updatedAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
tenantId: 'default-tenant'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'dsr-006',
|
||||||
|
referenceNumber: 'DSR-2025-000006',
|
||||||
|
type: 'access',
|
||||||
|
status: 'completed',
|
||||||
|
priority: 'normal',
|
||||||
|
requester: {
|
||||||
|
name: 'Sarah Braun',
|
||||||
|
email: 'sarah.braun@example.de'
|
||||||
|
},
|
||||||
|
source: 'email',
|
||||||
|
receivedAt: new Date(now.getTime() - 45 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
deadline: {
|
||||||
|
originalDeadline: new Date(now.getTime() - 15 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
currentDeadline: new Date(now.getTime() - 15 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
extended: false
|
||||||
|
},
|
||||||
|
completedAt: new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
identityVerification: {
|
||||||
|
verified: true,
|
||||||
|
method: 'id_document',
|
||||||
|
verifiedAt: new Date(now.getTime() - 42 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
verifiedBy: 'DSB Mueller'
|
||||||
|
},
|
||||||
|
assignment: {
|
||||||
|
assignedTo: 'DSB Mueller',
|
||||||
|
assignedAt: new Date(now.getTime() - 42 * 24 * 60 * 60 * 1000).toISOString()
|
||||||
|
},
|
||||||
|
dataExport: {
|
||||||
|
format: 'pdf',
|
||||||
|
generatedAt: new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
generatedBy: 'DSB Mueller',
|
||||||
|
fileName: 'datenauskunft_sarah_braun.pdf',
|
||||||
|
fileSize: 245000,
|
||||||
|
includesThirdPartyData: false
|
||||||
|
},
|
||||||
|
createdAt: new Date(now.getTime() - 45 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
createdBy: 'system',
|
||||||
|
updatedAt: new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
tenantId: 'default-tenant'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createMockStatistics(): DSRStatistics {
|
||||||
|
return {
|
||||||
|
total: 6,
|
||||||
|
byStatus: {
|
||||||
|
intake: 1,
|
||||||
|
identity_verification: 1,
|
||||||
|
processing: 2,
|
||||||
|
completed: 1,
|
||||||
|
rejected: 1,
|
||||||
|
cancelled: 0
|
||||||
|
},
|
||||||
|
byType: {
|
||||||
|
access: 2,
|
||||||
|
rectification: 1,
|
||||||
|
erasure: 1,
|
||||||
|
restriction: 0,
|
||||||
|
portability: 1,
|
||||||
|
objection: 1
|
||||||
|
},
|
||||||
|
overdue: 0,
|
||||||
|
dueThisWeek: 2,
|
||||||
|
averageProcessingDays: 18,
|
||||||
|
completedThisMonth: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
133
admin-compliance/lib/sdk/dsr/api-types.ts
Normal file
133
admin-compliance/lib/sdk/dsr/api-types.ts
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
/**
|
||||||
|
* DSR API Types & Transform
|
||||||
|
*
|
||||||
|
* Backend DSR type definition and transformation to frontend DSRRequest format.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { DSRRequest } from './types'
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// BACKEND TYPE
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export interface BackendDSR {
|
||||||
|
id: string
|
||||||
|
tenant_id: string
|
||||||
|
request_number: string
|
||||||
|
request_type: string
|
||||||
|
status: 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
|
||||||
|
deadline_at: string
|
||||||
|
extended_deadline_at?: string
|
||||||
|
extension_reason?: 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
|
||||||
|
created_by?: string
|
||||||
|
updated_by?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// TRANSFORM
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
||||||
|
return {
|
||||||
|
id: b.id,
|
||||||
|
referenceNumber: b.request_number,
|
||||||
|
type: b.request_type as DSRRequest['type'],
|
||||||
|
status: (b.status as DSRRequest['status']) || 'intake',
|
||||||
|
priority: (b.priority as DSRRequest['priority']) || 'normal',
|
||||||
|
requester: {
|
||||||
|
name: b.requester_name,
|
||||||
|
email: b.requester_email,
|
||||||
|
phone: b.requester_phone,
|
||||||
|
address: b.requester_address,
|
||||||
|
customerId: b.requester_customer_id,
|
||||||
|
},
|
||||||
|
source: (b.source as DSRRequest['source']) || 'email',
|
||||||
|
sourceDetails: b.source_details,
|
||||||
|
requestText: b.request_text,
|
||||||
|
receivedAt: b.received_at,
|
||||||
|
deadline: {
|
||||||
|
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.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: b.created_by || 'system',
|
||||||
|
updatedAt: b.updated_at,
|
||||||
|
updatedBy: b.updated_by,
|
||||||
|
tenantId: b.tenant_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// SHARED HELPERS
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export function getSdkHeaders(): HeadersInit {
|
||||||
|
if (typeof window === 'undefined') return {}
|
||||||
|
return {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Tenant-ID': localStorage.getItem('bp_tenant_id') || '',
|
||||||
|
'X-User-ID': localStorage.getItem('bp_user_id') || '',
|
||||||
|
}
|
||||||
|
}
|
||||||
161
admin-compliance/lib/sdk/dsr/api-workflow.ts
Normal file
161
admin-compliance/lib/sdk/dsr/api-workflow.ts
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
/**
|
||||||
|
* DSR API Workflow Actions
|
||||||
|
*
|
||||||
|
* Workflow operations: identity verification, assignment, deadline extension,
|
||||||
|
* completion, rejection, communications, exception checks, and history.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { DSRRequest } from './types'
|
||||||
|
import { transformBackendDSR, getSdkHeaders } from './api-types'
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// WORKFLOW ACTIONS
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify identity of DSR requester
|
||||||
|
*/
|
||||||
|
export async function verifyDSRIdentity(id: string, data: { method: string; notes?: string; document_ref?: string }): Promise<DSRRequest> {
|
||||||
|
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/verify-identity`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getSdkHeaders(),
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
})
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||||
|
return transformBackendDSR(await res.json())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assign DSR to a user
|
||||||
|
*/
|
||||||
|
export async function assignDSR(id: string, assigneeId: string): Promise<DSRRequest> {
|
||||||
|
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/assign`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getSdkHeaders(),
|
||||||
|
body: JSON.stringify({ assignee_id: assigneeId }),
|
||||||
|
})
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||||
|
return transformBackendDSR(await res.json())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extend DSR deadline (Art. 12 Abs. 3 DSGVO)
|
||||||
|
*/
|
||||||
|
export async function extendDSRDeadline(id: string, reason: string, days: number = 60): Promise<DSRRequest> {
|
||||||
|
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/extend`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getSdkHeaders(),
|
||||||
|
body: JSON.stringify({ reason, days }),
|
||||||
|
})
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||||
|
return transformBackendDSR(await res.json())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Complete a DSR
|
||||||
|
*/
|
||||||
|
export async function completeDSR(id: string, summary?: string): Promise<DSRRequest> {
|
||||||
|
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/complete`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getSdkHeaders(),
|
||||||
|
body: JSON.stringify({ summary }),
|
||||||
|
})
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||||
|
return transformBackendDSR(await res.json())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reject a DSR with legal basis
|
||||||
|
*/
|
||||||
|
export async function rejectDSR(id: string, reason: string, legalBasis?: string): Promise<DSRRequest> {
|
||||||
|
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/reject`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getSdkHeaders(),
|
||||||
|
body: JSON.stringify({ reason, legal_basis: legalBasis }),
|
||||||
|
})
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||||
|
return transformBackendDSR(await res.json())
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// COMMUNICATIONS
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch communications for a DSR
|
||||||
|
*/
|
||||||
|
export async function fetchDSRCommunications(id: string): Promise<any[]> {
|
||||||
|
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/communications`, {
|
||||||
|
headers: getSdkHeaders(),
|
||||||
|
})
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||||
|
return res.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a communication for a DSR
|
||||||
|
*/
|
||||||
|
export async function sendDSRCommunication(id: string, data: { communication_type?: string; channel?: string; subject?: string; content: string }): Promise<any> {
|
||||||
|
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/communicate`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getSdkHeaders(),
|
||||||
|
body: JSON.stringify({ communication_type: 'outgoing', channel: 'email', ...data }),
|
||||||
|
})
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||||
|
return res.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// EXCEPTION CHECKS (Art. 17)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch exception checks for an erasure DSR
|
||||||
|
*/
|
||||||
|
export async function fetchDSRExceptionChecks(id: string): Promise<any[]> {
|
||||||
|
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/exception-checks`, {
|
||||||
|
headers: getSdkHeaders(),
|
||||||
|
})
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||||
|
return res.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize Art. 17(3) exception checks for an erasure DSR
|
||||||
|
*/
|
||||||
|
export async function initDSRExceptionChecks(id: string): Promise<any[]> {
|
||||||
|
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/exception-checks/init`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getSdkHeaders(),
|
||||||
|
})
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||||
|
return res.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a single exception check
|
||||||
|
*/
|
||||||
|
export async function updateDSRExceptionCheck(dsrId: string, checkId: string, data: { applies: boolean; notes?: string }): Promise<any> {
|
||||||
|
const res = await fetch(`/api/sdk/v1/compliance/dsr/${dsrId}/exception-checks/${checkId}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: getSdkHeaders(),
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
})
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||||
|
return res.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// HISTORY
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch status change history for a DSR
|
||||||
|
*/
|
||||||
|
export async function fetchDSRHistory(id: string): Promise<any[]> {
|
||||||
|
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/history`, {
|
||||||
|
headers: getSdkHeaders(),
|
||||||
|
})
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||||
|
return res.json()
|
||||||
|
}
|
||||||
@@ -1,669 +1,38 @@
|
|||||||
/**
|
/**
|
||||||
* DSR API Client
|
* DSR API Client — Barrel re-exports
|
||||||
*
|
* Preserves the original public API so existing imports work unchanged.
|
||||||
* API client for Data Subject Request management.
|
|
||||||
* Connects to the native compliance backend (Python/FastAPI).
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
// Types & transform
|
||||||
DSRRequest,
|
export { transformBackendDSR, getSdkHeaders } from './api-types'
|
||||||
DSRCreateRequest,
|
export type { BackendDSR } from './api-types'
|
||||||
DSRStatistics,
|
|
||||||
} from './types'
|
|
||||||
|
|
||||||
// =============================================================================
|
// CRUD operations
|
||||||
// SDK API FUNCTIONS (via Next.js proxy to compliance backend)
|
export {
|
||||||
// =============================================================================
|
fetchSDKDSRList,
|
||||||
|
createSDKDSR,
|
||||||
|
fetchSDKDSR,
|
||||||
|
updateSDKDSRStatus,
|
||||||
|
updateDSR,
|
||||||
|
} from './api-crud'
|
||||||
|
|
||||||
interface BackendDSR {
|
// Workflow actions
|
||||||
id: string
|
export {
|
||||||
tenant_id: string
|
verifyDSRIdentity,
|
||||||
request_number: string
|
assignDSR,
|
||||||
request_type: string
|
extendDSRDeadline,
|
||||||
status: string
|
completeDSR,
|
||||||
priority: string
|
rejectDSR,
|
||||||
requester_name: string
|
fetchDSRCommunications,
|
||||||
requester_email: string
|
sendDSRCommunication,
|
||||||
requester_phone?: string
|
fetchDSRExceptionChecks,
|
||||||
requester_address?: string
|
initDSRExceptionChecks,
|
||||||
requester_customer_id?: string
|
updateDSRExceptionCheck,
|
||||||
source: string
|
fetchDSRHistory,
|
||||||
source_details?: string
|
} from './api-workflow'
|
||||||
request_text?: string
|
|
||||||
notes?: string
|
|
||||||
internal_notes?: string
|
|
||||||
received_at: string
|
|
||||||
deadline_at: string
|
|
||||||
extended_deadline_at?: string
|
|
||||||
extension_reason?: 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
|
|
||||||
created_by?: string
|
|
||||||
updated_by?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
// Mock data
|
||||||
* Transform flat backend DSR to nested SDK DSRRequest format.
|
export {
|
||||||
* New compliance backend already uses the same status names as frontend types.
|
createMockDSRList,
|
||||||
*/
|
createMockStatistics,
|
||||||
export function transformBackendDSR(b: BackendDSR): DSRRequest {
|
} from './api-mock'
|
||||||
return {
|
|
||||||
id: b.id,
|
|
||||||
referenceNumber: b.request_number,
|
|
||||||
type: b.request_type as DSRRequest['type'],
|
|
||||||
status: (b.status as DSRRequest['status']) || 'intake',
|
|
||||||
priority: (b.priority as DSRRequest['priority']) || 'normal',
|
|
||||||
requester: {
|
|
||||||
name: b.requester_name,
|
|
||||||
email: b.requester_email,
|
|
||||||
phone: b.requester_phone,
|
|
||||||
address: b.requester_address,
|
|
||||||
customerId: b.requester_customer_id,
|
|
||||||
},
|
|
||||||
source: (b.source as DSRRequest['source']) || 'email',
|
|
||||||
sourceDetails: b.source_details,
|
|
||||||
requestText: b.request_text,
|
|
||||||
receivedAt: b.received_at,
|
|
||||||
deadline: {
|
|
||||||
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.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: b.created_by || 'system',
|
|
||||||
updatedAt: b.updated_at,
|
|
||||||
updatedBy: b.updated_by,
|
|
||||||
tenantId: b.tenant_id,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSdkHeaders(): HeadersInit {
|
|
||||||
if (typeof window === 'undefined') return {}
|
|
||||||
return {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-Tenant-ID': localStorage.getItem('bp_tenant_id') || '',
|
|
||||||
'X-User-ID': localStorage.getItem('bp_user_id') || '',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch DSR list from compliance backend via proxy
|
|
||||||
*/
|
|
||||||
export async function fetchSDKDSRList(): Promise<{ requests: DSRRequest[]; statistics: DSRStatistics }> {
|
|
||||||
// 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 listData = await listRes.json()
|
|
||||||
const backendDSRs: BackendDSR[] = listData.requests || []
|
|
||||||
const requests = backendDSRs.map(transformBackendDSR)
|
|
||||||
|
|
||||||
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 compliance backend
|
|
||||||
*/
|
|
||||||
export async function createSDKDSR(request: DSRCreateRequest): Promise<void> {
|
|
||||||
const body = {
|
|
||||||
request_type: request.type,
|
|
||||||
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/compliance/dsr', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: getSdkHeaders(),
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
})
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error(`HTTP ${res.status}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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/compliance/dsr/${id}`, {
|
|
||||||
headers: getSdkHeaders(),
|
|
||||||
})
|
|
||||||
if (!res.ok) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
const data = await res.json()
|
|
||||||
if (!data || !data.id) return null
|
|
||||||
return transformBackendDSR(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update DSR status via compliance backend
|
|
||||||
*/
|
|
||||||
export async function updateSDKDSRStatus(id: string, status: string): Promise<void> {
|
|
||||||
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/status`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: getSdkHeaders(),
|
|
||||||
body: JSON.stringify({ status }),
|
|
||||||
})
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error(`HTTP ${res.status}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// WORKFLOW ACTIONS
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verify identity of DSR requester
|
|
||||||
*/
|
|
||||||
export async function verifyDSRIdentity(id: string, data: { method: string; notes?: string; document_ref?: string }): Promise<DSRRequest> {
|
|
||||||
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/verify-identity`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: getSdkHeaders(),
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
})
|
|
||||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
||||||
return transformBackendDSR(await res.json())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Assign DSR to a user
|
|
||||||
*/
|
|
||||||
export async function assignDSR(id: string, assigneeId: string): Promise<DSRRequest> {
|
|
||||||
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/assign`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: getSdkHeaders(),
|
|
||||||
body: JSON.stringify({ assignee_id: assigneeId }),
|
|
||||||
})
|
|
||||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
||||||
return transformBackendDSR(await res.json())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extend DSR deadline (Art. 12 Abs. 3 DSGVO)
|
|
||||||
*/
|
|
||||||
export async function extendDSRDeadline(id: string, reason: string, days: number = 60): Promise<DSRRequest> {
|
|
||||||
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/extend`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: getSdkHeaders(),
|
|
||||||
body: JSON.stringify({ reason, days }),
|
|
||||||
})
|
|
||||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
||||||
return transformBackendDSR(await res.json())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Complete a DSR
|
|
||||||
*/
|
|
||||||
export async function completeDSR(id: string, summary?: string): Promise<DSRRequest> {
|
|
||||||
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/complete`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: getSdkHeaders(),
|
|
||||||
body: JSON.stringify({ summary }),
|
|
||||||
})
|
|
||||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
||||||
return transformBackendDSR(await res.json())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reject a DSR with legal basis
|
|
||||||
*/
|
|
||||||
export async function rejectDSR(id: string, reason: string, legalBasis?: string): Promise<DSRRequest> {
|
|
||||||
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/reject`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: getSdkHeaders(),
|
|
||||||
body: JSON.stringify({ reason, legal_basis: legalBasis }),
|
|
||||||
})
|
|
||||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
||||||
return transformBackendDSR(await res.json())
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// COMMUNICATIONS
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch communications for a DSR
|
|
||||||
*/
|
|
||||||
export async function fetchDSRCommunications(id: string): Promise<any[]> {
|
|
||||||
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/communications`, {
|
|
||||||
headers: getSdkHeaders(),
|
|
||||||
})
|
|
||||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
||||||
return res.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send a communication for a DSR
|
|
||||||
*/
|
|
||||||
export async function sendDSRCommunication(id: string, data: { communication_type?: string; channel?: string; subject?: string; content: string }): Promise<any> {
|
|
||||||
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/communicate`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: getSdkHeaders(),
|
|
||||||
body: JSON.stringify({ communication_type: 'outgoing', channel: 'email', ...data }),
|
|
||||||
})
|
|
||||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
||||||
return res.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// EXCEPTION CHECKS (Art. 17)
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch exception checks for an erasure DSR
|
|
||||||
*/
|
|
||||||
export async function fetchDSRExceptionChecks(id: string): Promise<any[]> {
|
|
||||||
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/exception-checks`, {
|
|
||||||
headers: getSdkHeaders(),
|
|
||||||
})
|
|
||||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
||||||
return res.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize Art. 17(3) exception checks for an erasure DSR
|
|
||||||
*/
|
|
||||||
export async function initDSRExceptionChecks(id: string): Promise<any[]> {
|
|
||||||
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/exception-checks/init`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: getSdkHeaders(),
|
|
||||||
})
|
|
||||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
||||||
return res.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update a single exception check
|
|
||||||
*/
|
|
||||||
export async function updateDSRExceptionCheck(dsrId: string, checkId: string, data: { applies: boolean; notes?: string }): Promise<any> {
|
|
||||||
const res = await fetch(`/api/sdk/v1/compliance/dsr/${dsrId}/exception-checks/${checkId}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: getSdkHeaders(),
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
})
|
|
||||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
||||||
return res.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// HISTORY
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch status change history for a DSR
|
|
||||||
*/
|
|
||||||
export async function fetchDSRHistory(id: string): Promise<any[]> {
|
|
||||||
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/history`, {
|
|
||||||
headers: getSdkHeaders(),
|
|
||||||
})
|
|
||||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
||||||
return res.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update DSR fields (priority, notes, etc.)
|
|
||||||
*/
|
|
||||||
export async function updateDSR(id: string, data: Record<string, any>): Promise<DSRRequest> {
|
|
||||||
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: getSdkHeaders(),
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
})
|
|
||||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
||||||
return transformBackendDSR(await res.json())
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// MOCK DATA FUNCTIONS (kept as fallback)
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
export function createMockDSRList(): DSRRequest[] {
|
|
||||||
const now = new Date()
|
|
||||||
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
id: 'dsr-001',
|
|
||||||
referenceNumber: 'DSR-2025-000001',
|
|
||||||
type: 'access',
|
|
||||||
status: 'intake',
|
|
||||||
priority: 'high',
|
|
||||||
requester: {
|
|
||||||
name: 'Max Mustermann',
|
|
||||||
email: 'max.mustermann@example.de'
|
|
||||||
},
|
|
||||||
source: 'web_form',
|
|
||||||
sourceDetails: 'Kontaktformular auf breakpilot.de',
|
|
||||||
receivedAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
deadline: {
|
|
||||||
originalDeadline: new Date(now.getTime() + 28 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
currentDeadline: new Date(now.getTime() + 28 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
extended: false
|
|
||||||
},
|
|
||||||
identityVerification: {
|
|
||||||
verified: false
|
|
||||||
},
|
|
||||||
assignment: {
|
|
||||||
assignedTo: null
|
|
||||||
},
|
|
||||||
createdAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
createdBy: 'system',
|
|
||||||
updatedAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
tenantId: 'default-tenant'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'dsr-002',
|
|
||||||
referenceNumber: 'DSR-2025-000002',
|
|
||||||
type: 'erasure',
|
|
||||||
status: 'identity_verification',
|
|
||||||
priority: 'high',
|
|
||||||
requester: {
|
|
||||||
name: 'Anna Schmidt',
|
|
||||||
email: 'anna.schmidt@example.de',
|
|
||||||
phone: '+49 170 1234567'
|
|
||||||
},
|
|
||||||
source: 'email',
|
|
||||||
requestText: 'Ich moechte, dass alle meine Daten geloescht werden.',
|
|
||||||
receivedAt: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
deadline: {
|
|
||||||
originalDeadline: new Date(now.getTime() + 9 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
currentDeadline: new Date(now.getTime() + 9 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
extended: false
|
|
||||||
},
|
|
||||||
identityVerification: {
|
|
||||||
verified: false
|
|
||||||
},
|
|
||||||
assignment: {
|
|
||||||
assignedTo: 'DSB Mueller',
|
|
||||||
assignedAt: new Date(now.getTime() - 4 * 24 * 60 * 60 * 1000).toISOString()
|
|
||||||
},
|
|
||||||
createdAt: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
createdBy: 'system',
|
|
||||||
updatedAt: new Date(now.getTime() - 4 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
tenantId: 'default-tenant'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'dsr-003',
|
|
||||||
referenceNumber: 'DSR-2025-000003',
|
|
||||||
type: 'rectification',
|
|
||||||
status: 'processing',
|
|
||||||
priority: 'normal',
|
|
||||||
requester: {
|
|
||||||
name: 'Peter Meier',
|
|
||||||
email: 'peter.meier@example.de'
|
|
||||||
},
|
|
||||||
source: 'email',
|
|
||||||
requestText: 'Meine Adresse ist falsch gespeichert.',
|
|
||||||
receivedAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
deadline: {
|
|
||||||
originalDeadline: new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
currentDeadline: new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
extended: false
|
|
||||||
},
|
|
||||||
identityVerification: {
|
|
||||||
verified: true,
|
|
||||||
method: 'existing_account',
|
|
||||||
verifiedAt: new Date(now.getTime() - 6 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
verifiedBy: 'DSB Mueller'
|
|
||||||
},
|
|
||||||
assignment: {
|
|
||||||
assignedTo: 'DSB Mueller',
|
|
||||||
assignedAt: new Date(now.getTime() - 6 * 24 * 60 * 60 * 1000).toISOString()
|
|
||||||
},
|
|
||||||
rectificationDetails: {
|
|
||||||
fieldsToCorrect: [
|
|
||||||
{
|
|
||||||
field: 'Adresse',
|
|
||||||
currentValue: 'Musterstr. 1, 12345 Berlin',
|
|
||||||
requestedValue: 'Musterstr. 10, 12345 Berlin',
|
|
||||||
corrected: false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
createdAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
createdBy: 'system',
|
|
||||||
updatedAt: new Date(now.getTime() - 1 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
tenantId: 'default-tenant'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'dsr-004',
|
|
||||||
referenceNumber: 'DSR-2025-000004',
|
|
||||||
type: 'portability',
|
|
||||||
status: 'processing',
|
|
||||||
priority: 'normal',
|
|
||||||
requester: {
|
|
||||||
name: 'Lisa Weber',
|
|
||||||
email: 'lisa.weber@example.de'
|
|
||||||
},
|
|
||||||
source: 'web_form',
|
|
||||||
receivedAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
deadline: {
|
|
||||||
originalDeadline: new Date(now.getTime() + 20 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
currentDeadline: new Date(now.getTime() + 20 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
extended: false
|
|
||||||
},
|
|
||||||
identityVerification: {
|
|
||||||
verified: true,
|
|
||||||
method: 'id_document',
|
|
||||||
verifiedAt: new Date(now.getTime() - 8 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
verifiedBy: 'DSB Mueller'
|
|
||||||
},
|
|
||||||
assignment: {
|
|
||||||
assignedTo: 'IT Team',
|
|
||||||
assignedAt: new Date(now.getTime() - 8 * 24 * 60 * 60 * 1000).toISOString()
|
|
||||||
},
|
|
||||||
notes: 'JSON-Export wird vorbereitet',
|
|
||||||
createdAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
createdBy: 'system',
|
|
||||||
updatedAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
tenantId: 'default-tenant'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'dsr-005',
|
|
||||||
referenceNumber: 'DSR-2025-000005',
|
|
||||||
type: 'objection',
|
|
||||||
status: 'rejected',
|
|
||||||
priority: 'low',
|
|
||||||
requester: {
|
|
||||||
name: 'Thomas Klein',
|
|
||||||
email: 'thomas.klein@example.de'
|
|
||||||
},
|
|
||||||
source: 'letter',
|
|
||||||
requestText: 'Ich widerspreche der Verarbeitung meiner Daten fuer Marketingzwecke.',
|
|
||||||
receivedAt: new Date(now.getTime() - 35 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
deadline: {
|
|
||||||
originalDeadline: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
currentDeadline: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
extended: false
|
|
||||||
},
|
|
||||||
completedAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
identityVerification: {
|
|
||||||
verified: true,
|
|
||||||
method: 'postal',
|
|
||||||
verifiedAt: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
verifiedBy: 'DSB Mueller'
|
|
||||||
},
|
|
||||||
assignment: {
|
|
||||||
assignedTo: 'Rechtsabteilung',
|
|
||||||
assignedAt: new Date(now.getTime() - 28 * 24 * 60 * 60 * 1000).toISOString()
|
|
||||||
},
|
|
||||||
objectionDetails: {
|
|
||||||
processingPurpose: 'Marketing',
|
|
||||||
legalBasis: 'Berechtigtes Interesse (Art. 6(1)(f))',
|
|
||||||
objectionGrounds: 'Keine konkreten Gruende genannt',
|
|
||||||
decision: 'rejected',
|
|
||||||
decisionReason: 'Zwingende schutzwuerdige Gruende fuer die Verarbeitung ueberwiegen',
|
|
||||||
decisionBy: 'Rechtsabteilung',
|
|
||||||
decisionAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString()
|
|
||||||
},
|
|
||||||
notes: 'Widerspruch unberechtigt - zwingende schutzwuerdige Gruende',
|
|
||||||
createdAt: new Date(now.getTime() - 35 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
createdBy: 'system',
|
|
||||||
updatedAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
tenantId: 'default-tenant'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'dsr-006',
|
|
||||||
referenceNumber: 'DSR-2025-000006',
|
|
||||||
type: 'access',
|
|
||||||
status: 'completed',
|
|
||||||
priority: 'normal',
|
|
||||||
requester: {
|
|
||||||
name: 'Sarah Braun',
|
|
||||||
email: 'sarah.braun@example.de'
|
|
||||||
},
|
|
||||||
source: 'email',
|
|
||||||
receivedAt: new Date(now.getTime() - 45 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
deadline: {
|
|
||||||
originalDeadline: new Date(now.getTime() - 15 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
currentDeadline: new Date(now.getTime() - 15 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
extended: false
|
|
||||||
},
|
|
||||||
completedAt: new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
identityVerification: {
|
|
||||||
verified: true,
|
|
||||||
method: 'id_document',
|
|
||||||
verifiedAt: new Date(now.getTime() - 42 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
verifiedBy: 'DSB Mueller'
|
|
||||||
},
|
|
||||||
assignment: {
|
|
||||||
assignedTo: 'DSB Mueller',
|
|
||||||
assignedAt: new Date(now.getTime() - 42 * 24 * 60 * 60 * 1000).toISOString()
|
|
||||||
},
|
|
||||||
dataExport: {
|
|
||||||
format: 'pdf',
|
|
||||||
generatedAt: new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
generatedBy: 'DSB Mueller',
|
|
||||||
fileName: 'datenauskunft_sarah_braun.pdf',
|
|
||||||
fileSize: 245000,
|
|
||||||
includesThirdPartyData: false
|
|
||||||
},
|
|
||||||
createdAt: new Date(now.getTime() - 45 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
createdBy: 'system',
|
|
||||||
updatedAt: new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
tenantId: 'default-tenant'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createMockStatistics(): DSRStatistics {
|
|
||||||
return {
|
|
||||||
total: 6,
|
|
||||||
byStatus: {
|
|
||||||
intake: 1,
|
|
||||||
identity_verification: 1,
|
|
||||||
processing: 2,
|
|
||||||
completed: 1,
|
|
||||||
rejected: 1,
|
|
||||||
cancelled: 0
|
|
||||||
},
|
|
||||||
byType: {
|
|
||||||
access: 2,
|
|
||||||
rectification: 1,
|
|
||||||
erasure: 1,
|
|
||||||
restriction: 0,
|
|
||||||
portability: 1,
|
|
||||||
objection: 1
|
|
||||||
},
|
|
||||||
overdue: 0,
|
|
||||||
dueThisWeek: 2,
|
|
||||||
averageProcessingDays: 18,
|
|
||||||
completedThisMonth: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,669 +1,12 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
/**
|
|
||||||
* Einwilligungen Context & Reducer
|
|
||||||
*
|
|
||||||
* Zentrale State-Verwaltung fuer das Datenpunktkatalog & DSI-Generator Modul.
|
|
||||||
* Verwendet React Context + useReducer fuer vorhersehbare State-Updates.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
createContext,
|
|
||||||
useContext,
|
|
||||||
useReducer,
|
|
||||||
useCallback,
|
|
||||||
useMemo,
|
|
||||||
ReactNode,
|
|
||||||
Dispatch,
|
|
||||||
} from 'react'
|
|
||||||
import {
|
|
||||||
EinwilligungenState,
|
|
||||||
EinwilligungenAction,
|
|
||||||
EinwilligungenTab,
|
|
||||||
DataPoint,
|
|
||||||
DataPointCatalog,
|
|
||||||
GeneratedPrivacyPolicy,
|
|
||||||
CookieBannerConfig,
|
|
||||||
CompanyInfo,
|
|
||||||
ConsentStatistics,
|
|
||||||
PrivacyPolicySection,
|
|
||||||
SupportedLanguage,
|
|
||||||
ExportFormat,
|
|
||||||
DataPointCategory,
|
|
||||||
LegalBasis,
|
|
||||||
RiskLevel,
|
|
||||||
} from './types'
|
|
||||||
import {
|
|
||||||
PREDEFINED_DATA_POINTS,
|
|
||||||
RETENTION_MATRIX,
|
|
||||||
DEFAULT_COOKIE_CATEGORIES,
|
|
||||||
createDefaultCatalog,
|
|
||||||
getDataPointById,
|
|
||||||
getDataPointsByCategory,
|
|
||||||
countDataPointsByCategory,
|
|
||||||
countDataPointsByRiskLevel,
|
|
||||||
} from './catalog/loader'
|
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// INITIAL STATE
|
// Einwilligungen Context — Barrel re-exports
|
||||||
|
// Preserves the original public API so existing imports work unchanged.
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
const initialState: EinwilligungenState = {
|
export { EinwilligungenProvider } from './provider'
|
||||||
// Data
|
export { EinwilligungenContext } from './provider'
|
||||||
catalog: null,
|
export type { EinwilligungenContextValue } from './provider'
|
||||||
selectedDataPoints: [],
|
export { useEinwilligungen } from './hooks'
|
||||||
privacyPolicy: null,
|
export { initialState, einwilligungenReducer } from './reducer'
|
||||||
cookieBannerConfig: null,
|
|
||||||
companyInfo: null,
|
|
||||||
consentStatistics: null,
|
|
||||||
|
|
||||||
// UI State
|
|
||||||
activeTab: 'catalog',
|
|
||||||
isLoading: false,
|
|
||||||
isSaving: false,
|
|
||||||
error: null,
|
|
||||||
|
|
||||||
// Editor State
|
|
||||||
editingDataPoint: null,
|
|
||||||
editingSection: null,
|
|
||||||
|
|
||||||
// Preview
|
|
||||||
previewLanguage: 'de',
|
|
||||||
previewFormat: 'HTML',
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// REDUCER
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
function einwilligungenReducer(
|
|
||||||
state: EinwilligungenState,
|
|
||||||
action: EinwilligungenAction
|
|
||||||
): EinwilligungenState {
|
|
||||||
switch (action.type) {
|
|
||||||
case 'SET_CATALOG':
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
catalog: action.payload,
|
|
||||||
// Automatisch alle aktiven Datenpunkte auswaehlen
|
|
||||||
selectedDataPoints: [
|
|
||||||
...action.payload.dataPoints.filter((dp) => dp.isActive !== false).map((dp) => dp.id),
|
|
||||||
...action.payload.customDataPoints.filter((dp) => dp.isActive !== false).map((dp) => dp.id),
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'SET_SELECTED_DATA_POINTS':
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
selectedDataPoints: action.payload,
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'TOGGLE_DATA_POINT': {
|
|
||||||
const id = action.payload
|
|
||||||
const isSelected = state.selectedDataPoints.includes(id)
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
selectedDataPoints: isSelected
|
|
||||||
? state.selectedDataPoints.filter((dpId) => dpId !== id)
|
|
||||||
: [...state.selectedDataPoints, id],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'ADD_CUSTOM_DATA_POINT':
|
|
||||||
if (!state.catalog) return state
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
catalog: {
|
|
||||||
...state.catalog,
|
|
||||||
customDataPoints: [...state.catalog.customDataPoints, action.payload],
|
|
||||||
updatedAt: new Date(),
|
|
||||||
},
|
|
||||||
selectedDataPoints: [...state.selectedDataPoints, action.payload.id],
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'UPDATE_DATA_POINT': {
|
|
||||||
if (!state.catalog) return state
|
|
||||||
const { id, data } = action.payload
|
|
||||||
|
|
||||||
// Pruefe ob es ein vordefinierter oder kundenspezifischer Datenpunkt ist
|
|
||||||
const isCustom = state.catalog.customDataPoints.some((dp) => dp.id === id)
|
|
||||||
|
|
||||||
if (isCustom) {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
catalog: {
|
|
||||||
...state.catalog,
|
|
||||||
customDataPoints: state.catalog.customDataPoints.map((dp) =>
|
|
||||||
dp.id === id ? { ...dp, ...data } : dp
|
|
||||||
),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Vordefinierte Datenpunkte: nur isActive aendern
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
catalog: {
|
|
||||||
...state.catalog,
|
|
||||||
dataPoints: state.catalog.dataPoints.map((dp) =>
|
|
||||||
dp.id === id ? { ...dp, ...data } : dp
|
|
||||||
),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'DELETE_CUSTOM_DATA_POINT':
|
|
||||||
if (!state.catalog) return state
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
catalog: {
|
|
||||||
...state.catalog,
|
|
||||||
customDataPoints: state.catalog.customDataPoints.filter((dp) => dp.id !== action.payload),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
},
|
|
||||||
selectedDataPoints: state.selectedDataPoints.filter((id) => id !== action.payload),
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'SET_PRIVACY_POLICY':
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
privacyPolicy: action.payload,
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'SET_COOKIE_BANNER_CONFIG':
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
cookieBannerConfig: action.payload,
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'UPDATE_COOKIE_BANNER_STYLING':
|
|
||||||
if (!state.cookieBannerConfig) return state
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
cookieBannerConfig: {
|
|
||||||
...state.cookieBannerConfig,
|
|
||||||
styling: {
|
|
||||||
...state.cookieBannerConfig.styling,
|
|
||||||
...action.payload,
|
|
||||||
},
|
|
||||||
updatedAt: new Date(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'UPDATE_COOKIE_BANNER_TEXTS':
|
|
||||||
if (!state.cookieBannerConfig) return state
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
cookieBannerConfig: {
|
|
||||||
...state.cookieBannerConfig,
|
|
||||||
texts: {
|
|
||||||
...state.cookieBannerConfig.texts,
|
|
||||||
...action.payload,
|
|
||||||
},
|
|
||||||
updatedAt: new Date(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'SET_COMPANY_INFO':
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
companyInfo: action.payload,
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'SET_CONSENT_STATISTICS':
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
consentStatistics: action.payload,
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'SET_ACTIVE_TAB':
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
activeTab: action.payload,
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'SET_LOADING':
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
isLoading: action.payload,
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'SET_SAVING':
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
isSaving: action.payload,
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'SET_ERROR':
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
error: action.payload,
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'SET_EDITING_DATA_POINT':
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
editingDataPoint: action.payload,
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'SET_EDITING_SECTION':
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
editingSection: action.payload,
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'SET_PREVIEW_LANGUAGE':
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
previewLanguage: action.payload,
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'SET_PREVIEW_FORMAT':
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
previewFormat: action.payload,
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'RESET_STATE':
|
|
||||||
return initialState
|
|
||||||
|
|
||||||
default:
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// CONTEXT
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
interface EinwilligungenContextValue {
|
|
||||||
state: EinwilligungenState
|
|
||||||
dispatch: Dispatch<EinwilligungenAction>
|
|
||||||
|
|
||||||
// Computed Values
|
|
||||||
allDataPoints: DataPoint[]
|
|
||||||
selectedDataPointsData: DataPoint[]
|
|
||||||
dataPointsByCategory: Record<DataPointCategory, DataPoint[]>
|
|
||||||
categoryStats: Record<DataPointCategory, number>
|
|
||||||
riskStats: Record<RiskLevel, number>
|
|
||||||
legalBasisStats: Record<LegalBasis, number>
|
|
||||||
|
|
||||||
// Actions
|
|
||||||
initializeCatalog: (tenantId: string) => void
|
|
||||||
loadCatalog: (tenantId: string) => Promise<void>
|
|
||||||
saveCatalog: () => Promise<void>
|
|
||||||
toggleDataPoint: (id: string) => void
|
|
||||||
addCustomDataPoint: (dataPoint: DataPoint) => void
|
|
||||||
updateDataPoint: (id: string, data: Partial<DataPoint>) => void
|
|
||||||
deleteCustomDataPoint: (id: string) => void
|
|
||||||
setActiveTab: (tab: EinwilligungenTab) => void
|
|
||||||
setPreviewLanguage: (language: SupportedLanguage) => void
|
|
||||||
setPreviewFormat: (format: ExportFormat) => void
|
|
||||||
setCompanyInfo: (info: CompanyInfo) => void
|
|
||||||
generatePrivacyPolicy: () => Promise<void>
|
|
||||||
generateCookieBannerConfig: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const EinwilligungenContext = createContext<EinwilligungenContextValue | null>(null)
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// PROVIDER
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
interface EinwilligungenProviderProps {
|
|
||||||
children: ReactNode
|
|
||||||
tenantId?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function EinwilligungenProvider({ children, tenantId }: EinwilligungenProviderProps) {
|
|
||||||
const [state, dispatch] = useReducer(einwilligungenReducer, initialState)
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// COMPUTED VALUES
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
const allDataPoints = useMemo(() => {
|
|
||||||
if (!state.catalog) return PREDEFINED_DATA_POINTS
|
|
||||||
return [...state.catalog.dataPoints, ...state.catalog.customDataPoints]
|
|
||||||
}, [state.catalog])
|
|
||||||
|
|
||||||
const selectedDataPointsData = useMemo(() => {
|
|
||||||
return allDataPoints.filter((dp) => state.selectedDataPoints.includes(dp.id))
|
|
||||||
}, [allDataPoints, state.selectedDataPoints])
|
|
||||||
|
|
||||||
const dataPointsByCategory = useMemo(() => {
|
|
||||||
const result: Partial<Record<DataPointCategory, DataPoint[]>> = {}
|
|
||||||
// 18 Kategorien (A-R)
|
|
||||||
const categories: DataPointCategory[] = [
|
|
||||||
'MASTER_DATA', // A
|
|
||||||
'CONTACT_DATA', // B
|
|
||||||
'AUTHENTICATION', // C
|
|
||||||
'CONSENT', // D
|
|
||||||
'COMMUNICATION', // E
|
|
||||||
'PAYMENT', // F
|
|
||||||
'USAGE_DATA', // G
|
|
||||||
'LOCATION', // H
|
|
||||||
'DEVICE_DATA', // I
|
|
||||||
'MARKETING', // J
|
|
||||||
'ANALYTICS', // K
|
|
||||||
'SOCIAL_MEDIA', // L
|
|
||||||
'HEALTH_DATA', // M - Art. 9 DSGVO
|
|
||||||
'EMPLOYEE_DATA', // N - BDSG § 26
|
|
||||||
'CONTRACT_DATA', // O
|
|
||||||
'LOG_DATA', // P
|
|
||||||
'AI_DATA', // Q - AI Act
|
|
||||||
'SECURITY', // R
|
|
||||||
]
|
|
||||||
for (const cat of categories) {
|
|
||||||
result[cat] = selectedDataPointsData.filter((dp) => dp.category === cat)
|
|
||||||
}
|
|
||||||
return result as Record<DataPointCategory, DataPoint[]>
|
|
||||||
}, [selectedDataPointsData])
|
|
||||||
|
|
||||||
const categoryStats = useMemo(() => {
|
|
||||||
const counts: Partial<Record<DataPointCategory, number>> = {}
|
|
||||||
for (const dp of selectedDataPointsData) {
|
|
||||||
counts[dp.category] = (counts[dp.category] || 0) + 1
|
|
||||||
}
|
|
||||||
return counts as Record<DataPointCategory, number>
|
|
||||||
}, [selectedDataPointsData])
|
|
||||||
|
|
||||||
const riskStats = useMemo(() => {
|
|
||||||
const counts: Record<RiskLevel, number> = { LOW: 0, MEDIUM: 0, HIGH: 0 }
|
|
||||||
for (const dp of selectedDataPointsData) {
|
|
||||||
counts[dp.riskLevel]++
|
|
||||||
}
|
|
||||||
return counts
|
|
||||||
}, [selectedDataPointsData])
|
|
||||||
|
|
||||||
const legalBasisStats = useMemo(() => {
|
|
||||||
// Alle 7 Rechtsgrundlagen
|
|
||||||
const counts: Record<LegalBasis, number> = {
|
|
||||||
CONTRACT: 0,
|
|
||||||
CONSENT: 0,
|
|
||||||
EXPLICIT_CONSENT: 0,
|
|
||||||
LEGITIMATE_INTEREST: 0,
|
|
||||||
LEGAL_OBLIGATION: 0,
|
|
||||||
VITAL_INTERESTS: 0,
|
|
||||||
PUBLIC_INTEREST: 0,
|
|
||||||
}
|
|
||||||
for (const dp of selectedDataPointsData) {
|
|
||||||
counts[dp.legalBasis]++
|
|
||||||
}
|
|
||||||
return counts
|
|
||||||
}, [selectedDataPointsData])
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ACTIONS
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
const initializeCatalog = useCallback(
|
|
||||||
(tid: string) => {
|
|
||||||
const catalog = createDefaultCatalog(tid)
|
|
||||||
dispatch({ type: 'SET_CATALOG', payload: catalog })
|
|
||||||
},
|
|
||||||
[dispatch]
|
|
||||||
)
|
|
||||||
|
|
||||||
const loadCatalog = useCallback(
|
|
||||||
async (tid: string) => {
|
|
||||||
dispatch({ type: 'SET_LOADING', payload: true })
|
|
||||||
dispatch({ type: 'SET_ERROR', payload: null })
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`/api/sdk/v1/einwilligungen/catalog`, {
|
|
||||||
headers: {
|
|
||||||
'X-Tenant-ID': tid,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json()
|
|
||||||
dispatch({ type: 'SET_CATALOG', payload: data.catalog })
|
|
||||||
if (data.companyInfo) {
|
|
||||||
dispatch({ type: 'SET_COMPANY_INFO', payload: data.companyInfo })
|
|
||||||
}
|
|
||||||
if (data.cookieBannerConfig) {
|
|
||||||
dispatch({ type: 'SET_COOKIE_BANNER_CONFIG', payload: data.cookieBannerConfig })
|
|
||||||
}
|
|
||||||
} else if (response.status === 404) {
|
|
||||||
// Katalog existiert noch nicht - erstelle Default
|
|
||||||
initializeCatalog(tid)
|
|
||||||
} else {
|
|
||||||
throw new Error('Failed to load catalog')
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading catalog:', error)
|
|
||||||
dispatch({ type: 'SET_ERROR', payload: 'Fehler beim Laden des Katalogs' })
|
|
||||||
// Fallback zu Default
|
|
||||||
initializeCatalog(tid)
|
|
||||||
} finally {
|
|
||||||
dispatch({ type: 'SET_LOADING', payload: false })
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[dispatch, initializeCatalog]
|
|
||||||
)
|
|
||||||
|
|
||||||
const saveCatalog = useCallback(async () => {
|
|
||||||
if (!state.catalog) return
|
|
||||||
|
|
||||||
dispatch({ type: 'SET_SAVING', payload: true })
|
|
||||||
dispatch({ type: 'SET_ERROR', payload: null })
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`/api/sdk/v1/einwilligungen/catalog`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-Tenant-ID': state.catalog.tenantId,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
catalog: state.catalog,
|
|
||||||
companyInfo: state.companyInfo,
|
|
||||||
cookieBannerConfig: state.cookieBannerConfig,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Failed to save catalog')
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error saving catalog:', error)
|
|
||||||
dispatch({ type: 'SET_ERROR', payload: 'Fehler beim Speichern des Katalogs' })
|
|
||||||
} finally {
|
|
||||||
dispatch({ type: 'SET_SAVING', payload: false })
|
|
||||||
}
|
|
||||||
}, [state.catalog, state.companyInfo, state.cookieBannerConfig, dispatch])
|
|
||||||
|
|
||||||
const toggleDataPoint = useCallback(
|
|
||||||
(id: string) => {
|
|
||||||
dispatch({ type: 'TOGGLE_DATA_POINT', payload: id })
|
|
||||||
},
|
|
||||||
[dispatch]
|
|
||||||
)
|
|
||||||
|
|
||||||
const addCustomDataPoint = useCallback(
|
|
||||||
(dataPoint: DataPoint) => {
|
|
||||||
dispatch({ type: 'ADD_CUSTOM_DATA_POINT', payload: { ...dataPoint, isCustom: true } })
|
|
||||||
},
|
|
||||||
[dispatch]
|
|
||||||
)
|
|
||||||
|
|
||||||
const updateDataPoint = useCallback(
|
|
||||||
(id: string, data: Partial<DataPoint>) => {
|
|
||||||
dispatch({ type: 'UPDATE_DATA_POINT', payload: { id, data } })
|
|
||||||
},
|
|
||||||
[dispatch]
|
|
||||||
)
|
|
||||||
|
|
||||||
const deleteCustomDataPoint = useCallback(
|
|
||||||
(id: string) => {
|
|
||||||
dispatch({ type: 'DELETE_CUSTOM_DATA_POINT', payload: id })
|
|
||||||
},
|
|
||||||
[dispatch]
|
|
||||||
)
|
|
||||||
|
|
||||||
const setActiveTab = useCallback(
|
|
||||||
(tab: EinwilligungenTab) => {
|
|
||||||
dispatch({ type: 'SET_ACTIVE_TAB', payload: tab })
|
|
||||||
},
|
|
||||||
[dispatch]
|
|
||||||
)
|
|
||||||
|
|
||||||
const setPreviewLanguage = useCallback(
|
|
||||||
(language: SupportedLanguage) => {
|
|
||||||
dispatch({ type: 'SET_PREVIEW_LANGUAGE', payload: language })
|
|
||||||
},
|
|
||||||
[dispatch]
|
|
||||||
)
|
|
||||||
|
|
||||||
const setPreviewFormat = useCallback(
|
|
||||||
(format: ExportFormat) => {
|
|
||||||
dispatch({ type: 'SET_PREVIEW_FORMAT', payload: format })
|
|
||||||
},
|
|
||||||
[dispatch]
|
|
||||||
)
|
|
||||||
|
|
||||||
const setCompanyInfo = useCallback(
|
|
||||||
(info: CompanyInfo) => {
|
|
||||||
dispatch({ type: 'SET_COMPANY_INFO', payload: info })
|
|
||||||
},
|
|
||||||
[dispatch]
|
|
||||||
)
|
|
||||||
|
|
||||||
const generatePrivacyPolicy = useCallback(async () => {
|
|
||||||
if (!state.catalog || !state.companyInfo) {
|
|
||||||
dispatch({ type: 'SET_ERROR', payload: 'Bitte zuerst Firmendaten eingeben' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch({ type: 'SET_LOADING', payload: true })
|
|
||||||
dispatch({ type: 'SET_ERROR', payload: null })
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`/api/sdk/v1/einwilligungen/privacy-policy/generate`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-Tenant-ID': state.catalog.tenantId,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
dataPointIds: state.selectedDataPoints,
|
|
||||||
companyInfo: state.companyInfo,
|
|
||||||
language: state.previewLanguage,
|
|
||||||
format: state.previewFormat,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const policy = await response.json()
|
|
||||||
dispatch({ type: 'SET_PRIVACY_POLICY', payload: policy })
|
|
||||||
} else {
|
|
||||||
throw new Error('Failed to generate privacy policy')
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error generating privacy policy:', error)
|
|
||||||
dispatch({ type: 'SET_ERROR', payload: 'Fehler bei der Generierung der Datenschutzerklaerung' })
|
|
||||||
} finally {
|
|
||||||
dispatch({ type: 'SET_LOADING', payload: false })
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
state.catalog,
|
|
||||||
state.companyInfo,
|
|
||||||
state.selectedDataPoints,
|
|
||||||
state.previewLanguage,
|
|
||||||
state.previewFormat,
|
|
||||||
dispatch,
|
|
||||||
])
|
|
||||||
|
|
||||||
const generateCookieBannerConfig = useCallback(() => {
|
|
||||||
if (!state.catalog) return
|
|
||||||
|
|
||||||
const config: CookieBannerConfig = {
|
|
||||||
id: `cookie-banner-${state.catalog.tenantId}`,
|
|
||||||
tenantId: state.catalog.tenantId,
|
|
||||||
categories: DEFAULT_COOKIE_CATEGORIES.map((cat) => ({
|
|
||||||
...cat,
|
|
||||||
// Filtere nur die ausgewaehlten Datenpunkte
|
|
||||||
dataPointIds: cat.dataPointIds.filter((id) => state.selectedDataPoints.includes(id)),
|
|
||||||
})),
|
|
||||||
styling: {
|
|
||||||
position: 'BOTTOM',
|
|
||||||
theme: 'LIGHT',
|
|
||||||
primaryColor: '#6366f1',
|
|
||||||
borderRadius: 12,
|
|
||||||
},
|
|
||||||
texts: {
|
|
||||||
title: { de: 'Cookie-Einstellungen', en: 'Cookie Settings' },
|
|
||||||
description: {
|
|
||||||
de: 'Wir verwenden Cookies, um Ihnen die bestmoegliche Nutzung unserer Website zu ermoeglichen.',
|
|
||||||
en: 'We use cookies to provide you with the best possible experience on our website.',
|
|
||||||
},
|
|
||||||
acceptAll: { de: 'Alle akzeptieren', en: 'Accept All' },
|
|
||||||
rejectAll: { de: 'Alle ablehnen', en: 'Reject All' },
|
|
||||||
customize: { de: 'Anpassen', en: 'Customize' },
|
|
||||||
save: { de: 'Auswahl speichern', en: 'Save Selection' },
|
|
||||||
privacyPolicyLink: { de: 'Datenschutzerklaerung', en: 'Privacy Policy' },
|
|
||||||
},
|
|
||||||
updatedAt: new Date(),
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch({ type: 'SET_COOKIE_BANNER_CONFIG', payload: config })
|
|
||||||
}, [state.catalog, state.selectedDataPoints, dispatch])
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// CONTEXT VALUE
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
const value: EinwilligungenContextValue = {
|
|
||||||
state,
|
|
||||||
dispatch,
|
|
||||||
|
|
||||||
// Computed Values
|
|
||||||
allDataPoints,
|
|
||||||
selectedDataPointsData,
|
|
||||||
dataPointsByCategory,
|
|
||||||
categoryStats,
|
|
||||||
riskStats,
|
|
||||||
legalBasisStats,
|
|
||||||
|
|
||||||
// Actions
|
|
||||||
initializeCatalog,
|
|
||||||
loadCatalog,
|
|
||||||
saveCatalog,
|
|
||||||
toggleDataPoint,
|
|
||||||
addCustomDataPoint,
|
|
||||||
updateDataPoint,
|
|
||||||
deleteCustomDataPoint,
|
|
||||||
setActiveTab,
|
|
||||||
setPreviewLanguage,
|
|
||||||
setPreviewFormat,
|
|
||||||
setCompanyInfo,
|
|
||||||
generatePrivacyPolicy,
|
|
||||||
generateCookieBannerConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EinwilligungenContext.Provider value={value}>{children}</EinwilligungenContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// HOOK
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
export function useEinwilligungen(): EinwilligungenContextValue {
|
|
||||||
const context = useContext(EinwilligungenContext)
|
|
||||||
if (!context) {
|
|
||||||
throw new Error('useEinwilligungen must be used within EinwilligungenProvider')
|
|
||||||
}
|
|
||||||
return context
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// EXPORTS
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
export { initialState, einwilligungenReducer }
|
|
||||||
|
|||||||
18
admin-compliance/lib/sdk/einwilligungen/hooks.tsx
Normal file
18
admin-compliance/lib/sdk/einwilligungen/hooks.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Einwilligungen Hook
|
||||||
|
// Custom hook for consuming the Einwilligungen context
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
import { useContext } from 'react'
|
||||||
|
import { EinwilligungenContext } from './provider'
|
||||||
|
import type { EinwilligungenContextValue } from './provider'
|
||||||
|
|
||||||
|
export function useEinwilligungen(): EinwilligungenContextValue {
|
||||||
|
const context = useContext(EinwilligungenContext)
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useEinwilligungen must be used within EinwilligungenProvider')
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
}
|
||||||
384
admin-compliance/lib/sdk/einwilligungen/provider.tsx
Normal file
384
admin-compliance/lib/sdk/einwilligungen/provider.tsx
Normal file
@@ -0,0 +1,384 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Einwilligungen Provider
|
||||||
|
*
|
||||||
|
* React Context Provider fuer das Einwilligungen-Modul.
|
||||||
|
* Stellt State, computed values und Actions bereit.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
useReducer,
|
||||||
|
useCallback,
|
||||||
|
useMemo,
|
||||||
|
ReactNode,
|
||||||
|
Dispatch,
|
||||||
|
} from 'react'
|
||||||
|
import {
|
||||||
|
EinwilligungenState,
|
||||||
|
EinwilligungenAction,
|
||||||
|
EinwilligungenTab,
|
||||||
|
DataPoint,
|
||||||
|
CookieBannerConfig,
|
||||||
|
CompanyInfo,
|
||||||
|
SupportedLanguage,
|
||||||
|
ExportFormat,
|
||||||
|
DataPointCategory,
|
||||||
|
LegalBasis,
|
||||||
|
RiskLevel,
|
||||||
|
} from './types'
|
||||||
|
import {
|
||||||
|
PREDEFINED_DATA_POINTS,
|
||||||
|
DEFAULT_COOKIE_CATEGORIES,
|
||||||
|
createDefaultCatalog,
|
||||||
|
} from './catalog/loader'
|
||||||
|
import { einwilligungenReducer, initialState } from './reducer'
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// CONTEXT
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export interface EinwilligungenContextValue {
|
||||||
|
state: EinwilligungenState
|
||||||
|
dispatch: Dispatch<EinwilligungenAction>
|
||||||
|
|
||||||
|
// Computed Values
|
||||||
|
allDataPoints: DataPoint[]
|
||||||
|
selectedDataPointsData: DataPoint[]
|
||||||
|
dataPointsByCategory: Record<DataPointCategory, DataPoint[]>
|
||||||
|
categoryStats: Record<DataPointCategory, number>
|
||||||
|
riskStats: Record<RiskLevel, number>
|
||||||
|
legalBasisStats: Record<LegalBasis, number>
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
initializeCatalog: (tenantId: string) => void
|
||||||
|
loadCatalog: (tenantId: string) => Promise<void>
|
||||||
|
saveCatalog: () => Promise<void>
|
||||||
|
toggleDataPoint: (id: string) => void
|
||||||
|
addCustomDataPoint: (dataPoint: DataPoint) => void
|
||||||
|
updateDataPoint: (id: string, data: Partial<DataPoint>) => void
|
||||||
|
deleteCustomDataPoint: (id: string) => void
|
||||||
|
setActiveTab: (tab: EinwilligungenTab) => void
|
||||||
|
setPreviewLanguage: (language: SupportedLanguage) => void
|
||||||
|
setPreviewFormat: (format: ExportFormat) => void
|
||||||
|
setCompanyInfo: (info: CompanyInfo) => void
|
||||||
|
generatePrivacyPolicy: () => Promise<void>
|
||||||
|
generateCookieBannerConfig: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EinwilligungenContext = createContext<EinwilligungenContextValue | null>(null)
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// PROVIDER
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
interface EinwilligungenProviderProps {
|
||||||
|
children: ReactNode
|
||||||
|
tenantId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EinwilligungenProvider({ children, tenantId }: EinwilligungenProviderProps) {
|
||||||
|
const [state, dispatch] = useReducer(einwilligungenReducer, initialState)
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// COMPUTED VALUES
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const allDataPoints = useMemo(() => {
|
||||||
|
if (!state.catalog) return PREDEFINED_DATA_POINTS
|
||||||
|
return [...state.catalog.dataPoints, ...state.catalog.customDataPoints]
|
||||||
|
}, [state.catalog])
|
||||||
|
|
||||||
|
const selectedDataPointsData = useMemo(() => {
|
||||||
|
return allDataPoints.filter((dp) => state.selectedDataPoints.includes(dp.id))
|
||||||
|
}, [allDataPoints, state.selectedDataPoints])
|
||||||
|
|
||||||
|
const dataPointsByCategory = useMemo(() => {
|
||||||
|
const result: Partial<Record<DataPointCategory, DataPoint[]>> = {}
|
||||||
|
const categories: DataPointCategory[] = [
|
||||||
|
'MASTER_DATA', 'CONTACT_DATA', 'AUTHENTICATION', 'CONSENT',
|
||||||
|
'COMMUNICATION', 'PAYMENT', 'USAGE_DATA', 'LOCATION',
|
||||||
|
'DEVICE_DATA', 'MARKETING', 'ANALYTICS', 'SOCIAL_MEDIA',
|
||||||
|
'HEALTH_DATA', 'EMPLOYEE_DATA', 'CONTRACT_DATA', 'LOG_DATA',
|
||||||
|
'AI_DATA', 'SECURITY',
|
||||||
|
]
|
||||||
|
for (const cat of categories) {
|
||||||
|
result[cat] = selectedDataPointsData.filter((dp) => dp.category === cat)
|
||||||
|
}
|
||||||
|
return result as Record<DataPointCategory, DataPoint[]>
|
||||||
|
}, [selectedDataPointsData])
|
||||||
|
|
||||||
|
const categoryStats = useMemo(() => {
|
||||||
|
const counts: Partial<Record<DataPointCategory, number>> = {}
|
||||||
|
for (const dp of selectedDataPointsData) {
|
||||||
|
counts[dp.category] = (counts[dp.category] || 0) + 1
|
||||||
|
}
|
||||||
|
return counts as Record<DataPointCategory, number>
|
||||||
|
}, [selectedDataPointsData])
|
||||||
|
|
||||||
|
const riskStats = useMemo(() => {
|
||||||
|
const counts: Record<RiskLevel, number> = { LOW: 0, MEDIUM: 0, HIGH: 0 }
|
||||||
|
for (const dp of selectedDataPointsData) {
|
||||||
|
counts[dp.riskLevel]++
|
||||||
|
}
|
||||||
|
return counts
|
||||||
|
}, [selectedDataPointsData])
|
||||||
|
|
||||||
|
const legalBasisStats = useMemo(() => {
|
||||||
|
const counts: Record<LegalBasis, number> = {
|
||||||
|
CONTRACT: 0, CONSENT: 0, EXPLICIT_CONSENT: 0,
|
||||||
|
LEGITIMATE_INTEREST: 0, LEGAL_OBLIGATION: 0,
|
||||||
|
VITAL_INTERESTS: 0, PUBLIC_INTEREST: 0,
|
||||||
|
}
|
||||||
|
for (const dp of selectedDataPointsData) {
|
||||||
|
counts[dp.legalBasis]++
|
||||||
|
}
|
||||||
|
return counts
|
||||||
|
}, [selectedDataPointsData])
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ACTIONS
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const initializeCatalog = useCallback(
|
||||||
|
(tid: string) => {
|
||||||
|
const catalog = createDefaultCatalog(tid)
|
||||||
|
dispatch({ type: 'SET_CATALOG', payload: catalog })
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
)
|
||||||
|
|
||||||
|
const loadCatalog = useCallback(
|
||||||
|
async (tid: string) => {
|
||||||
|
dispatch({ type: 'SET_LOADING', payload: true })
|
||||||
|
dispatch({ type: 'SET_ERROR', payload: null })
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/sdk/v1/einwilligungen/catalog`, {
|
||||||
|
headers: { 'X-Tenant-ID': tid },
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json()
|
||||||
|
dispatch({ type: 'SET_CATALOG', payload: data.catalog })
|
||||||
|
if (data.companyInfo) {
|
||||||
|
dispatch({ type: 'SET_COMPANY_INFO', payload: data.companyInfo })
|
||||||
|
}
|
||||||
|
if (data.cookieBannerConfig) {
|
||||||
|
dispatch({ type: 'SET_COOKIE_BANNER_CONFIG', payload: data.cookieBannerConfig })
|
||||||
|
}
|
||||||
|
} else if (response.status === 404) {
|
||||||
|
initializeCatalog(tid)
|
||||||
|
} else {
|
||||||
|
throw new Error('Failed to load catalog')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading catalog:', error)
|
||||||
|
dispatch({ type: 'SET_ERROR', payload: 'Fehler beim Laden des Katalogs' })
|
||||||
|
initializeCatalog(tid)
|
||||||
|
} finally {
|
||||||
|
dispatch({ type: 'SET_LOADING', payload: false })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[dispatch, initializeCatalog]
|
||||||
|
)
|
||||||
|
|
||||||
|
const saveCatalog = useCallback(async () => {
|
||||||
|
if (!state.catalog) return
|
||||||
|
|
||||||
|
dispatch({ type: 'SET_SAVING', payload: true })
|
||||||
|
dispatch({ type: 'SET_ERROR', payload: null })
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/sdk/v1/einwilligungen/catalog`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Tenant-ID': state.catalog.tenantId,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
catalog: state.catalog,
|
||||||
|
companyInfo: state.companyInfo,
|
||||||
|
cookieBannerConfig: state.cookieBannerConfig,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to save catalog')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving catalog:', error)
|
||||||
|
dispatch({ type: 'SET_ERROR', payload: 'Fehler beim Speichern des Katalogs' })
|
||||||
|
} finally {
|
||||||
|
dispatch({ type: 'SET_SAVING', payload: false })
|
||||||
|
}
|
||||||
|
}, [state.catalog, state.companyInfo, state.cookieBannerConfig, dispatch])
|
||||||
|
|
||||||
|
const toggleDataPoint = useCallback(
|
||||||
|
(id: string) => {
|
||||||
|
dispatch({ type: 'TOGGLE_DATA_POINT', payload: id })
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
)
|
||||||
|
|
||||||
|
const addCustomDataPoint = useCallback(
|
||||||
|
(dataPoint: DataPoint) => {
|
||||||
|
dispatch({ type: 'ADD_CUSTOM_DATA_POINT', payload: { ...dataPoint, isCustom: true } })
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
)
|
||||||
|
|
||||||
|
const updateDataPoint = useCallback(
|
||||||
|
(id: string, data: Partial<DataPoint>) => {
|
||||||
|
dispatch({ type: 'UPDATE_DATA_POINT', payload: { id, data } })
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
)
|
||||||
|
|
||||||
|
const deleteCustomDataPoint = useCallback(
|
||||||
|
(id: string) => {
|
||||||
|
dispatch({ type: 'DELETE_CUSTOM_DATA_POINT', payload: id })
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
)
|
||||||
|
|
||||||
|
const setActiveTab = useCallback(
|
||||||
|
(tab: EinwilligungenTab) => {
|
||||||
|
dispatch({ type: 'SET_ACTIVE_TAB', payload: tab })
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
)
|
||||||
|
|
||||||
|
const setPreviewLanguage = useCallback(
|
||||||
|
(language: SupportedLanguage) => {
|
||||||
|
dispatch({ type: 'SET_PREVIEW_LANGUAGE', payload: language })
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
)
|
||||||
|
|
||||||
|
const setPreviewFormat = useCallback(
|
||||||
|
(format: ExportFormat) => {
|
||||||
|
dispatch({ type: 'SET_PREVIEW_FORMAT', payload: format })
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
)
|
||||||
|
|
||||||
|
const setCompanyInfo = useCallback(
|
||||||
|
(info: CompanyInfo) => {
|
||||||
|
dispatch({ type: 'SET_COMPANY_INFO', payload: info })
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
)
|
||||||
|
|
||||||
|
const generatePrivacyPolicy = useCallback(async () => {
|
||||||
|
if (!state.catalog || !state.companyInfo) {
|
||||||
|
dispatch({ type: 'SET_ERROR', payload: 'Bitte zuerst Firmendaten eingeben' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch({ type: 'SET_LOADING', payload: true })
|
||||||
|
dispatch({ type: 'SET_ERROR', payload: null })
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/sdk/v1/einwilligungen/privacy-policy/generate`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Tenant-ID': state.catalog.tenantId,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
dataPointIds: state.selectedDataPoints,
|
||||||
|
companyInfo: state.companyInfo,
|
||||||
|
language: state.previewLanguage,
|
||||||
|
format: state.previewFormat,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const policy = await response.json()
|
||||||
|
dispatch({ type: 'SET_PRIVACY_POLICY', payload: policy })
|
||||||
|
} else {
|
||||||
|
throw new Error('Failed to generate privacy policy')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error generating privacy policy:', error)
|
||||||
|
dispatch({ type: 'SET_ERROR', payload: 'Fehler bei der Generierung der Datenschutzerklaerung' })
|
||||||
|
} finally {
|
||||||
|
dispatch({ type: 'SET_LOADING', payload: false })
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
state.catalog,
|
||||||
|
state.companyInfo,
|
||||||
|
state.selectedDataPoints,
|
||||||
|
state.previewLanguage,
|
||||||
|
state.previewFormat,
|
||||||
|
dispatch,
|
||||||
|
])
|
||||||
|
|
||||||
|
const generateCookieBannerConfig = useCallback(() => {
|
||||||
|
if (!state.catalog) return
|
||||||
|
|
||||||
|
const config: CookieBannerConfig = {
|
||||||
|
id: `cookie-banner-${state.catalog.tenantId}`,
|
||||||
|
tenantId: state.catalog.tenantId,
|
||||||
|
categories: DEFAULT_COOKIE_CATEGORIES.map((cat) => ({
|
||||||
|
...cat,
|
||||||
|
dataPointIds: cat.dataPointIds.filter((id) => state.selectedDataPoints.includes(id)),
|
||||||
|
})),
|
||||||
|
styling: {
|
||||||
|
position: 'BOTTOM',
|
||||||
|
theme: 'LIGHT',
|
||||||
|
primaryColor: '#6366f1',
|
||||||
|
borderRadius: 12,
|
||||||
|
},
|
||||||
|
texts: {
|
||||||
|
title: { de: 'Cookie-Einstellungen', en: 'Cookie Settings' },
|
||||||
|
description: {
|
||||||
|
de: 'Wir verwenden Cookies, um Ihnen die bestmoegliche Nutzung unserer Website zu ermoeglichen.',
|
||||||
|
en: 'We use cookies to provide you with the best possible experience on our website.',
|
||||||
|
},
|
||||||
|
acceptAll: { de: 'Alle akzeptieren', en: 'Accept All' },
|
||||||
|
rejectAll: { de: 'Alle ablehnen', en: 'Reject All' },
|
||||||
|
customize: { de: 'Anpassen', en: 'Customize' },
|
||||||
|
save: { de: 'Auswahl speichern', en: 'Save Selection' },
|
||||||
|
privacyPolicyLink: { de: 'Datenschutzerklaerung', en: 'Privacy Policy' },
|
||||||
|
},
|
||||||
|
updatedAt: new Date(),
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch({ type: 'SET_COOKIE_BANNER_CONFIG', payload: config })
|
||||||
|
}, [state.catalog, state.selectedDataPoints, dispatch])
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// CONTEXT VALUE
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const value: EinwilligungenContextValue = {
|
||||||
|
state,
|
||||||
|
dispatch,
|
||||||
|
allDataPoints,
|
||||||
|
selectedDataPointsData,
|
||||||
|
dataPointsByCategory,
|
||||||
|
categoryStats,
|
||||||
|
riskStats,
|
||||||
|
legalBasisStats,
|
||||||
|
initializeCatalog,
|
||||||
|
loadCatalog,
|
||||||
|
saveCatalog,
|
||||||
|
toggleDataPoint,
|
||||||
|
addCustomDataPoint,
|
||||||
|
updateDataPoint,
|
||||||
|
deleteCustomDataPoint,
|
||||||
|
setActiveTab,
|
||||||
|
setPreviewLanguage,
|
||||||
|
setPreviewFormat,
|
||||||
|
setCompanyInfo,
|
||||||
|
generatePrivacyPolicy,
|
||||||
|
generateCookieBannerConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EinwilligungenContext.Provider value={value}>{children}</EinwilligungenContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
237
admin-compliance/lib/sdk/einwilligungen/reducer.ts
Normal file
237
admin-compliance/lib/sdk/einwilligungen/reducer.ts
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
/**
|
||||||
|
* Einwilligungen Reducer
|
||||||
|
*
|
||||||
|
* Action-Handling und State-Uebergaenge fuer das Einwilligungen-Modul.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
EinwilligungenState,
|
||||||
|
EinwilligungenAction,
|
||||||
|
} from './types'
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// INITIAL STATE
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export const initialState: EinwilligungenState = {
|
||||||
|
// Data
|
||||||
|
catalog: null,
|
||||||
|
selectedDataPoints: [],
|
||||||
|
privacyPolicy: null,
|
||||||
|
cookieBannerConfig: null,
|
||||||
|
companyInfo: null,
|
||||||
|
consentStatistics: null,
|
||||||
|
|
||||||
|
// UI State
|
||||||
|
activeTab: 'catalog',
|
||||||
|
isLoading: false,
|
||||||
|
isSaving: false,
|
||||||
|
error: null,
|
||||||
|
|
||||||
|
// Editor State
|
||||||
|
editingDataPoint: null,
|
||||||
|
editingSection: null,
|
||||||
|
|
||||||
|
// Preview
|
||||||
|
previewLanguage: 'de',
|
||||||
|
previewFormat: 'HTML',
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// REDUCER
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export function einwilligungenReducer(
|
||||||
|
state: EinwilligungenState,
|
||||||
|
action: EinwilligungenAction
|
||||||
|
): EinwilligungenState {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'SET_CATALOG':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
catalog: action.payload,
|
||||||
|
selectedDataPoints: [
|
||||||
|
...action.payload.dataPoints.filter((dp) => dp.isActive !== false).map((dp) => dp.id),
|
||||||
|
...action.payload.customDataPoints.filter((dp) => dp.isActive !== false).map((dp) => dp.id),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'SET_SELECTED_DATA_POINTS':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
selectedDataPoints: action.payload,
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'TOGGLE_DATA_POINT': {
|
||||||
|
const id = action.payload
|
||||||
|
const isSelected = state.selectedDataPoints.includes(id)
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
selectedDataPoints: isSelected
|
||||||
|
? state.selectedDataPoints.filter((dpId) => dpId !== id)
|
||||||
|
: [...state.selectedDataPoints, id],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'ADD_CUSTOM_DATA_POINT':
|
||||||
|
if (!state.catalog) return state
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
catalog: {
|
||||||
|
...state.catalog,
|
||||||
|
customDataPoints: [...state.catalog.customDataPoints, action.payload],
|
||||||
|
updatedAt: new Date(),
|
||||||
|
},
|
||||||
|
selectedDataPoints: [...state.selectedDataPoints, action.payload.id],
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'UPDATE_DATA_POINT': {
|
||||||
|
if (!state.catalog) return state
|
||||||
|
const { id, data } = action.payload
|
||||||
|
|
||||||
|
const isCustom = state.catalog.customDataPoints.some((dp) => dp.id === id)
|
||||||
|
|
||||||
|
if (isCustom) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
catalog: {
|
||||||
|
...state.catalog,
|
||||||
|
customDataPoints: state.catalog.customDataPoints.map((dp) =>
|
||||||
|
dp.id === id ? { ...dp, ...data } : dp
|
||||||
|
),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
catalog: {
|
||||||
|
...state.catalog,
|
||||||
|
dataPoints: state.catalog.dataPoints.map((dp) =>
|
||||||
|
dp.id === id ? { ...dp, ...data } : dp
|
||||||
|
),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'DELETE_CUSTOM_DATA_POINT':
|
||||||
|
if (!state.catalog) return state
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
catalog: {
|
||||||
|
...state.catalog,
|
||||||
|
customDataPoints: state.catalog.customDataPoints.filter((dp) => dp.id !== action.payload),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
},
|
||||||
|
selectedDataPoints: state.selectedDataPoints.filter((id) => id !== action.payload),
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'SET_PRIVACY_POLICY':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
privacyPolicy: action.payload,
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'SET_COOKIE_BANNER_CONFIG':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
cookieBannerConfig: action.payload,
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'UPDATE_COOKIE_BANNER_STYLING':
|
||||||
|
if (!state.cookieBannerConfig) return state
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
cookieBannerConfig: {
|
||||||
|
...state.cookieBannerConfig,
|
||||||
|
styling: {
|
||||||
|
...state.cookieBannerConfig.styling,
|
||||||
|
...action.payload,
|
||||||
|
},
|
||||||
|
updatedAt: new Date(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'UPDATE_COOKIE_BANNER_TEXTS':
|
||||||
|
if (!state.cookieBannerConfig) return state
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
cookieBannerConfig: {
|
||||||
|
...state.cookieBannerConfig,
|
||||||
|
texts: {
|
||||||
|
...state.cookieBannerConfig.texts,
|
||||||
|
...action.payload,
|
||||||
|
},
|
||||||
|
updatedAt: new Date(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'SET_COMPANY_INFO':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
companyInfo: action.payload,
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'SET_CONSENT_STATISTICS':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
consentStatistics: action.payload,
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'SET_ACTIVE_TAB':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
activeTab: action.payload,
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'SET_LOADING':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
isLoading: action.payload,
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'SET_SAVING':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
isSaving: action.payload,
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'SET_ERROR':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
error: action.payload,
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'SET_EDITING_DATA_POINT':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
editingDataPoint: action.payload,
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'SET_EDITING_SECTION':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
editingSection: action.payload,
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'SET_PREVIEW_LANGUAGE':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
previewLanguage: action.payload,
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'SET_PREVIEW_FORMAT':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
previewFormat: action.payload,
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'RESET_STATE':
|
||||||
|
return initialState
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
}
|
||||||
361
admin-compliance/lib/sdk/export-pdf.ts
Normal file
361
admin-compliance/lib/sdk/export-pdf.ts
Normal file
@@ -0,0 +1,361 @@
|
|||||||
|
/**
|
||||||
|
* SDK PDF Export
|
||||||
|
* Generates PDF compliance reports from SDK state
|
||||||
|
*/
|
||||||
|
|
||||||
|
import jsPDF from 'jspdf'
|
||||||
|
import { SDKState, SDK_STEPS } from './types'
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// TYPES
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export interface ExportOptions {
|
||||||
|
includeEvidence?: boolean
|
||||||
|
includeDocuments?: boolean
|
||||||
|
includeRawData?: boolean
|
||||||
|
language?: 'de' | 'en'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_OPTIONS: ExportOptions = {
|
||||||
|
includeEvidence: true,
|
||||||
|
includeDocuments: true,
|
||||||
|
includeRawData: true,
|
||||||
|
language: 'de',
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// LABELS (German)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export const LABELS_DE = {
|
||||||
|
title: 'AI Compliance SDK - Export',
|
||||||
|
subtitle: 'Compliance-Dokumentation',
|
||||||
|
generatedAt: 'Generiert am',
|
||||||
|
page: 'Seite',
|
||||||
|
summary: 'Zusammenfassung',
|
||||||
|
progress: 'Fortschritt',
|
||||||
|
phase1: 'Phase 1: Automatisches Compliance Assessment',
|
||||||
|
phase2: 'Phase 2: Dokumentengenerierung',
|
||||||
|
useCases: 'Use Cases',
|
||||||
|
risks: 'Risiken',
|
||||||
|
controls: 'Controls',
|
||||||
|
requirements: 'Anforderungen',
|
||||||
|
modules: 'Compliance-Module',
|
||||||
|
evidence: 'Nachweise',
|
||||||
|
checkpoints: 'Checkpoints',
|
||||||
|
noData: 'Keine Daten vorhanden',
|
||||||
|
status: 'Status',
|
||||||
|
completed: 'Abgeschlossen',
|
||||||
|
pending: 'Ausstehend',
|
||||||
|
inProgress: 'In Bearbeitung',
|
||||||
|
severity: 'Schweregrad',
|
||||||
|
mitigation: 'Mitigation',
|
||||||
|
description: 'Beschreibung',
|
||||||
|
category: 'Kategorie',
|
||||||
|
implementation: 'Implementierung',
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// HELPERS
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export function formatDate(date: Date | string | undefined): string {
|
||||||
|
if (!date) return '-'
|
||||||
|
const d = typeof date === 'string' ? new Date(date) : date
|
||||||
|
return d.toLocaleDateString('de-DE', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function addHeader(doc: jsPDF, title: string, pageNum: number, totalPages: number): void {
|
||||||
|
const pageWidth = doc.internal.pageSize.getWidth()
|
||||||
|
doc.setDrawColor(147, 51, 234)
|
||||||
|
doc.setLineWidth(0.5)
|
||||||
|
doc.line(20, 15, pageWidth - 20, 15)
|
||||||
|
doc.setFontSize(10)
|
||||||
|
doc.setTextColor(100)
|
||||||
|
doc.text(title, 20, 12)
|
||||||
|
doc.text(`${LABELS_DE.page} ${pageNum}/${totalPages}`, pageWidth - 40, 12)
|
||||||
|
}
|
||||||
|
|
||||||
|
function addFooter(doc: jsPDF, state: SDKState): void {
|
||||||
|
const pageWidth = doc.internal.pageSize.getWidth()
|
||||||
|
const pageHeight = doc.internal.pageSize.getHeight()
|
||||||
|
doc.setDrawColor(200)
|
||||||
|
doc.setLineWidth(0.3)
|
||||||
|
doc.line(20, pageHeight - 15, pageWidth - 20, pageHeight - 15)
|
||||||
|
doc.setFontSize(8)
|
||||||
|
doc.setTextColor(150)
|
||||||
|
doc.text(`Tenant: ${state.tenantId} | ${LABELS_DE.generatedAt}: ${formatDate(new Date())}`, 20, pageHeight - 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
function addSectionTitle(doc: jsPDF, title: string, y: number): number {
|
||||||
|
doc.setFontSize(14)
|
||||||
|
doc.setTextColor(147, 51, 234)
|
||||||
|
doc.setFont('helvetica', 'bold')
|
||||||
|
doc.text(title, 20, y)
|
||||||
|
doc.setFont('helvetica', 'normal')
|
||||||
|
return y + 10
|
||||||
|
}
|
||||||
|
|
||||||
|
function addSubsectionTitle(doc: jsPDF, title: string, y: number): number {
|
||||||
|
doc.setFontSize(11)
|
||||||
|
doc.setTextColor(60)
|
||||||
|
doc.setFont('helvetica', 'bold')
|
||||||
|
doc.text(title, 25, y)
|
||||||
|
doc.setFont('helvetica', 'normal')
|
||||||
|
return y + 7
|
||||||
|
}
|
||||||
|
|
||||||
|
function addText(doc: jsPDF, text: string, x: number, y: number, maxWidth: number = 170): number {
|
||||||
|
doc.setFontSize(10)
|
||||||
|
doc.setTextColor(60)
|
||||||
|
const lines = doc.splitTextToSize(text, maxWidth)
|
||||||
|
doc.text(lines, x, y)
|
||||||
|
return y + lines.length * 5
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkPageBreak(doc: jsPDF, y: number, requiredSpace: number = 40): number {
|
||||||
|
const pageHeight = doc.internal.pageSize.getHeight()
|
||||||
|
if (y + requiredSpace > pageHeight - 25) {
|
||||||
|
doc.addPage()
|
||||||
|
return 30
|
||||||
|
}
|
||||||
|
return y
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// PDF EXPORT
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export async function exportToPDF(state: SDKState, options: ExportOptions = {}): Promise<Blob> {
|
||||||
|
const doc = new jsPDF()
|
||||||
|
|
||||||
|
let y = 30
|
||||||
|
const pageWidth = doc.internal.pageSize.getWidth()
|
||||||
|
|
||||||
|
// Title Page
|
||||||
|
doc.setFillColor(147, 51, 234)
|
||||||
|
doc.rect(0, 0, pageWidth, 60, 'F')
|
||||||
|
doc.setFontSize(24)
|
||||||
|
doc.setTextColor(255)
|
||||||
|
doc.setFont('helvetica', 'bold')
|
||||||
|
doc.text(LABELS_DE.title, 20, 35)
|
||||||
|
doc.setFontSize(14)
|
||||||
|
doc.setFont('helvetica', 'normal')
|
||||||
|
doc.text(LABELS_DE.subtitle, 20, 48)
|
||||||
|
|
||||||
|
y = 80
|
||||||
|
doc.setDrawColor(200)
|
||||||
|
doc.setFillColor(249, 250, 251)
|
||||||
|
doc.roundedRect(20, y, pageWidth - 40, 50, 3, 3, 'FD')
|
||||||
|
|
||||||
|
y += 15
|
||||||
|
doc.setFontSize(12)
|
||||||
|
doc.setTextColor(60)
|
||||||
|
doc.text(`${LABELS_DE.generatedAt}: ${formatDate(new Date())}`, 30, y)
|
||||||
|
y += 10
|
||||||
|
doc.text(`Tenant ID: ${state.tenantId}`, 30, y)
|
||||||
|
y += 10
|
||||||
|
doc.text(`Version: ${state.version}`, 30, y)
|
||||||
|
y += 10
|
||||||
|
const completedSteps = state.completedSteps.length
|
||||||
|
const totalSteps = SDK_STEPS.length
|
||||||
|
doc.text(`${LABELS_DE.progress}: ${completedSteps}/${totalSteps} Schritte (${Math.round(completedSteps / totalSteps * 100)}%)`, 30, y)
|
||||||
|
|
||||||
|
y += 30
|
||||||
|
y = addSectionTitle(doc, 'Inhaltsverzeichnis', y)
|
||||||
|
|
||||||
|
const tocItems = [
|
||||||
|
{ title: 'Zusammenfassung', page: 2 },
|
||||||
|
{ title: 'Phase 1: Compliance Assessment', page: 3 },
|
||||||
|
{ title: 'Phase 2: Dokumentengenerierung', page: 4 },
|
||||||
|
{ title: 'Risiken & Controls', page: 5 },
|
||||||
|
{ title: 'Checkpoints', page: 6 },
|
||||||
|
]
|
||||||
|
|
||||||
|
doc.setFontSize(10)
|
||||||
|
doc.setTextColor(80)
|
||||||
|
tocItems.forEach((item, idx) => {
|
||||||
|
doc.text(`${idx + 1}. ${item.title}`, 25, y)
|
||||||
|
doc.text(`${item.page}`, pageWidth - 30, y, { align: 'right' })
|
||||||
|
y += 7
|
||||||
|
})
|
||||||
|
|
||||||
|
// Summary Page
|
||||||
|
doc.addPage()
|
||||||
|
y = 30
|
||||||
|
y = addSectionTitle(doc, LABELS_DE.summary, y)
|
||||||
|
|
||||||
|
doc.setFillColor(249, 250, 251)
|
||||||
|
doc.roundedRect(20, y, pageWidth - 40, 40, 3, 3, 'F')
|
||||||
|
y += 15
|
||||||
|
const phase1Steps = SDK_STEPS.filter(s => s.phase === 1)
|
||||||
|
const phase2Steps = SDK_STEPS.filter(s => s.phase === 2)
|
||||||
|
const phase1Completed = phase1Steps.filter(s => state.completedSteps.includes(s.id)).length
|
||||||
|
const phase2Completed = phase2Steps.filter(s => state.completedSteps.includes(s.id)).length
|
||||||
|
|
||||||
|
doc.setFontSize(10)
|
||||||
|
doc.setTextColor(60)
|
||||||
|
doc.text(`${LABELS_DE.phase1}: ${phase1Completed}/${phase1Steps.length} ${LABELS_DE.completed}`, 30, y)
|
||||||
|
y += 8
|
||||||
|
doc.text(`${LABELS_DE.phase2}: ${phase2Completed}/${phase2Steps.length} ${LABELS_DE.completed}`, 30, y)
|
||||||
|
y += 25
|
||||||
|
|
||||||
|
y = addSubsectionTitle(doc, 'Kennzahlen', y)
|
||||||
|
const metrics = [
|
||||||
|
{ label: 'Use Cases', value: state.useCases.length },
|
||||||
|
{ label: 'Risiken identifiziert', value: state.risks.length },
|
||||||
|
{ label: 'Controls definiert', value: state.controls.length },
|
||||||
|
{ label: 'Anforderungen', value: state.requirements.length },
|
||||||
|
{ label: 'Nachweise', value: state.evidence.length },
|
||||||
|
]
|
||||||
|
metrics.forEach(metric => {
|
||||||
|
doc.text(`${metric.label}: ${metric.value}`, 30, y)
|
||||||
|
y += 7
|
||||||
|
})
|
||||||
|
|
||||||
|
// Use Cases
|
||||||
|
y += 10
|
||||||
|
y = checkPageBreak(doc, y)
|
||||||
|
y = addSectionTitle(doc, LABELS_DE.useCases, y)
|
||||||
|
|
||||||
|
if (state.useCases.length === 0) {
|
||||||
|
y = addText(doc, LABELS_DE.noData, 25, y)
|
||||||
|
} else {
|
||||||
|
state.useCases.forEach((uc, idx) => {
|
||||||
|
y = checkPageBreak(doc, y, 50)
|
||||||
|
doc.setFillColor(249, 250, 251)
|
||||||
|
doc.roundedRect(20, y - 5, pageWidth - 40, 35, 2, 2, 'F')
|
||||||
|
doc.setFontSize(11)
|
||||||
|
doc.setTextColor(40)
|
||||||
|
doc.setFont('helvetica', 'bold')
|
||||||
|
doc.text(`${idx + 1}. ${uc.name}`, 25, y + 5)
|
||||||
|
doc.setFont('helvetica', 'normal')
|
||||||
|
doc.setFontSize(9)
|
||||||
|
doc.setTextColor(100)
|
||||||
|
const ucStatus = uc.stepsCompleted === uc.steps.length ? LABELS_DE.completed : `${uc.stepsCompleted}/${uc.steps.length} Schritte`
|
||||||
|
doc.text(`ID: ${uc.id} | ${LABELS_DE.status}: ${ucStatus}`, 25, y + 13)
|
||||||
|
if (uc.description) {
|
||||||
|
y = addText(doc, uc.description, 25, y + 21, 160)
|
||||||
|
}
|
||||||
|
y += 40
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Risks
|
||||||
|
doc.addPage()
|
||||||
|
y = 30
|
||||||
|
y = addSectionTitle(doc, LABELS_DE.risks, y)
|
||||||
|
|
||||||
|
if (state.risks.length === 0) {
|
||||||
|
y = addText(doc, LABELS_DE.noData, 25, y)
|
||||||
|
} else {
|
||||||
|
const sortedRisks = [...state.risks].sort((a, b) => {
|
||||||
|
const order = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 }
|
||||||
|
return (order[a.severity] || 4) - (order[b.severity] || 4)
|
||||||
|
})
|
||||||
|
sortedRisks.forEach((risk, idx) => {
|
||||||
|
y = checkPageBreak(doc, y, 45)
|
||||||
|
const severityColors: Record<string, [number, number, number]> = {
|
||||||
|
CRITICAL: [220, 38, 38], HIGH: [234, 88, 12],
|
||||||
|
MEDIUM: [234, 179, 8], LOW: [34, 197, 94],
|
||||||
|
}
|
||||||
|
const color = severityColors[risk.severity] || [100, 100, 100]
|
||||||
|
doc.setFillColor(color[0], color[1], color[2])
|
||||||
|
doc.rect(20, y - 3, 3, 30, 'F')
|
||||||
|
doc.setFontSize(11)
|
||||||
|
doc.setTextColor(40)
|
||||||
|
doc.setFont('helvetica', 'bold')
|
||||||
|
doc.text(`${idx + 1}. ${risk.title}`, 28, y + 5)
|
||||||
|
doc.setFont('helvetica', 'normal')
|
||||||
|
doc.setFontSize(9)
|
||||||
|
doc.setTextColor(100)
|
||||||
|
doc.text(`${LABELS_DE.severity}: ${risk.severity} | ${LABELS_DE.category}: ${risk.category}`, 28, y + 13)
|
||||||
|
if (risk.description) {
|
||||||
|
y = addText(doc, risk.description, 28, y + 21, 155)
|
||||||
|
}
|
||||||
|
if (risk.mitigation && risk.mitigation.length > 0) {
|
||||||
|
y += 5
|
||||||
|
doc.setFontSize(9)
|
||||||
|
doc.setTextColor(34, 197, 94)
|
||||||
|
doc.text(`${LABELS_DE.mitigation}: ${risk.mitigation.join(', ')}`, 28, y)
|
||||||
|
}
|
||||||
|
y += 15
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Controls
|
||||||
|
doc.addPage()
|
||||||
|
y = 30
|
||||||
|
y = addSectionTitle(doc, LABELS_DE.controls, y)
|
||||||
|
|
||||||
|
if (state.controls.length === 0) {
|
||||||
|
y = addText(doc, LABELS_DE.noData, 25, y)
|
||||||
|
} else {
|
||||||
|
state.controls.forEach((ctrl, idx) => {
|
||||||
|
y = checkPageBreak(doc, y, 35)
|
||||||
|
doc.setFillColor(249, 250, 251)
|
||||||
|
doc.roundedRect(20, y - 5, pageWidth - 40, 28, 2, 2, 'F')
|
||||||
|
doc.setFontSize(10)
|
||||||
|
doc.setTextColor(40)
|
||||||
|
doc.setFont('helvetica', 'bold')
|
||||||
|
doc.text(`${idx + 1}. ${ctrl.name}`, 25, y + 5)
|
||||||
|
doc.setFont('helvetica', 'normal')
|
||||||
|
doc.setFontSize(9)
|
||||||
|
doc.setTextColor(100)
|
||||||
|
doc.text(`${LABELS_DE.category}: ${ctrl.category} | ${LABELS_DE.implementation}: ${ctrl.implementationStatus || 'Nicht definiert'}`, 25, y + 13)
|
||||||
|
if (ctrl.description) {
|
||||||
|
y = addText(doc, ctrl.description.substring(0, 150) + (ctrl.description.length > 150 ? '...' : ''), 25, y + 20, 160)
|
||||||
|
}
|
||||||
|
y += 35
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checkpoints
|
||||||
|
doc.addPage()
|
||||||
|
y = 30
|
||||||
|
y = addSectionTitle(doc, LABELS_DE.checkpoints, y)
|
||||||
|
|
||||||
|
const checkpointIds = Object.keys(state.checkpoints)
|
||||||
|
if (checkpointIds.length === 0) {
|
||||||
|
y = addText(doc, LABELS_DE.noData, 25, y)
|
||||||
|
} else {
|
||||||
|
checkpointIds.forEach((cpId) => {
|
||||||
|
const cp = state.checkpoints[cpId]
|
||||||
|
y = checkPageBreak(doc, y, 25)
|
||||||
|
const statusColor = cp.passed ? [34, 197, 94] : [220, 38, 38]
|
||||||
|
doc.setFillColor(statusColor[0], statusColor[1], statusColor[2])
|
||||||
|
doc.circle(25, y + 2, 3, 'F')
|
||||||
|
doc.setFontSize(10)
|
||||||
|
doc.setTextColor(40)
|
||||||
|
doc.text(cpId, 35, y + 5)
|
||||||
|
doc.setFontSize(9)
|
||||||
|
doc.setTextColor(100)
|
||||||
|
doc.text(`${LABELS_DE.status}: ${cp.passed ? LABELS_DE.completed : LABELS_DE.pending}`, 35, y + 12)
|
||||||
|
if (cp.errors && cp.errors.length > 0) {
|
||||||
|
doc.setTextColor(220, 38, 38)
|
||||||
|
doc.text(`Fehler: ${cp.errors.map(e => e.message).join(', ')}`, 35, y + 19)
|
||||||
|
y += 7
|
||||||
|
}
|
||||||
|
y += 20
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add page numbers
|
||||||
|
const pageCount = doc.getNumberOfPages()
|
||||||
|
for (let i = 1; i <= pageCount; i++) {
|
||||||
|
doc.setPage(i)
|
||||||
|
if (i > 1) {
|
||||||
|
addHeader(doc, LABELS_DE.title, i, pageCount)
|
||||||
|
}
|
||||||
|
addFooter(doc, state)
|
||||||
|
}
|
||||||
|
|
||||||
|
return doc.output('blob')
|
||||||
|
}
|
||||||
240
admin-compliance/lib/sdk/export-zip.ts
Normal file
240
admin-compliance/lib/sdk/export-zip.ts
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
/**
|
||||||
|
* SDK ZIP Export
|
||||||
|
* Packages SDK state, documents, and a PDF report into a ZIP archive
|
||||||
|
*/
|
||||||
|
|
||||||
|
import JSZip from 'jszip'
|
||||||
|
import { SDKState, SDK_STEPS } from './types'
|
||||||
|
import { ExportOptions, DEFAULT_OPTIONS, formatDate, exportToPDF } from './export-pdf'
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// ZIP EXPORT
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export async function exportToZIP(state: SDKState, options: ExportOptions = {}): Promise<Blob> {
|
||||||
|
const opts = { ...DEFAULT_OPTIONS, ...options }
|
||||||
|
const zip = new JSZip()
|
||||||
|
|
||||||
|
const rootFolder = zip.folder('ai-compliance-sdk-export')
|
||||||
|
if (!rootFolder) throw new Error('Failed to create ZIP folder')
|
||||||
|
|
||||||
|
const phase1Folder = rootFolder.folder('phase1-assessment')
|
||||||
|
const phase2Folder = rootFolder.folder('phase2-documents')
|
||||||
|
const dataFolder = rootFolder.folder('data')
|
||||||
|
|
||||||
|
// Main State JSON
|
||||||
|
if (opts.includeRawData && dataFolder) {
|
||||||
|
dataFolder.file('state.json', JSON.stringify(state, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
// README
|
||||||
|
const readmeContent = `# AI Compliance SDK Export
|
||||||
|
|
||||||
|
Generated: ${formatDate(new Date())}
|
||||||
|
Tenant: ${state.tenantId}
|
||||||
|
Version: ${state.version}
|
||||||
|
|
||||||
|
## Folder Structure
|
||||||
|
|
||||||
|
- **phase1-assessment/**: Compliance Assessment Ergebnisse
|
||||||
|
- use-cases.json: Alle Use Cases
|
||||||
|
- risks.json: Identifizierte Risiken
|
||||||
|
- controls.json: Definierte Controls
|
||||||
|
- requirements.json: Compliance-Anforderungen
|
||||||
|
|
||||||
|
- **phase2-documents/**: Generierte Dokumente
|
||||||
|
- dsfa.json: Datenschutz-Folgenabschaetzung
|
||||||
|
- toms.json: Technische und organisatorische Massnahmen
|
||||||
|
- vvt.json: Verarbeitungsverzeichnis
|
||||||
|
- documents.json: Rechtliche Dokumente
|
||||||
|
|
||||||
|
- **data/**: Rohdaten
|
||||||
|
- state.json: Kompletter SDK State
|
||||||
|
|
||||||
|
## Progress
|
||||||
|
|
||||||
|
Phase 1: ${SDK_STEPS.filter(s => s.phase === 1 && state.completedSteps.includes(s.id)).length}/${SDK_STEPS.filter(s => s.phase === 1).length} completed
|
||||||
|
Phase 2: ${SDK_STEPS.filter(s => s.phase === 2 && state.completedSteps.includes(s.id)).length}/${SDK_STEPS.filter(s => s.phase === 2).length} completed
|
||||||
|
|
||||||
|
## Key Metrics
|
||||||
|
|
||||||
|
- Use Cases: ${state.useCases.length}
|
||||||
|
- Risks: ${state.risks.length}
|
||||||
|
- Controls: ${state.controls.length}
|
||||||
|
- Requirements: ${state.requirements.length}
|
||||||
|
- Evidence: ${state.evidence.length}
|
||||||
|
`
|
||||||
|
|
||||||
|
rootFolder.file('README.md', readmeContent)
|
||||||
|
|
||||||
|
// Phase 1 Files
|
||||||
|
if (phase1Folder) {
|
||||||
|
phase1Folder.file('use-cases.json', JSON.stringify({
|
||||||
|
exportedAt: new Date().toISOString(),
|
||||||
|
count: state.useCases.length,
|
||||||
|
useCases: state.useCases,
|
||||||
|
}, null, 2))
|
||||||
|
|
||||||
|
phase1Folder.file('risks.json', JSON.stringify({
|
||||||
|
exportedAt: new Date().toISOString(),
|
||||||
|
count: state.risks.length,
|
||||||
|
risks: state.risks,
|
||||||
|
summary: {
|
||||||
|
critical: state.risks.filter(r => r.severity === 'CRITICAL').length,
|
||||||
|
high: state.risks.filter(r => r.severity === 'HIGH').length,
|
||||||
|
medium: state.risks.filter(r => r.severity === 'MEDIUM').length,
|
||||||
|
low: state.risks.filter(r => r.severity === 'LOW').length,
|
||||||
|
},
|
||||||
|
}, null, 2))
|
||||||
|
|
||||||
|
phase1Folder.file('controls.json', JSON.stringify({
|
||||||
|
exportedAt: new Date().toISOString(),
|
||||||
|
count: state.controls.length,
|
||||||
|
controls: state.controls,
|
||||||
|
}, null, 2))
|
||||||
|
|
||||||
|
phase1Folder.file('requirements.json', JSON.stringify({
|
||||||
|
exportedAt: new Date().toISOString(),
|
||||||
|
count: state.requirements.length,
|
||||||
|
requirements: state.requirements,
|
||||||
|
}, null, 2))
|
||||||
|
|
||||||
|
phase1Folder.file('modules.json', JSON.stringify({
|
||||||
|
exportedAt: new Date().toISOString(),
|
||||||
|
count: state.modules.length,
|
||||||
|
modules: state.modules,
|
||||||
|
}, null, 2))
|
||||||
|
|
||||||
|
if (opts.includeEvidence) {
|
||||||
|
phase1Folder.file('evidence.json', JSON.stringify({
|
||||||
|
exportedAt: new Date().toISOString(),
|
||||||
|
count: state.evidence.length,
|
||||||
|
evidence: state.evidence,
|
||||||
|
}, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
phase1Folder.file('checkpoints.json', JSON.stringify({
|
||||||
|
exportedAt: new Date().toISOString(),
|
||||||
|
checkpoints: state.checkpoints,
|
||||||
|
}, null, 2))
|
||||||
|
|
||||||
|
if (state.screening) {
|
||||||
|
phase1Folder.file('screening.json', JSON.stringify({
|
||||||
|
exportedAt: new Date().toISOString(),
|
||||||
|
screening: state.screening,
|
||||||
|
}, null, 2))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 2 Files
|
||||||
|
if (phase2Folder) {
|
||||||
|
if (state.dsfa) {
|
||||||
|
phase2Folder.file('dsfa.json', JSON.stringify({
|
||||||
|
exportedAt: new Date().toISOString(),
|
||||||
|
dsfa: state.dsfa,
|
||||||
|
}, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
phase2Folder.file('toms.json', JSON.stringify({
|
||||||
|
exportedAt: new Date().toISOString(),
|
||||||
|
count: state.toms.length,
|
||||||
|
toms: state.toms,
|
||||||
|
}, null, 2))
|
||||||
|
|
||||||
|
phase2Folder.file('vvt.json', JSON.stringify({
|
||||||
|
exportedAt: new Date().toISOString(),
|
||||||
|
count: state.vvt.length,
|
||||||
|
processingActivities: state.vvt,
|
||||||
|
}, null, 2))
|
||||||
|
|
||||||
|
if (opts.includeDocuments) {
|
||||||
|
phase2Folder.file('documents.json', JSON.stringify({
|
||||||
|
exportedAt: new Date().toISOString(),
|
||||||
|
count: state.documents.length,
|
||||||
|
documents: state.documents,
|
||||||
|
}, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.cookieBanner) {
|
||||||
|
phase2Folder.file('cookie-banner.json', JSON.stringify({
|
||||||
|
exportedAt: new Date().toISOString(),
|
||||||
|
config: state.cookieBanner,
|
||||||
|
}, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
phase2Folder.file('retention-policies.json', JSON.stringify({
|
||||||
|
exportedAt: new Date().toISOString(),
|
||||||
|
count: state.retentionPolicies.length,
|
||||||
|
policies: state.retentionPolicies,
|
||||||
|
}, null, 2))
|
||||||
|
|
||||||
|
if (state.aiActClassification) {
|
||||||
|
phase2Folder.file('ai-act-classification.json', JSON.stringify({
|
||||||
|
exportedAt: new Date().toISOString(),
|
||||||
|
classification: state.aiActClassification,
|
||||||
|
}, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
phase2Folder.file('obligations.json', JSON.stringify({
|
||||||
|
exportedAt: new Date().toISOString(),
|
||||||
|
count: state.obligations.length,
|
||||||
|
obligations: state.obligations,
|
||||||
|
}, null, 2))
|
||||||
|
|
||||||
|
phase2Folder.file('consents.json', JSON.stringify({
|
||||||
|
exportedAt: new Date().toISOString(),
|
||||||
|
count: state.consents.length,
|
||||||
|
consents: state.consents,
|
||||||
|
}, null, 2))
|
||||||
|
|
||||||
|
if (state.dsrConfig) {
|
||||||
|
phase2Folder.file('dsr-config.json', JSON.stringify({
|
||||||
|
exportedAt: new Date().toISOString(),
|
||||||
|
config: state.dsrConfig,
|
||||||
|
}, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
phase2Folder.file('escalation-workflows.json', JSON.stringify({
|
||||||
|
exportedAt: new Date().toISOString(),
|
||||||
|
count: state.escalationWorkflows.length,
|
||||||
|
workflows: state.escalationWorkflows,
|
||||||
|
}, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Security Data
|
||||||
|
if (dataFolder) {
|
||||||
|
if (state.sbom) {
|
||||||
|
dataFolder.file('sbom.json', JSON.stringify({
|
||||||
|
exportedAt: new Date().toISOString(),
|
||||||
|
sbom: state.sbom,
|
||||||
|
}, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.securityIssues.length > 0) {
|
||||||
|
dataFolder.file('security-issues.json', JSON.stringify({
|
||||||
|
exportedAt: new Date().toISOString(),
|
||||||
|
count: state.securityIssues.length,
|
||||||
|
issues: state.securityIssues,
|
||||||
|
}, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.securityBacklog.length > 0) {
|
||||||
|
dataFolder.file('security-backlog.json', JSON.stringify({
|
||||||
|
exportedAt: new Date().toISOString(),
|
||||||
|
count: state.securityBacklog.length,
|
||||||
|
backlog: state.securityBacklog,
|
||||||
|
}, null, 2))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate PDF and include in ZIP
|
||||||
|
try {
|
||||||
|
const pdfBlob = await exportToPDF(state, options)
|
||||||
|
const pdfArrayBuffer = await pdfBlob.arrayBuffer()
|
||||||
|
rootFolder.file('compliance-report.pdf', pdfArrayBuffer)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to generate PDF for ZIP:', error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return zip.generateAsync({ type: 'blob', compression: 'DEFLATE' })
|
||||||
|
}
|
||||||
@@ -1,711 +1,12 @@
|
|||||||
/**
|
/**
|
||||||
* SDK Export Utilities
|
* SDK Export Utilities — Barrel re-exports
|
||||||
* Handles PDF and ZIP export of SDK state and documents
|
* Preserves the original public API so existing imports work unchanged.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import jsPDF from 'jspdf'
|
import { SDKState } from './types'
|
||||||
import JSZip from 'jszip'
|
export { exportToPDF } from './export-pdf'
|
||||||
import { SDKState, SDK_STEPS, getStepById } from './types'
|
export type { ExportOptions } from './export-pdf'
|
||||||
|
export { exportToZIP } from './export-zip'
|
||||||
// =============================================================================
|
|
||||||
// TYPES
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
export interface ExportOptions {
|
|
||||||
includeEvidence?: boolean
|
|
||||||
includeDocuments?: boolean
|
|
||||||
includeRawData?: boolean
|
|
||||||
language?: 'de' | 'en'
|
|
||||||
}
|
|
||||||
|
|
||||||
const DEFAULT_OPTIONS: ExportOptions = {
|
|
||||||
includeEvidence: true,
|
|
||||||
includeDocuments: true,
|
|
||||||
includeRawData: true,
|
|
||||||
language: 'de',
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// LABELS (German)
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
const LABELS_DE = {
|
|
||||||
title: 'AI Compliance SDK - Export',
|
|
||||||
subtitle: 'Compliance-Dokumentation',
|
|
||||||
generatedAt: 'Generiert am',
|
|
||||||
page: 'Seite',
|
|
||||||
summary: 'Zusammenfassung',
|
|
||||||
progress: 'Fortschritt',
|
|
||||||
phase1: 'Phase 1: Automatisches Compliance Assessment',
|
|
||||||
phase2: 'Phase 2: Dokumentengenerierung',
|
|
||||||
useCases: 'Use Cases',
|
|
||||||
risks: 'Risiken',
|
|
||||||
controls: 'Controls',
|
|
||||||
requirements: 'Anforderungen',
|
|
||||||
modules: 'Compliance-Module',
|
|
||||||
evidence: 'Nachweise',
|
|
||||||
checkpoints: 'Checkpoints',
|
|
||||||
noData: 'Keine Daten vorhanden',
|
|
||||||
status: 'Status',
|
|
||||||
completed: 'Abgeschlossen',
|
|
||||||
pending: 'Ausstehend',
|
|
||||||
inProgress: 'In Bearbeitung',
|
|
||||||
severity: 'Schweregrad',
|
|
||||||
mitigation: 'Mitigation',
|
|
||||||
description: 'Beschreibung',
|
|
||||||
category: 'Kategorie',
|
|
||||||
implementation: 'Implementierung',
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// PDF EXPORT
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
function formatDate(date: Date | string | undefined): string {
|
|
||||||
if (!date) return '-'
|
|
||||||
const d = typeof date === 'string' ? new Date(date) : date
|
|
||||||
return d.toLocaleDateString('de-DE', {
|
|
||||||
day: '2-digit',
|
|
||||||
month: '2-digit',
|
|
||||||
year: 'numeric',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function addHeader(doc: jsPDF, title: string, pageNum: number, totalPages: number): void {
|
|
||||||
const pageWidth = doc.internal.pageSize.getWidth()
|
|
||||||
|
|
||||||
// Header line
|
|
||||||
doc.setDrawColor(147, 51, 234) // Purple
|
|
||||||
doc.setLineWidth(0.5)
|
|
||||||
doc.line(20, 15, pageWidth - 20, 15)
|
|
||||||
|
|
||||||
// Title
|
|
||||||
doc.setFontSize(10)
|
|
||||||
doc.setTextColor(100)
|
|
||||||
doc.text(title, 20, 12)
|
|
||||||
|
|
||||||
// Page number
|
|
||||||
doc.text(`${LABELS_DE.page} ${pageNum}/${totalPages}`, pageWidth - 40, 12)
|
|
||||||
}
|
|
||||||
|
|
||||||
function addFooter(doc: jsPDF, state: SDKState): void {
|
|
||||||
const pageWidth = doc.internal.pageSize.getWidth()
|
|
||||||
const pageHeight = doc.internal.pageSize.getHeight()
|
|
||||||
|
|
||||||
// Footer line
|
|
||||||
doc.setDrawColor(200)
|
|
||||||
doc.setLineWidth(0.3)
|
|
||||||
doc.line(20, pageHeight - 15, pageWidth - 20, pageHeight - 15)
|
|
||||||
|
|
||||||
// Footer text
|
|
||||||
doc.setFontSize(8)
|
|
||||||
doc.setTextColor(150)
|
|
||||||
doc.text(`Tenant: ${state.tenantId} | ${LABELS_DE.generatedAt}: ${formatDate(new Date())}`, 20, pageHeight - 10)
|
|
||||||
}
|
|
||||||
|
|
||||||
function addSectionTitle(doc: jsPDF, title: string, y: number): number {
|
|
||||||
doc.setFontSize(14)
|
|
||||||
doc.setTextColor(147, 51, 234) // Purple
|
|
||||||
doc.setFont('helvetica', 'bold')
|
|
||||||
doc.text(title, 20, y)
|
|
||||||
doc.setFont('helvetica', 'normal')
|
|
||||||
return y + 10
|
|
||||||
}
|
|
||||||
|
|
||||||
function addSubsectionTitle(doc: jsPDF, title: string, y: number): number {
|
|
||||||
doc.setFontSize(11)
|
|
||||||
doc.setTextColor(60)
|
|
||||||
doc.setFont('helvetica', 'bold')
|
|
||||||
doc.text(title, 25, y)
|
|
||||||
doc.setFont('helvetica', 'normal')
|
|
||||||
return y + 7
|
|
||||||
}
|
|
||||||
|
|
||||||
function addText(doc: jsPDF, text: string, x: number, y: number, maxWidth: number = 170): number {
|
|
||||||
doc.setFontSize(10)
|
|
||||||
doc.setTextColor(60)
|
|
||||||
const lines = doc.splitTextToSize(text, maxWidth)
|
|
||||||
doc.text(lines, x, y)
|
|
||||||
return y + lines.length * 5
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkPageBreak(doc: jsPDF, y: number, requiredSpace: number = 40): number {
|
|
||||||
const pageHeight = doc.internal.pageSize.getHeight()
|
|
||||||
if (y + requiredSpace > pageHeight - 25) {
|
|
||||||
doc.addPage()
|
|
||||||
return 30
|
|
||||||
}
|
|
||||||
return y
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function exportToPDF(state: SDKState, options: ExportOptions = {}): Promise<Blob> {
|
|
||||||
const opts = { ...DEFAULT_OPTIONS, ...options }
|
|
||||||
const doc = new jsPDF()
|
|
||||||
|
|
||||||
let y = 30
|
|
||||||
const pageWidth = doc.internal.pageSize.getWidth()
|
|
||||||
|
|
||||||
// ==========================================================================
|
|
||||||
// Title Page
|
|
||||||
// ==========================================================================
|
|
||||||
|
|
||||||
// Logo/Title area
|
|
||||||
doc.setFillColor(147, 51, 234)
|
|
||||||
doc.rect(0, 0, pageWidth, 60, 'F')
|
|
||||||
|
|
||||||
doc.setFontSize(24)
|
|
||||||
doc.setTextColor(255)
|
|
||||||
doc.setFont('helvetica', 'bold')
|
|
||||||
doc.text(LABELS_DE.title, 20, 35)
|
|
||||||
|
|
||||||
doc.setFontSize(14)
|
|
||||||
doc.setFont('helvetica', 'normal')
|
|
||||||
doc.text(LABELS_DE.subtitle, 20, 48)
|
|
||||||
|
|
||||||
// Reset for content
|
|
||||||
y = 80
|
|
||||||
|
|
||||||
// Summary box
|
|
||||||
doc.setDrawColor(200)
|
|
||||||
doc.setFillColor(249, 250, 251)
|
|
||||||
doc.roundedRect(20, y, pageWidth - 40, 50, 3, 3, 'FD')
|
|
||||||
|
|
||||||
y += 15
|
|
||||||
doc.setFontSize(12)
|
|
||||||
doc.setTextColor(60)
|
|
||||||
doc.text(`${LABELS_DE.generatedAt}: ${formatDate(new Date())}`, 30, y)
|
|
||||||
|
|
||||||
y += 10
|
|
||||||
doc.text(`Tenant ID: ${state.tenantId}`, 30, y)
|
|
||||||
|
|
||||||
y += 10
|
|
||||||
doc.text(`Version: ${state.version}`, 30, y)
|
|
||||||
|
|
||||||
y += 10
|
|
||||||
const completedSteps = state.completedSteps.length
|
|
||||||
const totalSteps = SDK_STEPS.length
|
|
||||||
doc.text(`${LABELS_DE.progress}: ${completedSteps}/${totalSteps} Schritte (${Math.round(completedSteps / totalSteps * 100)}%)`, 30, y)
|
|
||||||
|
|
||||||
y += 30
|
|
||||||
|
|
||||||
// Table of Contents
|
|
||||||
y = addSectionTitle(doc, 'Inhaltsverzeichnis', y)
|
|
||||||
|
|
||||||
const tocItems = [
|
|
||||||
{ title: 'Zusammenfassung', page: 2 },
|
|
||||||
{ title: 'Phase 1: Compliance Assessment', page: 3 },
|
|
||||||
{ title: 'Phase 2: Dokumentengenerierung', page: 4 },
|
|
||||||
{ title: 'Risiken & Controls', page: 5 },
|
|
||||||
{ title: 'Checkpoints', page: 6 },
|
|
||||||
]
|
|
||||||
|
|
||||||
doc.setFontSize(10)
|
|
||||||
doc.setTextColor(80)
|
|
||||||
tocItems.forEach((item, idx) => {
|
|
||||||
doc.text(`${idx + 1}. ${item.title}`, 25, y)
|
|
||||||
doc.text(`${item.page}`, pageWidth - 30, y, { align: 'right' })
|
|
||||||
y += 7
|
|
||||||
})
|
|
||||||
|
|
||||||
// ==========================================================================
|
|
||||||
// Summary Page
|
|
||||||
// ==========================================================================
|
|
||||||
|
|
||||||
doc.addPage()
|
|
||||||
y = 30
|
|
||||||
|
|
||||||
y = addSectionTitle(doc, LABELS_DE.summary, y)
|
|
||||||
|
|
||||||
// Progress overview
|
|
||||||
doc.setFillColor(249, 250, 251)
|
|
||||||
doc.roundedRect(20, y, pageWidth - 40, 40, 3, 3, 'F')
|
|
||||||
|
|
||||||
y += 15
|
|
||||||
const phase1Steps = SDK_STEPS.filter(s => s.phase === 1)
|
|
||||||
const phase2Steps = SDK_STEPS.filter(s => s.phase === 2)
|
|
||||||
const phase1Completed = phase1Steps.filter(s => state.completedSteps.includes(s.id)).length
|
|
||||||
const phase2Completed = phase2Steps.filter(s => state.completedSteps.includes(s.id)).length
|
|
||||||
|
|
||||||
doc.setFontSize(10)
|
|
||||||
doc.setTextColor(60)
|
|
||||||
doc.text(`${LABELS_DE.phase1}: ${phase1Completed}/${phase1Steps.length} ${LABELS_DE.completed}`, 30, y)
|
|
||||||
y += 8
|
|
||||||
doc.text(`${LABELS_DE.phase2}: ${phase2Completed}/${phase2Steps.length} ${LABELS_DE.completed}`, 30, y)
|
|
||||||
|
|
||||||
y += 25
|
|
||||||
|
|
||||||
// Key metrics
|
|
||||||
y = addSubsectionTitle(doc, 'Kennzahlen', y)
|
|
||||||
|
|
||||||
const metrics = [
|
|
||||||
{ label: 'Use Cases', value: state.useCases.length },
|
|
||||||
{ label: 'Risiken identifiziert', value: state.risks.length },
|
|
||||||
{ label: 'Controls definiert', value: state.controls.length },
|
|
||||||
{ label: 'Anforderungen', value: state.requirements.length },
|
|
||||||
{ label: 'Nachweise', value: state.evidence.length },
|
|
||||||
]
|
|
||||||
|
|
||||||
metrics.forEach(metric => {
|
|
||||||
doc.text(`${metric.label}: ${metric.value}`, 30, y)
|
|
||||||
y += 7
|
|
||||||
})
|
|
||||||
|
|
||||||
// ==========================================================================
|
|
||||||
// Use Cases
|
|
||||||
// ==========================================================================
|
|
||||||
|
|
||||||
y += 10
|
|
||||||
y = checkPageBreak(doc, y)
|
|
||||||
y = addSectionTitle(doc, LABELS_DE.useCases, y)
|
|
||||||
|
|
||||||
if (state.useCases.length === 0) {
|
|
||||||
y = addText(doc, LABELS_DE.noData, 25, y)
|
|
||||||
} else {
|
|
||||||
state.useCases.forEach((uc, idx) => {
|
|
||||||
y = checkPageBreak(doc, y, 50)
|
|
||||||
|
|
||||||
doc.setFillColor(249, 250, 251)
|
|
||||||
doc.roundedRect(20, y - 5, pageWidth - 40, 35, 2, 2, 'F')
|
|
||||||
|
|
||||||
doc.setFontSize(11)
|
|
||||||
doc.setTextColor(40)
|
|
||||||
doc.setFont('helvetica', 'bold')
|
|
||||||
doc.text(`${idx + 1}. ${uc.name}`, 25, y + 5)
|
|
||||||
doc.setFont('helvetica', 'normal')
|
|
||||||
|
|
||||||
doc.setFontSize(9)
|
|
||||||
doc.setTextColor(100)
|
|
||||||
const ucStatus = uc.stepsCompleted === uc.steps.length ? LABELS_DE.completed : `${uc.stepsCompleted}/${uc.steps.length} Schritte`
|
|
||||||
doc.text(`ID: ${uc.id} | ${LABELS_DE.status}: ${ucStatus}`, 25, y + 13)
|
|
||||||
|
|
||||||
if (uc.description) {
|
|
||||||
y = addText(doc, uc.description, 25, y + 21, 160)
|
|
||||||
}
|
|
||||||
|
|
||||||
y += 40
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==========================================================================
|
|
||||||
// Risks
|
|
||||||
// ==========================================================================
|
|
||||||
|
|
||||||
doc.addPage()
|
|
||||||
y = 30
|
|
||||||
y = addSectionTitle(doc, LABELS_DE.risks, y)
|
|
||||||
|
|
||||||
if (state.risks.length === 0) {
|
|
||||||
y = addText(doc, LABELS_DE.noData, 25, y)
|
|
||||||
} else {
|
|
||||||
// Sort by severity
|
|
||||||
const sortedRisks = [...state.risks].sort((a, b) => {
|
|
||||||
const order = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 }
|
|
||||||
return (order[a.severity] || 4) - (order[b.severity] || 4)
|
|
||||||
})
|
|
||||||
|
|
||||||
sortedRisks.forEach((risk, idx) => {
|
|
||||||
y = checkPageBreak(doc, y, 45)
|
|
||||||
|
|
||||||
// Severity color
|
|
||||||
const severityColors: Record<string, [number, number, number]> = {
|
|
||||||
CRITICAL: [220, 38, 38],
|
|
||||||
HIGH: [234, 88, 12],
|
|
||||||
MEDIUM: [234, 179, 8],
|
|
||||||
LOW: [34, 197, 94],
|
|
||||||
}
|
|
||||||
const color = severityColors[risk.severity] || [100, 100, 100]
|
|
||||||
|
|
||||||
doc.setFillColor(color[0], color[1], color[2])
|
|
||||||
doc.rect(20, y - 3, 3, 30, 'F')
|
|
||||||
|
|
||||||
doc.setFontSize(11)
|
|
||||||
doc.setTextColor(40)
|
|
||||||
doc.setFont('helvetica', 'bold')
|
|
||||||
doc.text(`${idx + 1}. ${risk.title}`, 28, y + 5)
|
|
||||||
doc.setFont('helvetica', 'normal')
|
|
||||||
|
|
||||||
doc.setFontSize(9)
|
|
||||||
doc.setTextColor(100)
|
|
||||||
doc.text(`${LABELS_DE.severity}: ${risk.severity} | ${LABELS_DE.category}: ${risk.category}`, 28, y + 13)
|
|
||||||
|
|
||||||
if (risk.description) {
|
|
||||||
y = addText(doc, risk.description, 28, y + 21, 155)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (risk.mitigation && risk.mitigation.length > 0) {
|
|
||||||
y += 5
|
|
||||||
doc.setFontSize(9)
|
|
||||||
doc.setTextColor(34, 197, 94)
|
|
||||||
doc.text(`${LABELS_DE.mitigation}: ${risk.mitigation.join(', ')}`, 28, y)
|
|
||||||
}
|
|
||||||
|
|
||||||
y += 15
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==========================================================================
|
|
||||||
// Controls
|
|
||||||
// ==========================================================================
|
|
||||||
|
|
||||||
doc.addPage()
|
|
||||||
y = 30
|
|
||||||
y = addSectionTitle(doc, LABELS_DE.controls, y)
|
|
||||||
|
|
||||||
if (state.controls.length === 0) {
|
|
||||||
y = addText(doc, LABELS_DE.noData, 25, y)
|
|
||||||
} else {
|
|
||||||
state.controls.forEach((ctrl, idx) => {
|
|
||||||
y = checkPageBreak(doc, y, 35)
|
|
||||||
|
|
||||||
doc.setFillColor(249, 250, 251)
|
|
||||||
doc.roundedRect(20, y - 5, pageWidth - 40, 28, 2, 2, 'F')
|
|
||||||
|
|
||||||
doc.setFontSize(10)
|
|
||||||
doc.setTextColor(40)
|
|
||||||
doc.setFont('helvetica', 'bold')
|
|
||||||
doc.text(`${idx + 1}. ${ctrl.name}`, 25, y + 5)
|
|
||||||
doc.setFont('helvetica', 'normal')
|
|
||||||
|
|
||||||
doc.setFontSize(9)
|
|
||||||
doc.setTextColor(100)
|
|
||||||
doc.text(`${LABELS_DE.category}: ${ctrl.category} | ${LABELS_DE.implementation}: ${ctrl.implementationStatus || 'Nicht definiert'}`, 25, y + 13)
|
|
||||||
|
|
||||||
if (ctrl.description) {
|
|
||||||
y = addText(doc, ctrl.description.substring(0, 150) + (ctrl.description.length > 150 ? '...' : ''), 25, y + 20, 160)
|
|
||||||
}
|
|
||||||
|
|
||||||
y += 35
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==========================================================================
|
|
||||||
// Checkpoints
|
|
||||||
// ==========================================================================
|
|
||||||
|
|
||||||
doc.addPage()
|
|
||||||
y = 30
|
|
||||||
y = addSectionTitle(doc, LABELS_DE.checkpoints, y)
|
|
||||||
|
|
||||||
const checkpointIds = Object.keys(state.checkpoints)
|
|
||||||
|
|
||||||
if (checkpointIds.length === 0) {
|
|
||||||
y = addText(doc, LABELS_DE.noData, 25, y)
|
|
||||||
} else {
|
|
||||||
checkpointIds.forEach((cpId) => {
|
|
||||||
const cp = state.checkpoints[cpId]
|
|
||||||
y = checkPageBreak(doc, y, 25)
|
|
||||||
|
|
||||||
const statusColor = cp.passed ? [34, 197, 94] : [220, 38, 38]
|
|
||||||
doc.setFillColor(statusColor[0], statusColor[1], statusColor[2])
|
|
||||||
doc.circle(25, y + 2, 3, 'F')
|
|
||||||
|
|
||||||
doc.setFontSize(10)
|
|
||||||
doc.setTextColor(40)
|
|
||||||
doc.text(cpId, 35, y + 5)
|
|
||||||
|
|
||||||
doc.setFontSize(9)
|
|
||||||
doc.setTextColor(100)
|
|
||||||
doc.text(`${LABELS_DE.status}: ${cp.passed ? LABELS_DE.completed : LABELS_DE.pending}`, 35, y + 12)
|
|
||||||
|
|
||||||
if (cp.errors && cp.errors.length > 0) {
|
|
||||||
doc.setTextColor(220, 38, 38)
|
|
||||||
doc.text(`Fehler: ${cp.errors.map(e => e.message).join(', ')}`, 35, y + 19)
|
|
||||||
y += 7
|
|
||||||
}
|
|
||||||
|
|
||||||
y += 20
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==========================================================================
|
|
||||||
// Add page numbers
|
|
||||||
// ==========================================================================
|
|
||||||
|
|
||||||
const pageCount = doc.getNumberOfPages()
|
|
||||||
for (let i = 1; i <= pageCount; i++) {
|
|
||||||
doc.setPage(i)
|
|
||||||
if (i > 1) {
|
|
||||||
addHeader(doc, LABELS_DE.title, i, pageCount)
|
|
||||||
}
|
|
||||||
addFooter(doc, state)
|
|
||||||
}
|
|
||||||
|
|
||||||
return doc.output('blob')
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// ZIP EXPORT
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
export async function exportToZIP(state: SDKState, options: ExportOptions = {}): Promise<Blob> {
|
|
||||||
const opts = { ...DEFAULT_OPTIONS, ...options }
|
|
||||||
const zip = new JSZip()
|
|
||||||
|
|
||||||
// Create folder structure
|
|
||||||
const rootFolder = zip.folder('ai-compliance-sdk-export')
|
|
||||||
if (!rootFolder) throw new Error('Failed to create ZIP folder')
|
|
||||||
|
|
||||||
const phase1Folder = rootFolder.folder('phase1-assessment')
|
|
||||||
const phase2Folder = rootFolder.folder('phase2-documents')
|
|
||||||
const dataFolder = rootFolder.folder('data')
|
|
||||||
|
|
||||||
// ==========================================================================
|
|
||||||
// Main State JSON
|
|
||||||
// ==========================================================================
|
|
||||||
|
|
||||||
if (opts.includeRawData && dataFolder) {
|
|
||||||
dataFolder.file('state.json', JSON.stringify(state, null, 2))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==========================================================================
|
|
||||||
// README
|
|
||||||
// ==========================================================================
|
|
||||||
|
|
||||||
const readmeContent = `# AI Compliance SDK Export
|
|
||||||
|
|
||||||
Generated: ${formatDate(new Date())}
|
|
||||||
Tenant: ${state.tenantId}
|
|
||||||
Version: ${state.version}
|
|
||||||
|
|
||||||
## Folder Structure
|
|
||||||
|
|
||||||
- **phase1-assessment/**: Compliance Assessment Ergebnisse
|
|
||||||
- use-cases.json: Alle Use Cases
|
|
||||||
- risks.json: Identifizierte Risiken
|
|
||||||
- controls.json: Definierte Controls
|
|
||||||
- requirements.json: Compliance-Anforderungen
|
|
||||||
|
|
||||||
- **phase2-documents/**: Generierte Dokumente
|
|
||||||
- dsfa.json: Datenschutz-Folgenabschaetzung
|
|
||||||
- toms.json: Technische und organisatorische Massnahmen
|
|
||||||
- vvt.json: Verarbeitungsverzeichnis
|
|
||||||
- documents.json: Rechtliche Dokumente
|
|
||||||
|
|
||||||
- **data/**: Rohdaten
|
|
||||||
- state.json: Kompletter SDK State
|
|
||||||
|
|
||||||
## Progress
|
|
||||||
|
|
||||||
Phase 1: ${SDK_STEPS.filter(s => s.phase === 1 && state.completedSteps.includes(s.id)).length}/${SDK_STEPS.filter(s => s.phase === 1).length} completed
|
|
||||||
Phase 2: ${SDK_STEPS.filter(s => s.phase === 2 && state.completedSteps.includes(s.id)).length}/${SDK_STEPS.filter(s => s.phase === 2).length} completed
|
|
||||||
|
|
||||||
## Key Metrics
|
|
||||||
|
|
||||||
- Use Cases: ${state.useCases.length}
|
|
||||||
- Risks: ${state.risks.length}
|
|
||||||
- Controls: ${state.controls.length}
|
|
||||||
- Requirements: ${state.requirements.length}
|
|
||||||
- Evidence: ${state.evidence.length}
|
|
||||||
`
|
|
||||||
|
|
||||||
rootFolder.file('README.md', readmeContent)
|
|
||||||
|
|
||||||
// ==========================================================================
|
|
||||||
// Phase 1 Files
|
|
||||||
// ==========================================================================
|
|
||||||
|
|
||||||
if (phase1Folder) {
|
|
||||||
// Use Cases
|
|
||||||
phase1Folder.file('use-cases.json', JSON.stringify({
|
|
||||||
exportedAt: new Date().toISOString(),
|
|
||||||
count: state.useCases.length,
|
|
||||||
useCases: state.useCases,
|
|
||||||
}, null, 2))
|
|
||||||
|
|
||||||
// Risks
|
|
||||||
phase1Folder.file('risks.json', JSON.stringify({
|
|
||||||
exportedAt: new Date().toISOString(),
|
|
||||||
count: state.risks.length,
|
|
||||||
risks: state.risks,
|
|
||||||
summary: {
|
|
||||||
critical: state.risks.filter(r => r.severity === 'CRITICAL').length,
|
|
||||||
high: state.risks.filter(r => r.severity === 'HIGH').length,
|
|
||||||
medium: state.risks.filter(r => r.severity === 'MEDIUM').length,
|
|
||||||
low: state.risks.filter(r => r.severity === 'LOW').length,
|
|
||||||
},
|
|
||||||
}, null, 2))
|
|
||||||
|
|
||||||
// Controls
|
|
||||||
phase1Folder.file('controls.json', JSON.stringify({
|
|
||||||
exportedAt: new Date().toISOString(),
|
|
||||||
count: state.controls.length,
|
|
||||||
controls: state.controls,
|
|
||||||
}, null, 2))
|
|
||||||
|
|
||||||
// Requirements
|
|
||||||
phase1Folder.file('requirements.json', JSON.stringify({
|
|
||||||
exportedAt: new Date().toISOString(),
|
|
||||||
count: state.requirements.length,
|
|
||||||
requirements: state.requirements,
|
|
||||||
}, null, 2))
|
|
||||||
|
|
||||||
// Modules
|
|
||||||
phase1Folder.file('modules.json', JSON.stringify({
|
|
||||||
exportedAt: new Date().toISOString(),
|
|
||||||
count: state.modules.length,
|
|
||||||
modules: state.modules,
|
|
||||||
}, null, 2))
|
|
||||||
|
|
||||||
// Evidence
|
|
||||||
if (opts.includeEvidence) {
|
|
||||||
phase1Folder.file('evidence.json', JSON.stringify({
|
|
||||||
exportedAt: new Date().toISOString(),
|
|
||||||
count: state.evidence.length,
|
|
||||||
evidence: state.evidence,
|
|
||||||
}, null, 2))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checkpoints
|
|
||||||
phase1Folder.file('checkpoints.json', JSON.stringify({
|
|
||||||
exportedAt: new Date().toISOString(),
|
|
||||||
checkpoints: state.checkpoints,
|
|
||||||
}, null, 2))
|
|
||||||
|
|
||||||
// Screening
|
|
||||||
if (state.screening) {
|
|
||||||
phase1Folder.file('screening.json', JSON.stringify({
|
|
||||||
exportedAt: new Date().toISOString(),
|
|
||||||
screening: state.screening,
|
|
||||||
}, null, 2))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==========================================================================
|
|
||||||
// Phase 2 Files
|
|
||||||
// ==========================================================================
|
|
||||||
|
|
||||||
if (phase2Folder) {
|
|
||||||
// DSFA
|
|
||||||
if (state.dsfa) {
|
|
||||||
phase2Folder.file('dsfa.json', JSON.stringify({
|
|
||||||
exportedAt: new Date().toISOString(),
|
|
||||||
dsfa: state.dsfa,
|
|
||||||
}, null, 2))
|
|
||||||
}
|
|
||||||
|
|
||||||
// TOMs
|
|
||||||
phase2Folder.file('toms.json', JSON.stringify({
|
|
||||||
exportedAt: new Date().toISOString(),
|
|
||||||
count: state.toms.length,
|
|
||||||
toms: state.toms,
|
|
||||||
}, null, 2))
|
|
||||||
|
|
||||||
// VVT (Processing Activities)
|
|
||||||
phase2Folder.file('vvt.json', JSON.stringify({
|
|
||||||
exportedAt: new Date().toISOString(),
|
|
||||||
count: state.vvt.length,
|
|
||||||
processingActivities: state.vvt,
|
|
||||||
}, null, 2))
|
|
||||||
|
|
||||||
// Legal Documents
|
|
||||||
if (opts.includeDocuments) {
|
|
||||||
phase2Folder.file('documents.json', JSON.stringify({
|
|
||||||
exportedAt: new Date().toISOString(),
|
|
||||||
count: state.documents.length,
|
|
||||||
documents: state.documents,
|
|
||||||
}, null, 2))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cookie Banner Config
|
|
||||||
if (state.cookieBanner) {
|
|
||||||
phase2Folder.file('cookie-banner.json', JSON.stringify({
|
|
||||||
exportedAt: new Date().toISOString(),
|
|
||||||
config: state.cookieBanner,
|
|
||||||
}, null, 2))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retention Policies
|
|
||||||
phase2Folder.file('retention-policies.json', JSON.stringify({
|
|
||||||
exportedAt: new Date().toISOString(),
|
|
||||||
count: state.retentionPolicies.length,
|
|
||||||
policies: state.retentionPolicies,
|
|
||||||
}, null, 2))
|
|
||||||
|
|
||||||
// AI Act Classification
|
|
||||||
if (state.aiActClassification) {
|
|
||||||
phase2Folder.file('ai-act-classification.json', JSON.stringify({
|
|
||||||
exportedAt: new Date().toISOString(),
|
|
||||||
classification: state.aiActClassification,
|
|
||||||
}, null, 2))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Obligations
|
|
||||||
phase2Folder.file('obligations.json', JSON.stringify({
|
|
||||||
exportedAt: new Date().toISOString(),
|
|
||||||
count: state.obligations.length,
|
|
||||||
obligations: state.obligations,
|
|
||||||
}, null, 2))
|
|
||||||
|
|
||||||
// Consent Records
|
|
||||||
phase2Folder.file('consents.json', JSON.stringify({
|
|
||||||
exportedAt: new Date().toISOString(),
|
|
||||||
count: state.consents.length,
|
|
||||||
consents: state.consents,
|
|
||||||
}, null, 2))
|
|
||||||
|
|
||||||
// DSR Config
|
|
||||||
if (state.dsrConfig) {
|
|
||||||
phase2Folder.file('dsr-config.json', JSON.stringify({
|
|
||||||
exportedAt: new Date().toISOString(),
|
|
||||||
config: state.dsrConfig,
|
|
||||||
}, null, 2))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Escalation Workflows
|
|
||||||
phase2Folder.file('escalation-workflows.json', JSON.stringify({
|
|
||||||
exportedAt: new Date().toISOString(),
|
|
||||||
count: state.escalationWorkflows.length,
|
|
||||||
workflows: state.escalationWorkflows,
|
|
||||||
}, null, 2))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==========================================================================
|
|
||||||
// Security Data
|
|
||||||
// ==========================================================================
|
|
||||||
|
|
||||||
if (dataFolder) {
|
|
||||||
if (state.sbom) {
|
|
||||||
dataFolder.file('sbom.json', JSON.stringify({
|
|
||||||
exportedAt: new Date().toISOString(),
|
|
||||||
sbom: state.sbom,
|
|
||||||
}, null, 2))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.securityIssues.length > 0) {
|
|
||||||
dataFolder.file('security-issues.json', JSON.stringify({
|
|
||||||
exportedAt: new Date().toISOString(),
|
|
||||||
count: state.securityIssues.length,
|
|
||||||
issues: state.securityIssues,
|
|
||||||
}, null, 2))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.securityBacklog.length > 0) {
|
|
||||||
dataFolder.file('security-backlog.json', JSON.stringify({
|
|
||||||
exportedAt: new Date().toISOString(),
|
|
||||||
count: state.securityBacklog.length,
|
|
||||||
backlog: state.securityBacklog,
|
|
||||||
}, null, 2))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==========================================================================
|
|
||||||
// Generate PDF and include in ZIP
|
|
||||||
// ==========================================================================
|
|
||||||
|
|
||||||
try {
|
|
||||||
const pdfBlob = await exportToPDF(state, options)
|
|
||||||
const pdfArrayBuffer = await pdfBlob.arrayBuffer()
|
|
||||||
rootFolder.file('compliance-report.pdf', pdfArrayBuffer)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to generate PDF for ZIP:', error)
|
|
||||||
// Continue without PDF
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate ZIP
|
|
||||||
return zip.generateAsync({ type: 'blob', compression: 'DEFLATE' })
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// EXPORT HELPER
|
// EXPORT HELPER
|
||||||
@@ -714,7 +15,7 @@ Phase 2: ${SDK_STEPS.filter(s => s.phase === 2 && state.completedSteps.includes(
|
|||||||
export async function downloadExport(
|
export async function downloadExport(
|
||||||
state: SDKState,
|
state: SDKState,
|
||||||
format: 'json' | 'pdf' | 'zip',
|
format: 'json' | 'pdf' | 'zip',
|
||||||
options: ExportOptions = {}
|
options: import('./export-pdf').ExportOptions = {}
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
let blob: Blob
|
let blob: Blob
|
||||||
let filename: string
|
let filename: string
|
||||||
@@ -727,15 +28,19 @@ export async function downloadExport(
|
|||||||
filename = `ai-compliance-sdk-${timestamp}.json`
|
filename = `ai-compliance-sdk-${timestamp}.json`
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'pdf':
|
case 'pdf': {
|
||||||
|
const { exportToPDF } = await import('./export-pdf')
|
||||||
blob = await exportToPDF(state, options)
|
blob = await exportToPDF(state, options)
|
||||||
filename = `ai-compliance-sdk-${timestamp}.pdf`
|
filename = `ai-compliance-sdk-${timestamp}.pdf`
|
||||||
break
|
break
|
||||||
|
}
|
||||||
|
|
||||||
case 'zip':
|
case 'zip': {
|
||||||
|
const { exportToZIP } = await import('./export-zip')
|
||||||
blob = await exportToZIP(state, options)
|
blob = await exportToZIP(state, options)
|
||||||
filename = `ai-compliance-sdk-${timestamp}.zip`
|
filename = `ai-compliance-sdk-${timestamp}.zip`
|
||||||
break
|
break
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown export format: ${format}`)
|
throw new Error(`Unknown export format: ${format}`)
|
||||||
|
|||||||
83
admin-compliance/lib/sdk/incidents/api-helpers.ts
Normal file
83
admin-compliance/lib/sdk/incidents/api-helpers.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
/**
|
||||||
|
* Incident API - Shared configuration and helper functions
|
||||||
|
*/
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// CONFIGURATION
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export const INCIDENTS_API_BASE = process.env.NEXT_PUBLIC_SDK_API_URL || 'http://localhost:8093'
|
||||||
|
export const API_TIMEOUT = 30000 // 30 seconds
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// HELPER FUNCTIONS
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export function getTenantId(): string {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
return localStorage.getItem('bp_tenant_id') || 'default-tenant'
|
||||||
|
}
|
||||||
|
return 'default-tenant'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAuthHeaders(): HeadersInit {
|
||||||
|
const headers: HeadersInit = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Tenant-ID': getTenantId()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const token = localStorage.getItem('authToken')
|
||||||
|
if (token) {
|
||||||
|
headers['Authorization'] = `Bearer ${token}`
|
||||||
|
}
|
||||||
|
const userId = localStorage.getItem('bp_user_id')
|
||||||
|
if (userId) {
|
||||||
|
headers['X-User-ID'] = userId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
372
admin-compliance/lib/sdk/incidents/api-incidents.ts
Normal file
372
admin-compliance/lib/sdk/incidents/api-incidents.ts
Normal file
@@ -0,0 +1,372 @@
|
|||||||
|
/**
|
||||||
|
* Incident CRUD, Risk Assessment, Notifications, Measures, Timeline, Statistics
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
Incident,
|
||||||
|
IncidentListResponse,
|
||||||
|
IncidentFilters,
|
||||||
|
IncidentCreateRequest,
|
||||||
|
IncidentUpdateRequest,
|
||||||
|
IncidentStatistics,
|
||||||
|
IncidentMeasure,
|
||||||
|
TimelineEntry,
|
||||||
|
RiskAssessmentRequest,
|
||||||
|
AuthorityNotification,
|
||||||
|
DataSubjectNotification,
|
||||||
|
IncidentSeverity,
|
||||||
|
IncidentStatus,
|
||||||
|
IncidentCategory,
|
||||||
|
} from './types'
|
||||||
|
|
||||||
|
import { INCIDENTS_API_BASE, fetchWithTimeout, getAuthHeaders } from './api-helpers'
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// INCIDENT LIST & CRUD
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alle Vorfaelle abrufen mit optionalen Filtern
|
||||||
|
*/
|
||||||
|
export async function fetchIncidents(filters?: IncidentFilters): Promise<IncidentListResponse> {
|
||||||
|
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.severity) {
|
||||||
|
const severities = Array.isArray(filters.severity) ? filters.severity : [filters.severity]
|
||||||
|
severities.forEach(s => params.append('severity', s))
|
||||||
|
}
|
||||||
|
if (filters.category) {
|
||||||
|
const categories = Array.isArray(filters.category) ? filters.category : [filters.category]
|
||||||
|
categories.forEach(c => params.append('category', c))
|
||||||
|
}
|
||||||
|
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 = `${INCIDENTS_API_BASE}/api/v1/incidents${queryString ? `?${queryString}` : ''}`
|
||||||
|
|
||||||
|
return fetchWithTimeout<IncidentListResponse>(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Einzelnen Vorfall per ID abrufen
|
||||||
|
*/
|
||||||
|
export async function fetchIncident(id: string): Promise<Incident> {
|
||||||
|
return fetchWithTimeout<Incident>(`${INCIDENTS_API_BASE}/api/v1/incidents/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Neuen Vorfall erstellen
|
||||||
|
*/
|
||||||
|
export async function createIncident(request: IncidentCreateRequest): Promise<Incident> {
|
||||||
|
return fetchWithTimeout<Incident>(`${INCIDENTS_API_BASE}/api/v1/incidents`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(request)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vorfall aktualisieren
|
||||||
|
*/
|
||||||
|
export async function updateIncident(id: string, update: IncidentUpdateRequest): Promise<Incident> {
|
||||||
|
return fetchWithTimeout<Incident>(`${INCIDENTS_API_BASE}/api/v1/incidents/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify(update)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vorfall loeschen (Soft Delete)
|
||||||
|
*/
|
||||||
|
export async function deleteIncident(id: string): Promise<void> {
|
||||||
|
await fetchWithTimeout<void>(`${INCIDENTS_API_BASE}/api/v1/incidents/${id}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// RISK ASSESSMENT
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Risikobewertung fuer einen Vorfall durchfuehren (Art. 33 DSGVO)
|
||||||
|
*/
|
||||||
|
export async function submitRiskAssessment(
|
||||||
|
incidentId: string,
|
||||||
|
assessment: RiskAssessmentRequest
|
||||||
|
): Promise<Incident> {
|
||||||
|
return fetchWithTimeout<Incident>(
|
||||||
|
`${INCIDENTS_API_BASE}/api/v1/incidents/${incidentId}/risk-assessment`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(assessment)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// AUTHORITY NOTIFICATION (Art. 33 DSGVO)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Meldeformular fuer die Aufsichtsbehoerde generieren
|
||||||
|
*/
|
||||||
|
export async function generateAuthorityForm(incidentId: string): Promise<Blob> {
|
||||||
|
const response = await fetch(
|
||||||
|
`${INCIDENTS_API_BASE}/api/v1/incidents/${incidentId}/authority-form/pdf`,
|
||||||
|
{
|
||||||
|
headers: getAuthHeaders()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`PDF-Generierung fehlgeschlagen: ${response.statusText}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.blob()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Meldung an die Aufsichtsbehoerde einreichen (Art. 33 DSGVO)
|
||||||
|
*/
|
||||||
|
export async function submitAuthorityNotification(
|
||||||
|
incidentId: string,
|
||||||
|
data: Partial<AuthorityNotification>
|
||||||
|
): Promise<Incident> {
|
||||||
|
return fetchWithTimeout<Incident>(
|
||||||
|
`${INCIDENTS_API_BASE}/api/v1/incidents/${incidentId}/authority-notification`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// DATA SUBJECT NOTIFICATION (Art. 34 DSGVO)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Betroffene Personen benachrichtigen (Art. 34 DSGVO)
|
||||||
|
*/
|
||||||
|
export async function sendDataSubjectNotification(
|
||||||
|
incidentId: string,
|
||||||
|
data: Partial<DataSubjectNotification>
|
||||||
|
): Promise<Incident> {
|
||||||
|
return fetchWithTimeout<Incident>(
|
||||||
|
`${INCIDENTS_API_BASE}/api/v1/incidents/${incidentId}/data-subject-notification`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// MEASURES (Massnahmen)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Massnahme hinzufuegen (Sofort-, Korrektur- oder Praeventionsmassnahme)
|
||||||
|
*/
|
||||||
|
export async function addMeasure(
|
||||||
|
incidentId: string,
|
||||||
|
measure: Omit<IncidentMeasure, 'id' | 'incidentId'>
|
||||||
|
): Promise<Incident> {
|
||||||
|
return fetchWithTimeout<Incident>(
|
||||||
|
`${INCIDENTS_API_BASE}/api/v1/incidents/${incidentId}/measures`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(measure)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Massnahme aktualisieren
|
||||||
|
*/
|
||||||
|
export async function updateMeasure(
|
||||||
|
measureId: string,
|
||||||
|
update: Partial<IncidentMeasure>
|
||||||
|
): Promise<IncidentMeasure> {
|
||||||
|
return fetchWithTimeout<IncidentMeasure>(
|
||||||
|
`${INCIDENTS_API_BASE}/api/v1/measures/${measureId}`,
|
||||||
|
{
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify(update)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Massnahme als abgeschlossen markieren
|
||||||
|
*/
|
||||||
|
export async function completeMeasure(measureId: string): Promise<IncidentMeasure> {
|
||||||
|
return fetchWithTimeout<IncidentMeasure>(
|
||||||
|
`${INCIDENTS_API_BASE}/api/v1/measures/${measureId}/complete`,
|
||||||
|
{
|
||||||
|
method: 'POST'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// TIMELINE
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zeitleisteneintrag hinzufuegen
|
||||||
|
*/
|
||||||
|
export async function addTimelineEntry(
|
||||||
|
incidentId: string,
|
||||||
|
entry: Omit<TimelineEntry, 'id' | 'incidentId'>
|
||||||
|
): Promise<Incident> {
|
||||||
|
return fetchWithTimeout<Incident>(
|
||||||
|
`${INCIDENTS_API_BASE}/api/v1/incidents/${incidentId}/timeline`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(entry)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// CLOSE INCIDENT
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vorfall abschliessen mit Lessons Learned
|
||||||
|
*/
|
||||||
|
export async function closeIncident(
|
||||||
|
incidentId: string,
|
||||||
|
lessonsLearned: string
|
||||||
|
): Promise<Incident> {
|
||||||
|
return fetchWithTimeout<Incident>(
|
||||||
|
`${INCIDENTS_API_BASE}/api/v1/incidents/${incidentId}/close`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ lessonsLearned })
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// STATISTICS
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vorfall-Statistiken abrufen
|
||||||
|
*/
|
||||||
|
export async function fetchIncidentStatistics(): Promise<IncidentStatistics> {
|
||||||
|
return fetchWithTimeout<IncidentStatistics>(
|
||||||
|
`${INCIDENTS_API_BASE}/api/v1/incidents/statistics`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// SDK PROXY FUNCTION (mit Fallback auf Mock-Daten)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch Incident-Liste via SDK-Proxy mit Fallback auf Mock-Daten
|
||||||
|
*/
|
||||||
|
export async function fetchSDKIncidentList(): Promise<{ incidents: Incident[]; statistics: IncidentStatistics }> {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/sdk/v1/incidents', {
|
||||||
|
headers: getAuthHeaders()
|
||||||
|
})
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`HTTP ${res.status}`)
|
||||||
|
}
|
||||||
|
const data = await res.json()
|
||||||
|
const incidents: Incident[] = data.incidents || []
|
||||||
|
|
||||||
|
const statistics = computeStatistics(incidents)
|
||||||
|
return { incidents, statistics }
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('SDK-Backend nicht erreichbar, verwende Mock-Daten:', error)
|
||||||
|
// Import mock data lazily to keep this file lean
|
||||||
|
const { createMockIncidents, createMockStatistics } = await import('./api-mock')
|
||||||
|
const incidents = createMockIncidents()
|
||||||
|
const statistics = createMockStatistics()
|
||||||
|
return { incidents, statistics }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Statistiken lokal aus Incident-Liste berechnen
|
||||||
|
*/
|
||||||
|
function computeStatistics(incidents: Incident[]): IncidentStatistics {
|
||||||
|
const countBy = <K extends string>(items: { [key: string]: unknown }[], field: string): Record<K, number> => {
|
||||||
|
const result: Record<string, number> = {}
|
||||||
|
items.forEach(item => {
|
||||||
|
const key = String(item[field])
|
||||||
|
result[key] = (result[key] || 0) + 1
|
||||||
|
})
|
||||||
|
return result as Record<K, number>
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusCounts = countBy<IncidentStatus>(incidents as unknown as { [key: string]: unknown }[], 'status')
|
||||||
|
const severityCounts = countBy<IncidentSeverity>(incidents as unknown as { [key: string]: unknown }[], 'severity')
|
||||||
|
const categoryCounts = countBy<IncidentCategory>(incidents as unknown as { [key: string]: unknown }[], 'category')
|
||||||
|
|
||||||
|
const openIncidents = incidents.filter(i => i.status !== 'closed').length
|
||||||
|
const notificationsPending = incidents.filter(i =>
|
||||||
|
i.authorityNotification !== null &&
|
||||||
|
i.authorityNotification.status === 'pending' &&
|
||||||
|
i.status !== 'closed'
|
||||||
|
).length
|
||||||
|
|
||||||
|
let totalResponseHours = 0
|
||||||
|
let respondedCount = 0
|
||||||
|
incidents.forEach(i => {
|
||||||
|
if (i.riskAssessment && i.riskAssessment.assessedAt) {
|
||||||
|
const detected = new Date(i.detectedAt).getTime()
|
||||||
|
const assessed = new Date(i.riskAssessment.assessedAt).getTime()
|
||||||
|
totalResponseHours += (assessed - detected) / (1000 * 60 * 60)
|
||||||
|
respondedCount++
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalIncidents: incidents.length,
|
||||||
|
openIncidents,
|
||||||
|
notificationsPending,
|
||||||
|
averageResponseTimeHours: respondedCount > 0 ? Math.round(totalResponseHours / respondedCount * 10) / 10 : 0,
|
||||||
|
bySeverity: {
|
||||||
|
low: severityCounts['low'] || 0,
|
||||||
|
medium: severityCounts['medium'] || 0,
|
||||||
|
high: severityCounts['high'] || 0,
|
||||||
|
critical: severityCounts['critical'] || 0
|
||||||
|
},
|
||||||
|
byCategory: {
|
||||||
|
data_breach: categoryCounts['data_breach'] || 0,
|
||||||
|
unauthorized_access: categoryCounts['unauthorized_access'] || 0,
|
||||||
|
data_loss: categoryCounts['data_loss'] || 0,
|
||||||
|
system_compromise: categoryCounts['system_compromise'] || 0,
|
||||||
|
phishing: categoryCounts['phishing'] || 0,
|
||||||
|
ransomware: categoryCounts['ransomware'] || 0,
|
||||||
|
insider_threat: categoryCounts['insider_threat'] || 0,
|
||||||
|
physical_breach: categoryCounts['physical_breach'] || 0,
|
||||||
|
other: categoryCounts['other'] || 0
|
||||||
|
},
|
||||||
|
byStatus: {
|
||||||
|
detected: statusCounts['detected'] || 0,
|
||||||
|
assessment: statusCounts['assessment'] || 0,
|
||||||
|
containment: statusCounts['containment'] || 0,
|
||||||
|
notification_required: statusCounts['notification_required'] || 0,
|
||||||
|
notification_sent: statusCounts['notification_sent'] || 0,
|
||||||
|
remediation: statusCounts['remediation'] || 0,
|
||||||
|
closed: statusCounts['closed'] || 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
392
admin-compliance/lib/sdk/incidents/api-mock.ts
Normal file
392
admin-compliance/lib/sdk/incidents/api-mock.ts
Normal file
@@ -0,0 +1,392 @@
|
|||||||
|
/**
|
||||||
|
* Incident Mock Data (Demo-Daten fuer Entwicklung und Tests)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
Incident,
|
||||||
|
IncidentStatistics,
|
||||||
|
} from './types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erstellt Demo-Vorfaelle fuer die Entwicklung
|
||||||
|
*/
|
||||||
|
export function createMockIncidents(): Incident[] {
|
||||||
|
const now = new Date()
|
||||||
|
|
||||||
|
return [
|
||||||
|
// 1. Gerade erkannt - noch nicht bewertet (detected/new)
|
||||||
|
{
|
||||||
|
id: 'inc-001',
|
||||||
|
referenceNumber: 'INC-2026-000001',
|
||||||
|
title: 'Unbefugter Zugriff auf Schuelerdatenbank',
|
||||||
|
description: 'Ein ehemaliger Mitarbeiter hat sich mit noch aktiven Zugangsdaten in die Schuelerdatenbank eingeloggt. Der Zugriff wurde durch die Log-Analyse entdeckt.',
|
||||||
|
category: 'unauthorized_access',
|
||||||
|
severity: 'high',
|
||||||
|
status: 'detected',
|
||||||
|
detectedAt: new Date(now.getTime() - 3 * 60 * 60 * 1000).toISOString(),
|
||||||
|
detectedBy: 'Log-Analyse (automatisiert)',
|
||||||
|
affectedSystems: ['Schuelerdatenbank', 'Schulverwaltungssystem'],
|
||||||
|
affectedDataCategories: ['Personenbezogene Daten', 'Daten von Kindern', 'Gesundheitsdaten'],
|
||||||
|
estimatedAffectedPersons: 800,
|
||||||
|
riskAssessment: null,
|
||||||
|
authorityNotification: null,
|
||||||
|
dataSubjectNotification: null,
|
||||||
|
measures: [],
|
||||||
|
timeline: [
|
||||||
|
{
|
||||||
|
id: 'tl-001',
|
||||||
|
incidentId: 'inc-001',
|
||||||
|
timestamp: new Date(now.getTime() - 3 * 60 * 60 * 1000).toISOString(),
|
||||||
|
action: 'Vorfall erkannt',
|
||||||
|
description: 'Automatische Log-Analyse meldet verdaechtigen Login eines deaktivierten Kontos',
|
||||||
|
performedBy: 'SIEM-System'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
assignedTo: undefined
|
||||||
|
},
|
||||||
|
|
||||||
|
// 2. In Bewertung (assessment) - Risikobewertung laeuft
|
||||||
|
{
|
||||||
|
id: 'inc-002',
|
||||||
|
referenceNumber: 'INC-2026-000002',
|
||||||
|
title: 'E-Mail mit Kundendaten an falschen Empfaenger',
|
||||||
|
description: 'Ein Mitarbeiter hat eine Excel-Datei mit Kundendaten (Name, Adresse, Vertragsnummer) an einen falschen E-Mail-Empfaenger gesendet. Der Empfaenger wurde kontaktiert und hat die Loeschung bestaetigt.',
|
||||||
|
category: 'data_breach',
|
||||||
|
severity: 'medium',
|
||||||
|
status: 'assessment',
|
||||||
|
detectedAt: new Date(now.getTime() - 18 * 60 * 60 * 1000).toISOString(),
|
||||||
|
detectedBy: 'Vertriebsabteilung',
|
||||||
|
affectedSystems: ['E-Mail-System (Exchange)'],
|
||||||
|
affectedDataCategories: ['Personenbezogene Daten', 'Kundendaten'],
|
||||||
|
estimatedAffectedPersons: 150,
|
||||||
|
riskAssessment: {
|
||||||
|
id: 'ra-002',
|
||||||
|
assessedBy: 'DSB Mueller',
|
||||||
|
assessedAt: new Date(now.getTime() - 12 * 60 * 60 * 1000).toISOString(),
|
||||||
|
likelihoodScore: 3,
|
||||||
|
impactScore: 2,
|
||||||
|
overallRisk: 'medium',
|
||||||
|
notificationRequired: false,
|
||||||
|
reasoning: 'Empfaenger hat Loeschung bestaetigt. Datenkategorie: allgemeine Kontaktdaten und Vertragsnummern. Geringes Risiko fuer betroffene Personen.'
|
||||||
|
},
|
||||||
|
authorityNotification: {
|
||||||
|
id: 'an-002',
|
||||||
|
authority: 'LfD Niedersachsen',
|
||||||
|
deadline72h: new Date(new Date(now.getTime() - 18 * 60 * 60 * 1000).getTime() + 72 * 60 * 60 * 1000).toISOString(),
|
||||||
|
status: 'pending',
|
||||||
|
formData: {}
|
||||||
|
},
|
||||||
|
dataSubjectNotification: null,
|
||||||
|
measures: [
|
||||||
|
{
|
||||||
|
id: 'meas-001',
|
||||||
|
incidentId: 'inc-002',
|
||||||
|
title: 'Empfaenger kontaktiert',
|
||||||
|
description: 'Falscher Empfaenger kontaktiert mit Bitte um Loeschung',
|
||||||
|
type: 'immediate',
|
||||||
|
status: 'completed',
|
||||||
|
responsible: 'Vertriebsleitung',
|
||||||
|
dueDate: new Date(now.getTime() - 16 * 60 * 60 * 1000).toISOString(),
|
||||||
|
completedAt: new Date(now.getTime() - 15 * 60 * 60 * 1000).toISOString()
|
||||||
|
}
|
||||||
|
],
|
||||||
|
timeline: [
|
||||||
|
{
|
||||||
|
id: 'tl-002',
|
||||||
|
incidentId: 'inc-002',
|
||||||
|
timestamp: new Date(now.getTime() - 18 * 60 * 60 * 1000).toISOString(),
|
||||||
|
action: 'Vorfall gemeldet',
|
||||||
|
description: 'Mitarbeiter meldet versehentlichen E-Mail-Versand',
|
||||||
|
performedBy: 'M. Schmidt (Vertrieb)'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tl-003',
|
||||||
|
incidentId: 'inc-002',
|
||||||
|
timestamp: new Date(now.getTime() - 15 * 60 * 60 * 1000).toISOString(),
|
||||||
|
action: 'Sofortmassnahme',
|
||||||
|
description: 'Empfaenger kontaktiert und Loeschung bestaetigt',
|
||||||
|
performedBy: 'Vertriebsleitung'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tl-004',
|
||||||
|
incidentId: 'inc-002',
|
||||||
|
timestamp: new Date(now.getTime() - 12 * 60 * 60 * 1000).toISOString(),
|
||||||
|
action: 'Risikobewertung',
|
||||||
|
description: 'Bewertung durchgefuehrt - mittleres Risiko, keine Meldepflicht',
|
||||||
|
performedBy: 'DSB Mueller'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
assignedTo: 'DSB Mueller'
|
||||||
|
},
|
||||||
|
|
||||||
|
// 3. Gemeldet (notification_sent) - Ransomware-Angriff
|
||||||
|
{
|
||||||
|
id: 'inc-003',
|
||||||
|
referenceNumber: 'INC-2026-000003',
|
||||||
|
title: 'Ransomware-Angriff auf Dateiserver',
|
||||||
|
description: 'Am Montagmorgen wurde ein Ransomware-Angriff auf den zentralen Dateiserver erkannt. Mehrere verschluesselte Dateien wurden identifiziert. Der Angriffsvektor war eine Phishing-E-Mail an einen Mitarbeiter.',
|
||||||
|
category: 'ransomware',
|
||||||
|
severity: 'critical',
|
||||||
|
status: 'notification_sent',
|
||||||
|
detectedAt: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
detectedBy: 'IT-Sicherheitsteam',
|
||||||
|
affectedSystems: ['Dateiserver (FS-01)', 'E-Mail-System', 'Backup-Server'],
|
||||||
|
affectedDataCategories: ['Personenbezogene Daten', 'Beschaeftigtendaten', 'Kundendaten', 'Finanzdaten'],
|
||||||
|
estimatedAffectedPersons: 2500,
|
||||||
|
riskAssessment: {
|
||||||
|
id: 'ra-003',
|
||||||
|
assessedBy: 'DSB Mueller',
|
||||||
|
assessedAt: new Date(now.getTime() - 4.5 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
likelihoodScore: 5,
|
||||||
|
impactScore: 5,
|
||||||
|
overallRisk: 'critical',
|
||||||
|
notificationRequired: true,
|
||||||
|
reasoning: 'Hohes Risiko fuer Rechte und Freiheiten der betroffenen Personen durch potentiellen Zugriff auf personenbezogene Daten und Finanzdaten. Verschluesselung betrifft Verfuegbarkeit, Exfiltration nicht auszuschliessen.'
|
||||||
|
},
|
||||||
|
authorityNotification: {
|
||||||
|
id: 'an-003',
|
||||||
|
authority: 'LfD Niedersachsen',
|
||||||
|
deadline72h: new Date(new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).getTime() + 72 * 60 * 60 * 1000).toISOString(),
|
||||||
|
submittedAt: new Date(now.getTime() - 4 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
status: 'submitted',
|
||||||
|
formData: {
|
||||||
|
referenceNumber: 'LfD-NI-2026-04821',
|
||||||
|
incidentType: 'Ransomware',
|
||||||
|
affectedPersons: 2500
|
||||||
|
},
|
||||||
|
pdfUrl: '/api/sdk/v1/incidents/inc-003/authority-form.pdf'
|
||||||
|
},
|
||||||
|
dataSubjectNotification: {
|
||||||
|
id: 'dsn-003',
|
||||||
|
notificationRequired: true,
|
||||||
|
templateText: 'Sehr geehrte Damen und Herren, wir informieren Sie ueber einen Sicherheitsvorfall, bei dem moeglicherweise Ihre personenbezogenen Daten betroffen sind...',
|
||||||
|
sentAt: new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
recipientCount: 2500,
|
||||||
|
method: 'email'
|
||||||
|
},
|
||||||
|
measures: [
|
||||||
|
{
|
||||||
|
id: 'meas-002',
|
||||||
|
incidentId: 'inc-003',
|
||||||
|
title: 'Netzwerksegmentierung',
|
||||||
|
description: 'Betroffene Systeme vom Netzwerk isoliert',
|
||||||
|
type: 'immediate',
|
||||||
|
status: 'completed',
|
||||||
|
responsible: 'IT-Sicherheitsteam',
|
||||||
|
dueDate: new Date(now.getTime() - 4.8 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
completedAt: new Date(now.getTime() - 4.9 * 24 * 60 * 60 * 1000).toISOString()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'meas-003',
|
||||||
|
incidentId: 'inc-003',
|
||||||
|
title: 'Passwoerter zuruecksetzen',
|
||||||
|
description: 'Alle Benutzerpasswoerter zurueckgesetzt',
|
||||||
|
type: 'immediate',
|
||||||
|
status: 'completed',
|
||||||
|
responsible: 'IT-Administration',
|
||||||
|
dueDate: new Date(now.getTime() - 4.5 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
completedAt: new Date(now.getTime() - 4.5 * 24 * 60 * 60 * 1000).toISOString()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'meas-004',
|
||||||
|
incidentId: 'inc-003',
|
||||||
|
title: 'E-Mail-Security Gateway implementieren',
|
||||||
|
description: 'Implementierung eines fortgeschrittenen E-Mail-Sicherheitsgateways mit Sandboxing',
|
||||||
|
type: 'preventive',
|
||||||
|
status: 'in_progress',
|
||||||
|
responsible: 'IT-Sicherheitsteam',
|
||||||
|
dueDate: new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000).toISOString()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'meas-005',
|
||||||
|
incidentId: 'inc-003',
|
||||||
|
title: 'Mitarbeiterschulung Phishing',
|
||||||
|
description: 'Verpflichtende Schulung fuer alle Mitarbeiter zum Thema Phishing-Erkennung',
|
||||||
|
type: 'preventive',
|
||||||
|
status: 'planned',
|
||||||
|
responsible: 'Personalwesen',
|
||||||
|
dueDate: new Date(now.getTime() + 60 * 24 * 60 * 60 * 1000).toISOString()
|
||||||
|
}
|
||||||
|
],
|
||||||
|
timeline: [
|
||||||
|
{
|
||||||
|
id: 'tl-005',
|
||||||
|
incidentId: 'inc-003',
|
||||||
|
timestamp: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
action: 'Vorfall erkannt',
|
||||||
|
description: 'IT-Sicherheitsteam erkennt ungewoehnliche Verschluesselungsaktivitaet',
|
||||||
|
performedBy: 'IT-Sicherheitsteam'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tl-006',
|
||||||
|
incidentId: 'inc-003',
|
||||||
|
timestamp: new Date(now.getTime() - 4.9 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
action: 'Eindaemmung gestartet',
|
||||||
|
description: 'Netzwerksegmentierung und Isolation betroffener Systeme',
|
||||||
|
performedBy: 'IT-Sicherheitsteam'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tl-007',
|
||||||
|
incidentId: 'inc-003',
|
||||||
|
timestamp: new Date(now.getTime() - 4.5 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
action: 'Risikobewertung abgeschlossen',
|
||||||
|
description: 'Kritisches Risiko festgestellt - Meldepflicht ausgeloest',
|
||||||
|
performedBy: 'DSB Mueller'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tl-008',
|
||||||
|
incidentId: 'inc-003',
|
||||||
|
timestamp: new Date(now.getTime() - 4 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
action: 'Behoerdenbenachrichtigung',
|
||||||
|
description: 'Meldung an LfD Niedersachsen eingereicht',
|
||||||
|
performedBy: 'DSB Mueller'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tl-009',
|
||||||
|
incidentId: 'inc-003',
|
||||||
|
timestamp: new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
action: 'Betroffene benachrichtigt',
|
||||||
|
description: '2.500 betroffene Personen per E-Mail informiert',
|
||||||
|
performedBy: 'Kommunikationsabteilung'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
assignedTo: 'DSB Mueller'
|
||||||
|
},
|
||||||
|
|
||||||
|
// 4. Abgeschlossener Vorfall (closed) - Phishing
|
||||||
|
{
|
||||||
|
id: 'inc-004',
|
||||||
|
referenceNumber: 'INC-2026-000004',
|
||||||
|
title: 'Phishing-Angriff auf Personalabteilung',
|
||||||
|
description: 'Gezielter Phishing-Angriff auf die Personalabteilung. Ein Mitarbeiter hat Zugangsdaten auf einer gefaelschten Login-Seite eingegeben. Das Konto wurde sofort gesperrt. Keine Datenexfiltration festgestellt.',
|
||||||
|
category: 'phishing',
|
||||||
|
severity: 'high',
|
||||||
|
status: 'closed',
|
||||||
|
detectedAt: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
detectedBy: 'IT-Sicherheitsteam (SIEM-Alert)',
|
||||||
|
affectedSystems: ['Active Directory', 'HR-Portal'],
|
||||||
|
affectedDataCategories: ['Beschaeftigtendaten', 'Personenbezogene Daten'],
|
||||||
|
estimatedAffectedPersons: 0,
|
||||||
|
riskAssessment: {
|
||||||
|
id: 'ra-004',
|
||||||
|
assessedBy: 'DSB Mueller',
|
||||||
|
assessedAt: new Date(now.getTime() - 29 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
likelihoodScore: 4,
|
||||||
|
impactScore: 3,
|
||||||
|
overallRisk: 'high',
|
||||||
|
notificationRequired: true,
|
||||||
|
reasoning: 'Zugangsdaten kompromittiert, potentieller Zugriff auf Personaldaten. Keine Exfiltration festgestellt, dennoch Meldung wegen Kompromittierung der Zugangsdaten.'
|
||||||
|
},
|
||||||
|
authorityNotification: {
|
||||||
|
id: 'an-004',
|
||||||
|
authority: 'LfD Niedersachsen',
|
||||||
|
deadline72h: new Date(new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).getTime() + 72 * 60 * 60 * 1000).toISOString(),
|
||||||
|
submittedAt: new Date(now.getTime() - 29 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
status: 'acknowledged',
|
||||||
|
formData: {
|
||||||
|
referenceNumber: 'LfD-NI-2026-03912',
|
||||||
|
incidentType: 'Phishing',
|
||||||
|
affectedPersons: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dataSubjectNotification: {
|
||||||
|
id: 'dsn-004',
|
||||||
|
notificationRequired: false,
|
||||||
|
templateText: '',
|
||||||
|
recipientCount: 0,
|
||||||
|
method: 'email'
|
||||||
|
},
|
||||||
|
measures: [
|
||||||
|
{
|
||||||
|
id: 'meas-006',
|
||||||
|
incidentId: 'inc-004',
|
||||||
|
title: 'Konto gesperrt',
|
||||||
|
description: 'Kompromittiertes Benutzerkonto sofort gesperrt',
|
||||||
|
type: 'immediate',
|
||||||
|
status: 'completed',
|
||||||
|
responsible: 'IT-Administration',
|
||||||
|
dueDate: new Date(now.getTime() - 29.8 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
completedAt: new Date(now.getTime() - 29.9 * 24 * 60 * 60 * 1000).toISOString()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'meas-007',
|
||||||
|
incidentId: 'inc-004',
|
||||||
|
title: 'MFA fuer alle Mitarbeiter',
|
||||||
|
description: 'Einfuehrung von Multi-Faktor-Authentifizierung fuer alle Konten',
|
||||||
|
type: 'preventive',
|
||||||
|
status: 'completed',
|
||||||
|
responsible: 'IT-Sicherheitsteam',
|
||||||
|
dueDate: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
completedAt: new Date(now.getTime() - 12 * 24 * 60 * 60 * 1000).toISOString()
|
||||||
|
}
|
||||||
|
],
|
||||||
|
timeline: [
|
||||||
|
{
|
||||||
|
id: 'tl-010',
|
||||||
|
incidentId: 'inc-004',
|
||||||
|
timestamp: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
action: 'SIEM-Alert',
|
||||||
|
description: 'Verdaechtiger Login-Versuch aus unbekannter Region erkannt',
|
||||||
|
performedBy: 'IT-Sicherheitsteam'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tl-011',
|
||||||
|
incidentId: 'inc-004',
|
||||||
|
timestamp: new Date(now.getTime() - 29 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
action: 'Behoerdenbenachrichtigung',
|
||||||
|
description: 'Meldung an LfD Niedersachsen',
|
||||||
|
performedBy: 'DSB Mueller'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tl-012',
|
||||||
|
incidentId: 'inc-004',
|
||||||
|
timestamp: new Date(now.getTime() - 15 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
action: 'Vorfall abgeschlossen',
|
||||||
|
description: 'Alle Massnahmen umgesetzt, keine Datenexfiltration festgestellt',
|
||||||
|
performedBy: 'DSB Mueller'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
assignedTo: 'DSB Mueller',
|
||||||
|
closedAt: new Date(now.getTime() - 15 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
|
lessonsLearned: '1. MFA haette den Zugriff verhindert (jetzt implementiert). 2. E-Mail-Security-Gateway muss verbesserte Phishing-Erkennung erhalten. 3. Regelmaessige Phishing-Simulationen fuer alle Mitarbeiter einfuehren.'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erstellt Mock-Statistiken fuer die Entwicklung
|
||||||
|
*/
|
||||||
|
export function createMockStatistics(): IncidentStatistics {
|
||||||
|
return {
|
||||||
|
totalIncidents: 4,
|
||||||
|
openIncidents: 3,
|
||||||
|
notificationsPending: 1,
|
||||||
|
averageResponseTimeHours: 8.5,
|
||||||
|
bySeverity: {
|
||||||
|
low: 0,
|
||||||
|
medium: 1,
|
||||||
|
high: 2,
|
||||||
|
critical: 1
|
||||||
|
},
|
||||||
|
byCategory: {
|
||||||
|
data_breach: 1,
|
||||||
|
unauthorized_access: 1,
|
||||||
|
data_loss: 0,
|
||||||
|
system_compromise: 0,
|
||||||
|
phishing: 1,
|
||||||
|
ransomware: 1,
|
||||||
|
insider_threat: 0,
|
||||||
|
physical_breach: 0,
|
||||||
|
other: 0
|
||||||
|
},
|
||||||
|
byStatus: {
|
||||||
|
detected: 1,
|
||||||
|
assessment: 1,
|
||||||
|
containment: 0,
|
||||||
|
notification_required: 0,
|
||||||
|
notification_sent: 1,
|
||||||
|
remediation: 0,
|
||||||
|
closed: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,843 +3,30 @@
|
|||||||
*
|
*
|
||||||
* API client for DSGVO Art. 33/34 Incident & Data Breach Management
|
* API client for DSGVO Art. 33/34 Incident & Data Breach Management
|
||||||
* Connects via Next.js proxy to the ai-compliance-sdk backend
|
* Connects via Next.js proxy to the ai-compliance-sdk backend
|
||||||
|
*
|
||||||
|
* Barrel re-export from split modules.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
export {
|
||||||
Incident,
|
fetchIncidents,
|
||||||
IncidentListResponse,
|
fetchIncident,
|
||||||
IncidentFilters,
|
createIncident,
|
||||||
IncidentCreateRequest,
|
updateIncident,
|
||||||
IncidentUpdateRequest,
|
deleteIncident,
|
||||||
IncidentStatistics,
|
submitRiskAssessment,
|
||||||
IncidentMeasure,
|
generateAuthorityForm,
|
||||||
TimelineEntry,
|
submitAuthorityNotification,
|
||||||
RiskAssessmentRequest,
|
sendDataSubjectNotification,
|
||||||
RiskAssessment,
|
addMeasure,
|
||||||
AuthorityNotification,
|
updateMeasure,
|
||||||
DataSubjectNotification,
|
completeMeasure,
|
||||||
IncidentSeverity,
|
addTimelineEntry,
|
||||||
IncidentStatus,
|
closeIncident,
|
||||||
IncidentCategory,
|
fetchIncidentStatistics,
|
||||||
calculateRiskLevel,
|
fetchSDKIncidentList,
|
||||||
isNotificationRequired,
|
} from './api-incidents'
|
||||||
get72hDeadline
|
|
||||||
} from './types'
|
|
||||||
|
|
||||||
// =============================================================================
|
export {
|
||||||
// CONFIGURATION
|
createMockIncidents,
|
||||||
// =============================================================================
|
createMockStatistics,
|
||||||
|
} from './api-mock'
|
||||||
const INCIDENTS_API_BASE = process.env.NEXT_PUBLIC_SDK_API_URL || 'http://localhost:8093'
|
|
||||||
const API_TIMEOUT = 30000 // 30 seconds
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// HELPER FUNCTIONS
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
function getTenantId(): string {
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
return localStorage.getItem('bp_tenant_id') || 'default-tenant'
|
|
||||||
}
|
|
||||||
return 'default-tenant'
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAuthHeaders(): HeadersInit {
|
|
||||||
const headers: HeadersInit = {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-Tenant-ID': getTenantId()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
const token = localStorage.getItem('authToken')
|
|
||||||
if (token) {
|
|
||||||
headers['Authorization'] = `Bearer ${token}`
|
|
||||||
}
|
|
||||||
const userId = localStorage.getItem('bp_user_id')
|
|
||||||
if (userId) {
|
|
||||||
headers['X-User-ID'] = userId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// INCIDENT LIST & CRUD
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Alle Vorfaelle abrufen mit optionalen Filtern
|
|
||||||
*/
|
|
||||||
export async function fetchIncidents(filters?: IncidentFilters): Promise<IncidentListResponse> {
|
|
||||||
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.severity) {
|
|
||||||
const severities = Array.isArray(filters.severity) ? filters.severity : [filters.severity]
|
|
||||||
severities.forEach(s => params.append('severity', s))
|
|
||||||
}
|
|
||||||
if (filters.category) {
|
|
||||||
const categories = Array.isArray(filters.category) ? filters.category : [filters.category]
|
|
||||||
categories.forEach(c => params.append('category', c))
|
|
||||||
}
|
|
||||||
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 = `${INCIDENTS_API_BASE}/api/v1/incidents${queryString ? `?${queryString}` : ''}`
|
|
||||||
|
|
||||||
return fetchWithTimeout<IncidentListResponse>(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Einzelnen Vorfall per ID abrufen
|
|
||||||
*/
|
|
||||||
export async function fetchIncident(id: string): Promise<Incident> {
|
|
||||||
return fetchWithTimeout<Incident>(`${INCIDENTS_API_BASE}/api/v1/incidents/${id}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Neuen Vorfall erstellen
|
|
||||||
*/
|
|
||||||
export async function createIncident(request: IncidentCreateRequest): Promise<Incident> {
|
|
||||||
return fetchWithTimeout<Incident>(`${INCIDENTS_API_BASE}/api/v1/incidents`, {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify(request)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vorfall aktualisieren
|
|
||||||
*/
|
|
||||||
export async function updateIncident(id: string, update: IncidentUpdateRequest): Promise<Incident> {
|
|
||||||
return fetchWithTimeout<Incident>(`${INCIDENTS_API_BASE}/api/v1/incidents/${id}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
body: JSON.stringify(update)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vorfall loeschen (Soft Delete)
|
|
||||||
*/
|
|
||||||
export async function deleteIncident(id: string): Promise<void> {
|
|
||||||
await fetchWithTimeout<void>(`${INCIDENTS_API_BASE}/api/v1/incidents/${id}`, {
|
|
||||||
method: 'DELETE'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// RISK ASSESSMENT
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Risikobewertung fuer einen Vorfall durchfuehren (Art. 33 DSGVO)
|
|
||||||
*/
|
|
||||||
export async function submitRiskAssessment(
|
|
||||||
incidentId: string,
|
|
||||||
assessment: RiskAssessmentRequest
|
|
||||||
): Promise<Incident> {
|
|
||||||
return fetchWithTimeout<Incident>(
|
|
||||||
`${INCIDENTS_API_BASE}/api/v1/incidents/${incidentId}/risk-assessment`,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify(assessment)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// AUTHORITY NOTIFICATION (Art. 33 DSGVO)
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Meldeformular fuer die Aufsichtsbehoerde generieren
|
|
||||||
*/
|
|
||||||
export async function generateAuthorityForm(incidentId: string): Promise<Blob> {
|
|
||||||
const response = await fetch(
|
|
||||||
`${INCIDENTS_API_BASE}/api/v1/incidents/${incidentId}/authority-form/pdf`,
|
|
||||||
{
|
|
||||||
headers: getAuthHeaders()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`PDF-Generierung fehlgeschlagen: ${response.statusText}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.blob()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Meldung an die Aufsichtsbehoerde einreichen (Art. 33 DSGVO)
|
|
||||||
*/
|
|
||||||
export async function submitAuthorityNotification(
|
|
||||||
incidentId: string,
|
|
||||||
data: Partial<AuthorityNotification>
|
|
||||||
): Promise<Incident> {
|
|
||||||
return fetchWithTimeout<Incident>(
|
|
||||||
`${INCIDENTS_API_BASE}/api/v1/incidents/${incidentId}/authority-notification`,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify(data)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// DATA SUBJECT NOTIFICATION (Art. 34 DSGVO)
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Betroffene Personen benachrichtigen (Art. 34 DSGVO)
|
|
||||||
*/
|
|
||||||
export async function sendDataSubjectNotification(
|
|
||||||
incidentId: string,
|
|
||||||
data: Partial<DataSubjectNotification>
|
|
||||||
): Promise<Incident> {
|
|
||||||
return fetchWithTimeout<Incident>(
|
|
||||||
`${INCIDENTS_API_BASE}/api/v1/incidents/${incidentId}/data-subject-notification`,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify(data)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// MEASURES (Massnahmen)
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Massnahme hinzufuegen (Sofort-, Korrektur- oder Praeventionsmassnahme)
|
|
||||||
*/
|
|
||||||
export async function addMeasure(
|
|
||||||
incidentId: string,
|
|
||||||
measure: Omit<IncidentMeasure, 'id' | 'incidentId'>
|
|
||||||
): Promise<Incident> {
|
|
||||||
return fetchWithTimeout<Incident>(
|
|
||||||
`${INCIDENTS_API_BASE}/api/v1/incidents/${incidentId}/measures`,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify(measure)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Massnahme aktualisieren
|
|
||||||
*/
|
|
||||||
export async function updateMeasure(
|
|
||||||
measureId: string,
|
|
||||||
update: Partial<IncidentMeasure>
|
|
||||||
): Promise<IncidentMeasure> {
|
|
||||||
return fetchWithTimeout<IncidentMeasure>(
|
|
||||||
`${INCIDENTS_API_BASE}/api/v1/measures/${measureId}`,
|
|
||||||
{
|
|
||||||
method: 'PUT',
|
|
||||||
body: JSON.stringify(update)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Massnahme als abgeschlossen markieren
|
|
||||||
*/
|
|
||||||
export async function completeMeasure(measureId: string): Promise<IncidentMeasure> {
|
|
||||||
return fetchWithTimeout<IncidentMeasure>(
|
|
||||||
`${INCIDENTS_API_BASE}/api/v1/measures/${measureId}/complete`,
|
|
||||||
{
|
|
||||||
method: 'POST'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// TIMELINE
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Zeitleisteneintrag hinzufuegen
|
|
||||||
*/
|
|
||||||
export async function addTimelineEntry(
|
|
||||||
incidentId: string,
|
|
||||||
entry: Omit<TimelineEntry, 'id' | 'incidentId'>
|
|
||||||
): Promise<Incident> {
|
|
||||||
return fetchWithTimeout<Incident>(
|
|
||||||
`${INCIDENTS_API_BASE}/api/v1/incidents/${incidentId}/timeline`,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify(entry)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// CLOSE INCIDENT
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vorfall abschliessen mit Lessons Learned
|
|
||||||
*/
|
|
||||||
export async function closeIncident(
|
|
||||||
incidentId: string,
|
|
||||||
lessonsLearned: string
|
|
||||||
): Promise<Incident> {
|
|
||||||
return fetchWithTimeout<Incident>(
|
|
||||||
`${INCIDENTS_API_BASE}/api/v1/incidents/${incidentId}/close`,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({ lessonsLearned })
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// STATISTICS
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vorfall-Statistiken abrufen
|
|
||||||
*/
|
|
||||||
export async function fetchIncidentStatistics(): Promise<IncidentStatistics> {
|
|
||||||
return fetchWithTimeout<IncidentStatistics>(
|
|
||||||
`${INCIDENTS_API_BASE}/api/v1/incidents/statistics`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// SDK PROXY FUNCTION (mit Fallback auf Mock-Daten)
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch Incident-Liste via SDK-Proxy mit Fallback auf Mock-Daten
|
|
||||||
*/
|
|
||||||
export async function fetchSDKIncidentList(): Promise<{ incidents: Incident[]; statistics: IncidentStatistics }> {
|
|
||||||
try {
|
|
||||||
const res = await fetch('/api/sdk/v1/incidents', {
|
|
||||||
headers: getAuthHeaders()
|
|
||||||
})
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error(`HTTP ${res.status}`)
|
|
||||||
}
|
|
||||||
const data = await res.json()
|
|
||||||
const incidents: Incident[] = data.incidents || []
|
|
||||||
|
|
||||||
// Statistiken lokal berechnen
|
|
||||||
const statistics = computeStatistics(incidents)
|
|
||||||
return { incidents, statistics }
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('SDK-Backend nicht erreichbar, verwende Mock-Daten:', error)
|
|
||||||
const incidents = createMockIncidents()
|
|
||||||
const statistics = createMockStatistics()
|
|
||||||
return { incidents, statistics }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Statistiken lokal aus Incident-Liste berechnen
|
|
||||||
*/
|
|
||||||
function computeStatistics(incidents: Incident[]): IncidentStatistics {
|
|
||||||
const countBy = <K extends string>(items: { [key: string]: unknown }[], field: string): Record<K, number> => {
|
|
||||||
const result: Record<string, number> = {}
|
|
||||||
items.forEach(item => {
|
|
||||||
const key = String(item[field])
|
|
||||||
result[key] = (result[key] || 0) + 1
|
|
||||||
})
|
|
||||||
return result as Record<K, number>
|
|
||||||
}
|
|
||||||
|
|
||||||
const statusCounts = countBy<IncidentStatus>(incidents as unknown as { [key: string]: unknown }[], 'status')
|
|
||||||
const severityCounts = countBy<IncidentSeverity>(incidents as unknown as { [key: string]: unknown }[], 'severity')
|
|
||||||
const categoryCounts = countBy<IncidentCategory>(incidents as unknown as { [key: string]: unknown }[], 'category')
|
|
||||||
|
|
||||||
const openIncidents = incidents.filter(i => i.status !== 'closed').length
|
|
||||||
const notificationsPending = incidents.filter(i =>
|
|
||||||
i.authorityNotification !== null &&
|
|
||||||
i.authorityNotification.status === 'pending' &&
|
|
||||||
i.status !== 'closed'
|
|
||||||
).length
|
|
||||||
|
|
||||||
// Durchschnittliche Reaktionszeit berechnen
|
|
||||||
let totalResponseHours = 0
|
|
||||||
let respondedCount = 0
|
|
||||||
incidents.forEach(i => {
|
|
||||||
if (i.riskAssessment && i.riskAssessment.assessedAt) {
|
|
||||||
const detected = new Date(i.detectedAt).getTime()
|
|
||||||
const assessed = new Date(i.riskAssessment.assessedAt).getTime()
|
|
||||||
totalResponseHours += (assessed - detected) / (1000 * 60 * 60)
|
|
||||||
respondedCount++
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
totalIncidents: incidents.length,
|
|
||||||
openIncidents,
|
|
||||||
notificationsPending,
|
|
||||||
averageResponseTimeHours: respondedCount > 0 ? Math.round(totalResponseHours / respondedCount * 10) / 10 : 0,
|
|
||||||
bySeverity: {
|
|
||||||
low: severityCounts['low'] || 0,
|
|
||||||
medium: severityCounts['medium'] || 0,
|
|
||||||
high: severityCounts['high'] || 0,
|
|
||||||
critical: severityCounts['critical'] || 0
|
|
||||||
},
|
|
||||||
byCategory: {
|
|
||||||
data_breach: categoryCounts['data_breach'] || 0,
|
|
||||||
unauthorized_access: categoryCounts['unauthorized_access'] || 0,
|
|
||||||
data_loss: categoryCounts['data_loss'] || 0,
|
|
||||||
system_compromise: categoryCounts['system_compromise'] || 0,
|
|
||||||
phishing: categoryCounts['phishing'] || 0,
|
|
||||||
ransomware: categoryCounts['ransomware'] || 0,
|
|
||||||
insider_threat: categoryCounts['insider_threat'] || 0,
|
|
||||||
physical_breach: categoryCounts['physical_breach'] || 0,
|
|
||||||
other: categoryCounts['other'] || 0
|
|
||||||
},
|
|
||||||
byStatus: {
|
|
||||||
detected: statusCounts['detected'] || 0,
|
|
||||||
assessment: statusCounts['assessment'] || 0,
|
|
||||||
containment: statusCounts['containment'] || 0,
|
|
||||||
notification_required: statusCounts['notification_required'] || 0,
|
|
||||||
notification_sent: statusCounts['notification_sent'] || 0,
|
|
||||||
remediation: statusCounts['remediation'] || 0,
|
|
||||||
closed: statusCounts['closed'] || 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// MOCK DATA (Demo-Daten fuer Entwicklung und Tests)
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Erstellt Demo-Vorfaelle fuer die Entwicklung
|
|
||||||
*/
|
|
||||||
export function createMockIncidents(): Incident[] {
|
|
||||||
const now = new Date()
|
|
||||||
|
|
||||||
return [
|
|
||||||
// 1. Gerade erkannt - noch nicht bewertet (detected/new)
|
|
||||||
{
|
|
||||||
id: 'inc-001',
|
|
||||||
referenceNumber: 'INC-2026-000001',
|
|
||||||
title: 'Unbefugter Zugriff auf Schuelerdatenbank',
|
|
||||||
description: 'Ein ehemaliger Mitarbeiter hat sich mit noch aktiven Zugangsdaten in die Schuelerdatenbank eingeloggt. Der Zugriff wurde durch die Log-Analyse entdeckt.',
|
|
||||||
category: 'unauthorized_access',
|
|
||||||
severity: 'high',
|
|
||||||
status: 'detected',
|
|
||||||
detectedAt: new Date(now.getTime() - 3 * 60 * 60 * 1000).toISOString(), // 3 Stunden her
|
|
||||||
detectedBy: 'Log-Analyse (automatisiert)',
|
|
||||||
affectedSystems: ['Schuelerdatenbank', 'Schulverwaltungssystem'],
|
|
||||||
affectedDataCategories: ['Personenbezogene Daten', 'Daten von Kindern', 'Gesundheitsdaten'],
|
|
||||||
estimatedAffectedPersons: 800,
|
|
||||||
riskAssessment: null,
|
|
||||||
authorityNotification: null,
|
|
||||||
dataSubjectNotification: null,
|
|
||||||
measures: [],
|
|
||||||
timeline: [
|
|
||||||
{
|
|
||||||
id: 'tl-001',
|
|
||||||
incidentId: 'inc-001',
|
|
||||||
timestamp: new Date(now.getTime() - 3 * 60 * 60 * 1000).toISOString(),
|
|
||||||
action: 'Vorfall erkannt',
|
|
||||||
description: 'Automatische Log-Analyse meldet verdaechtigen Login eines deaktivierten Kontos',
|
|
||||||
performedBy: 'SIEM-System'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
assignedTo: undefined
|
|
||||||
},
|
|
||||||
|
|
||||||
// 2. In Bewertung (assessment) - Risikobewertung laeuft
|
|
||||||
{
|
|
||||||
id: 'inc-002',
|
|
||||||
referenceNumber: 'INC-2026-000002',
|
|
||||||
title: 'E-Mail mit Kundendaten an falschen Empfaenger',
|
|
||||||
description: 'Ein Mitarbeiter hat eine Excel-Datei mit Kundendaten (Name, Adresse, Vertragsnummer) an einen falschen E-Mail-Empfaenger gesendet. Der Empfaenger wurde kontaktiert und hat die Loeschung bestaetigt.',
|
|
||||||
category: 'data_breach',
|
|
||||||
severity: 'medium',
|
|
||||||
status: 'assessment',
|
|
||||||
detectedAt: new Date(now.getTime() - 18 * 60 * 60 * 1000).toISOString(), // 18 Stunden her
|
|
||||||
detectedBy: 'Vertriebsabteilung',
|
|
||||||
affectedSystems: ['E-Mail-System (Exchange)'],
|
|
||||||
affectedDataCategories: ['Personenbezogene Daten', 'Kundendaten'],
|
|
||||||
estimatedAffectedPersons: 150,
|
|
||||||
riskAssessment: {
|
|
||||||
id: 'ra-002',
|
|
||||||
assessedBy: 'DSB Mueller',
|
|
||||||
assessedAt: new Date(now.getTime() - 12 * 60 * 60 * 1000).toISOString(),
|
|
||||||
likelihoodScore: 3,
|
|
||||||
impactScore: 2,
|
|
||||||
overallRisk: 'medium',
|
|
||||||
notificationRequired: false,
|
|
||||||
reasoning: 'Empfaenger hat Loeschung bestaetigt. Datenkategorie: allgemeine Kontaktdaten und Vertragsnummern. Geringes Risiko fuer betroffene Personen.'
|
|
||||||
},
|
|
||||||
authorityNotification: {
|
|
||||||
id: 'an-002',
|
|
||||||
authority: 'LfD Niedersachsen',
|
|
||||||
deadline72h: new Date(new Date(now.getTime() - 18 * 60 * 60 * 1000).getTime() + 72 * 60 * 60 * 1000).toISOString(),
|
|
||||||
status: 'pending',
|
|
||||||
formData: {}
|
|
||||||
},
|
|
||||||
dataSubjectNotification: null,
|
|
||||||
measures: [
|
|
||||||
{
|
|
||||||
id: 'meas-001',
|
|
||||||
incidentId: 'inc-002',
|
|
||||||
title: 'Empfaenger kontaktiert',
|
|
||||||
description: 'Falscher Empfaenger kontaktiert mit Bitte um Loeschung',
|
|
||||||
type: 'immediate',
|
|
||||||
status: 'completed',
|
|
||||||
responsible: 'Vertriebsleitung',
|
|
||||||
dueDate: new Date(now.getTime() - 16 * 60 * 60 * 1000).toISOString(),
|
|
||||||
completedAt: new Date(now.getTime() - 15 * 60 * 60 * 1000).toISOString()
|
|
||||||
}
|
|
||||||
],
|
|
||||||
timeline: [
|
|
||||||
{
|
|
||||||
id: 'tl-002',
|
|
||||||
incidentId: 'inc-002',
|
|
||||||
timestamp: new Date(now.getTime() - 18 * 60 * 60 * 1000).toISOString(),
|
|
||||||
action: 'Vorfall gemeldet',
|
|
||||||
description: 'Mitarbeiter meldet versehentlichen E-Mail-Versand',
|
|
||||||
performedBy: 'M. Schmidt (Vertrieb)'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'tl-003',
|
|
||||||
incidentId: 'inc-002',
|
|
||||||
timestamp: new Date(now.getTime() - 15 * 60 * 60 * 1000).toISOString(),
|
|
||||||
action: 'Sofortmassnahme',
|
|
||||||
description: 'Empfaenger kontaktiert und Loeschung bestaetigt',
|
|
||||||
performedBy: 'Vertriebsleitung'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'tl-004',
|
|
||||||
incidentId: 'inc-002',
|
|
||||||
timestamp: new Date(now.getTime() - 12 * 60 * 60 * 1000).toISOString(),
|
|
||||||
action: 'Risikobewertung',
|
|
||||||
description: 'Bewertung durchgefuehrt - mittleres Risiko, keine Meldepflicht',
|
|
||||||
performedBy: 'DSB Mueller'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
assignedTo: 'DSB Mueller'
|
|
||||||
},
|
|
||||||
|
|
||||||
// 3. Gemeldet (notification_sent) - Ransomware-Angriff
|
|
||||||
{
|
|
||||||
id: 'inc-003',
|
|
||||||
referenceNumber: 'INC-2026-000003',
|
|
||||||
title: 'Ransomware-Angriff auf Dateiserver',
|
|
||||||
description: 'Am Montagmorgen wurde ein Ransomware-Angriff auf den zentralen Dateiserver erkannt. Mehrere verschluesselte Dateien wurden identifiziert. Der Angriffsvektor war eine Phishing-E-Mail an einen Mitarbeiter.',
|
|
||||||
category: 'ransomware',
|
|
||||||
severity: 'critical',
|
|
||||||
status: 'notification_sent',
|
|
||||||
detectedAt: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
detectedBy: 'IT-Sicherheitsteam',
|
|
||||||
affectedSystems: ['Dateiserver (FS-01)', 'E-Mail-System', 'Backup-Server'],
|
|
||||||
affectedDataCategories: ['Personenbezogene Daten', 'Beschaeftigtendaten', 'Kundendaten', 'Finanzdaten'],
|
|
||||||
estimatedAffectedPersons: 2500,
|
|
||||||
riskAssessment: {
|
|
||||||
id: 'ra-003',
|
|
||||||
assessedBy: 'DSB Mueller',
|
|
||||||
assessedAt: new Date(now.getTime() - 4.5 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
likelihoodScore: 5,
|
|
||||||
impactScore: 5,
|
|
||||||
overallRisk: 'critical',
|
|
||||||
notificationRequired: true,
|
|
||||||
reasoning: 'Hohes Risiko fuer Rechte und Freiheiten der betroffenen Personen durch potentiellen Zugriff auf personenbezogene Daten und Finanzdaten. Verschluesselung betrifft Verfuegbarkeit, Exfiltration nicht auszuschliessen.'
|
|
||||||
},
|
|
||||||
authorityNotification: {
|
|
||||||
id: 'an-003',
|
|
||||||
authority: 'LfD Niedersachsen',
|
|
||||||
deadline72h: new Date(new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).getTime() + 72 * 60 * 60 * 1000).toISOString(),
|
|
||||||
submittedAt: new Date(now.getTime() - 4 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
status: 'submitted',
|
|
||||||
formData: {
|
|
||||||
referenceNumber: 'LfD-NI-2026-04821',
|
|
||||||
incidentType: 'Ransomware',
|
|
||||||
affectedPersons: 2500
|
|
||||||
},
|
|
||||||
pdfUrl: '/api/sdk/v1/incidents/inc-003/authority-form.pdf'
|
|
||||||
},
|
|
||||||
dataSubjectNotification: {
|
|
||||||
id: 'dsn-003',
|
|
||||||
notificationRequired: true,
|
|
||||||
templateText: 'Sehr geehrte Damen und Herren, wir informieren Sie ueber einen Sicherheitsvorfall, bei dem moeglicherweise Ihre personenbezogenen Daten betroffen sind...',
|
|
||||||
sentAt: new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
recipientCount: 2500,
|
|
||||||
method: 'email'
|
|
||||||
},
|
|
||||||
measures: [
|
|
||||||
{
|
|
||||||
id: 'meas-002',
|
|
||||||
incidentId: 'inc-003',
|
|
||||||
title: 'Netzwerksegmentierung',
|
|
||||||
description: 'Betroffene Systeme vom Netzwerk isoliert',
|
|
||||||
type: 'immediate',
|
|
||||||
status: 'completed',
|
|
||||||
responsible: 'IT-Sicherheitsteam',
|
|
||||||
dueDate: new Date(now.getTime() - 4.8 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
completedAt: new Date(now.getTime() - 4.9 * 24 * 60 * 60 * 1000).toISOString()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'meas-003',
|
|
||||||
incidentId: 'inc-003',
|
|
||||||
title: 'Passwoerter zuruecksetzen',
|
|
||||||
description: 'Alle Benutzerpasswoerter zurueckgesetzt',
|
|
||||||
type: 'immediate',
|
|
||||||
status: 'completed',
|
|
||||||
responsible: 'IT-Administration',
|
|
||||||
dueDate: new Date(now.getTime() - 4.5 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
completedAt: new Date(now.getTime() - 4.5 * 24 * 60 * 60 * 1000).toISOString()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'meas-004',
|
|
||||||
incidentId: 'inc-003',
|
|
||||||
title: 'E-Mail-Security Gateway implementieren',
|
|
||||||
description: 'Implementierung eines fortgeschrittenen E-Mail-Sicherheitsgateways mit Sandboxing',
|
|
||||||
type: 'preventive',
|
|
||||||
status: 'in_progress',
|
|
||||||
responsible: 'IT-Sicherheitsteam',
|
|
||||||
dueDate: new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000).toISOString()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'meas-005',
|
|
||||||
incidentId: 'inc-003',
|
|
||||||
title: 'Mitarbeiterschulung Phishing',
|
|
||||||
description: 'Verpflichtende Schulung fuer alle Mitarbeiter zum Thema Phishing-Erkennung',
|
|
||||||
type: 'preventive',
|
|
||||||
status: 'planned',
|
|
||||||
responsible: 'Personalwesen',
|
|
||||||
dueDate: new Date(now.getTime() + 60 * 24 * 60 * 60 * 1000).toISOString()
|
|
||||||
}
|
|
||||||
],
|
|
||||||
timeline: [
|
|
||||||
{
|
|
||||||
id: 'tl-005',
|
|
||||||
incidentId: 'inc-003',
|
|
||||||
timestamp: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
action: 'Vorfall erkannt',
|
|
||||||
description: 'IT-Sicherheitsteam erkennt ungewoehnliche Verschluesselungsaktivitaet',
|
|
||||||
performedBy: 'IT-Sicherheitsteam'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'tl-006',
|
|
||||||
incidentId: 'inc-003',
|
|
||||||
timestamp: new Date(now.getTime() - 4.9 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
action: 'Eindaemmung gestartet',
|
|
||||||
description: 'Netzwerksegmentierung und Isolation betroffener Systeme',
|
|
||||||
performedBy: 'IT-Sicherheitsteam'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'tl-007',
|
|
||||||
incidentId: 'inc-003',
|
|
||||||
timestamp: new Date(now.getTime() - 4.5 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
action: 'Risikobewertung abgeschlossen',
|
|
||||||
description: 'Kritisches Risiko festgestellt - Meldepflicht ausgeloest',
|
|
||||||
performedBy: 'DSB Mueller'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'tl-008',
|
|
||||||
incidentId: 'inc-003',
|
|
||||||
timestamp: new Date(now.getTime() - 4 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
action: 'Behoerdenbenachrichtigung',
|
|
||||||
description: 'Meldung an LfD Niedersachsen eingereicht',
|
|
||||||
performedBy: 'DSB Mueller'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'tl-009',
|
|
||||||
incidentId: 'inc-003',
|
|
||||||
timestamp: new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
action: 'Betroffene benachrichtigt',
|
|
||||||
description: '2.500 betroffene Personen per E-Mail informiert',
|
|
||||||
performedBy: 'Kommunikationsabteilung'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
assignedTo: 'DSB Mueller'
|
|
||||||
},
|
|
||||||
|
|
||||||
// 4. Abgeschlossener Vorfall (closed) - Phishing
|
|
||||||
{
|
|
||||||
id: 'inc-004',
|
|
||||||
referenceNumber: 'INC-2026-000004',
|
|
||||||
title: 'Phishing-Angriff auf Personalabteilung',
|
|
||||||
description: 'Gezielter Phishing-Angriff auf die Personalabteilung. Ein Mitarbeiter hat Zugangsdaten auf einer gefaelschten Login-Seite eingegeben. Das Konto wurde sofort gesperrt. Keine Datenexfiltration festgestellt.',
|
|
||||||
category: 'phishing',
|
|
||||||
severity: 'high',
|
|
||||||
status: 'closed',
|
|
||||||
detectedAt: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
detectedBy: 'IT-Sicherheitsteam (SIEM-Alert)',
|
|
||||||
affectedSystems: ['Active Directory', 'HR-Portal'],
|
|
||||||
affectedDataCategories: ['Beschaeftigtendaten', 'Personenbezogene Daten'],
|
|
||||||
estimatedAffectedPersons: 0,
|
|
||||||
riskAssessment: {
|
|
||||||
id: 'ra-004',
|
|
||||||
assessedBy: 'DSB Mueller',
|
|
||||||
assessedAt: new Date(now.getTime() - 29 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
likelihoodScore: 4,
|
|
||||||
impactScore: 3,
|
|
||||||
overallRisk: 'high',
|
|
||||||
notificationRequired: true,
|
|
||||||
reasoning: 'Zugangsdaten kompromittiert, potentieller Zugriff auf Personaldaten. Keine Exfiltration festgestellt, dennoch Meldung wegen Kompromittierung der Zugangsdaten.'
|
|
||||||
},
|
|
||||||
authorityNotification: {
|
|
||||||
id: 'an-004',
|
|
||||||
authority: 'LfD Niedersachsen',
|
|
||||||
deadline72h: new Date(new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).getTime() + 72 * 60 * 60 * 1000).toISOString(),
|
|
||||||
submittedAt: new Date(now.getTime() - 29 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
status: 'acknowledged',
|
|
||||||
formData: {
|
|
||||||
referenceNumber: 'LfD-NI-2026-03912',
|
|
||||||
incidentType: 'Phishing',
|
|
||||||
affectedPersons: 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dataSubjectNotification: {
|
|
||||||
id: 'dsn-004',
|
|
||||||
notificationRequired: false,
|
|
||||||
templateText: '',
|
|
||||||
recipientCount: 0,
|
|
||||||
method: 'email'
|
|
||||||
},
|
|
||||||
measures: [
|
|
||||||
{
|
|
||||||
id: 'meas-006',
|
|
||||||
incidentId: 'inc-004',
|
|
||||||
title: 'Konto gesperrt',
|
|
||||||
description: 'Kompromittiertes Benutzerkonto sofort gesperrt',
|
|
||||||
type: 'immediate',
|
|
||||||
status: 'completed',
|
|
||||||
responsible: 'IT-Administration',
|
|
||||||
dueDate: new Date(now.getTime() - 29.8 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
completedAt: new Date(now.getTime() - 29.9 * 24 * 60 * 60 * 1000).toISOString()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'meas-007',
|
|
||||||
incidentId: 'inc-004',
|
|
||||||
title: 'MFA fuer alle Mitarbeiter',
|
|
||||||
description: 'Einfuehrung von Multi-Faktor-Authentifizierung fuer alle Konten',
|
|
||||||
type: 'preventive',
|
|
||||||
status: 'completed',
|
|
||||||
responsible: 'IT-Sicherheitsteam',
|
|
||||||
dueDate: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
completedAt: new Date(now.getTime() - 12 * 24 * 60 * 60 * 1000).toISOString()
|
|
||||||
}
|
|
||||||
],
|
|
||||||
timeline: [
|
|
||||||
{
|
|
||||||
id: 'tl-010',
|
|
||||||
incidentId: 'inc-004',
|
|
||||||
timestamp: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
action: 'SIEM-Alert',
|
|
||||||
description: 'Verdaechtiger Login-Versuch aus unbekannter Region erkannt',
|
|
||||||
performedBy: 'IT-Sicherheitsteam'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'tl-011',
|
|
||||||
incidentId: 'inc-004',
|
|
||||||
timestamp: new Date(now.getTime() - 29 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
action: 'Behoerdenbenachrichtigung',
|
|
||||||
description: 'Meldung an LfD Niedersachsen',
|
|
||||||
performedBy: 'DSB Mueller'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'tl-012',
|
|
||||||
incidentId: 'inc-004',
|
|
||||||
timestamp: new Date(now.getTime() - 15 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
action: 'Vorfall abgeschlossen',
|
|
||||||
description: 'Alle Massnahmen umgesetzt, keine Datenexfiltration festgestellt',
|
|
||||||
performedBy: 'DSB Mueller'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
assignedTo: 'DSB Mueller',
|
|
||||||
closedAt: new Date(now.getTime() - 15 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
lessonsLearned: '1. MFA haette den Zugriff verhindert (jetzt implementiert). 2. E-Mail-Security-Gateway muss verbesserte Phishing-Erkennung erhalten. 3. Regelmaessige Phishing-Simulationen fuer alle Mitarbeiter einfuehren.'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Erstellt Mock-Statistiken fuer die Entwicklung
|
|
||||||
*/
|
|
||||||
export function createMockStatistics(): IncidentStatistics {
|
|
||||||
return {
|
|
||||||
totalIncidents: 4,
|
|
||||||
openIncidents: 3,
|
|
||||||
notificationsPending: 1,
|
|
||||||
averageResponseTimeHours: 8.5,
|
|
||||||
bySeverity: {
|
|
||||||
low: 0,
|
|
||||||
medium: 1,
|
|
||||||
high: 2,
|
|
||||||
critical: 1
|
|
||||||
},
|
|
||||||
byCategory: {
|
|
||||||
data_breach: 1,
|
|
||||||
unauthorized_access: 1,
|
|
||||||
data_loss: 0,
|
|
||||||
system_compromise: 0,
|
|
||||||
phishing: 1,
|
|
||||||
ransomware: 1,
|
|
||||||
insider_threat: 0,
|
|
||||||
physical_breach: 0,
|
|
||||||
other: 0
|
|
||||||
},
|
|
||||||
byStatus: {
|
|
||||||
detected: 1,
|
|
||||||
assessment: 1,
|
|
||||||
containment: 0,
|
|
||||||
notification_required: 0,
|
|
||||||
notification_sent: 1,
|
|
||||||
remediation: 0,
|
|
||||||
closed: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,720 +1,13 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// TOM Generator Context
|
// TOM Generator Context — Barrel re-exports
|
||||||
// State management for the TOM Generator Wizard
|
// Preserves the original public API so existing imports work unchanged.
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
import React, {
|
export { TOMGeneratorProvider } from './provider'
|
||||||
createContext,
|
export { TOMGeneratorContext } from './provider'
|
||||||
useContext,
|
export type { TOMGeneratorContextValue } from './provider'
|
||||||
useReducer,
|
export { useTOMGenerator } from './hooks'
|
||||||
useCallback,
|
export { tomGeneratorReducer } from './reducer'
|
||||||
useEffect,
|
export type { TOMGeneratorAction } from './reducer'
|
||||||
useRef,
|
|
||||||
ReactNode,
|
|
||||||
} from 'react'
|
|
||||||
import {
|
|
||||||
TOMGeneratorState,
|
|
||||||
TOMGeneratorStepId,
|
|
||||||
CompanyProfile,
|
|
||||||
DataProfile,
|
|
||||||
ArchitectureProfile,
|
|
||||||
SecurityProfile,
|
|
||||||
RiskProfile,
|
|
||||||
EvidenceDocument,
|
|
||||||
DerivedTOM,
|
|
||||||
GapAnalysisResult,
|
|
||||||
ExportRecord,
|
|
||||||
WizardStep,
|
|
||||||
createInitialTOMGeneratorState,
|
|
||||||
TOM_GENERATOR_STEPS,
|
|
||||||
getStepIndex,
|
|
||||||
calculateProtectionLevel,
|
|
||||||
isDSFARequired,
|
|
||||||
hasSpecialCategories,
|
|
||||||
} from './types'
|
|
||||||
import { TOMRulesEngine } from './rules-engine'
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// ACTION TYPES
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
type TOMGeneratorAction =
|
|
||||||
| { type: 'INITIALIZE'; payload: { tenantId: string; state?: TOMGeneratorState } }
|
|
||||||
| { type: 'RESET'; payload: { tenantId: string } }
|
|
||||||
| { type: 'SET_CURRENT_STEP'; payload: TOMGeneratorStepId }
|
|
||||||
| { type: 'SET_COMPANY_PROFILE'; payload: CompanyProfile }
|
|
||||||
| { type: 'UPDATE_COMPANY_PROFILE'; payload: Partial<CompanyProfile> }
|
|
||||||
| { type: 'SET_DATA_PROFILE'; payload: DataProfile }
|
|
||||||
| { type: 'UPDATE_DATA_PROFILE'; payload: Partial<DataProfile> }
|
|
||||||
| { type: 'SET_ARCHITECTURE_PROFILE'; payload: ArchitectureProfile }
|
|
||||||
| { type: 'UPDATE_ARCHITECTURE_PROFILE'; payload: Partial<ArchitectureProfile> }
|
|
||||||
| { type: 'SET_SECURITY_PROFILE'; payload: SecurityProfile }
|
|
||||||
| { type: 'UPDATE_SECURITY_PROFILE'; payload: Partial<SecurityProfile> }
|
|
||||||
| { type: 'SET_RISK_PROFILE'; payload: RiskProfile }
|
|
||||||
| { type: 'UPDATE_RISK_PROFILE'; payload: Partial<RiskProfile> }
|
|
||||||
| { type: 'COMPLETE_STEP'; payload: { stepId: TOMGeneratorStepId; data: unknown } }
|
|
||||||
| { type: 'UNCOMPLETE_STEP'; payload: TOMGeneratorStepId }
|
|
||||||
| { type: 'ADD_EVIDENCE'; payload: EvidenceDocument }
|
|
||||||
| { type: 'UPDATE_EVIDENCE'; payload: { id: string; data: Partial<EvidenceDocument> } }
|
|
||||||
| { type: 'DELETE_EVIDENCE'; payload: string }
|
|
||||||
| { type: 'SET_DERIVED_TOMS'; payload: DerivedTOM[] }
|
|
||||||
| { type: 'UPDATE_DERIVED_TOM'; payload: { id: string; data: Partial<DerivedTOM> } }
|
|
||||||
| { type: 'SET_GAP_ANALYSIS'; payload: GapAnalysisResult }
|
|
||||||
| { type: 'ADD_EXPORT'; payload: ExportRecord }
|
|
||||||
| { type: 'BULK_UPDATE_TOMS'; payload: { updates: Array<{ id: string; data: Partial<DerivedTOM> }> } }
|
|
||||||
| { type: 'LOAD_STATE'; payload: TOMGeneratorState }
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// REDUCER
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
function tomGeneratorReducer(
|
|
||||||
state: TOMGeneratorState,
|
|
||||||
action: TOMGeneratorAction
|
|
||||||
): TOMGeneratorState {
|
|
||||||
const updateState = (updates: Partial<TOMGeneratorState>): TOMGeneratorState => ({
|
|
||||||
...state,
|
|
||||||
...updates,
|
|
||||||
updatedAt: new Date(),
|
|
||||||
})
|
|
||||||
|
|
||||||
switch (action.type) {
|
|
||||||
case 'INITIALIZE': {
|
|
||||||
if (action.payload.state) {
|
|
||||||
return action.payload.state
|
|
||||||
}
|
|
||||||
return createInitialTOMGeneratorState(action.payload.tenantId)
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'RESET': {
|
|
||||||
return createInitialTOMGeneratorState(action.payload.tenantId)
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'SET_CURRENT_STEP': {
|
|
||||||
return updateState({ currentStep: action.payload })
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'SET_COMPANY_PROFILE': {
|
|
||||||
return updateState({ companyProfile: action.payload })
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'UPDATE_COMPANY_PROFILE': {
|
|
||||||
if (!state.companyProfile) return state
|
|
||||||
return updateState({
|
|
||||||
companyProfile: { ...state.companyProfile, ...action.payload },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'SET_DATA_PROFILE': {
|
|
||||||
// Automatically set hasSpecialCategories based on categories
|
|
||||||
const profile: DataProfile = {
|
|
||||||
...action.payload,
|
|
||||||
hasSpecialCategories: hasSpecialCategories(action.payload.categories),
|
|
||||||
}
|
|
||||||
return updateState({ dataProfile: profile })
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'UPDATE_DATA_PROFILE': {
|
|
||||||
if (!state.dataProfile) return state
|
|
||||||
const updatedProfile = { ...state.dataProfile, ...action.payload }
|
|
||||||
// Recalculate hasSpecialCategories if categories changed
|
|
||||||
if (action.payload.categories) {
|
|
||||||
updatedProfile.hasSpecialCategories = hasSpecialCategories(
|
|
||||||
action.payload.categories
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return updateState({ dataProfile: updatedProfile })
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'SET_ARCHITECTURE_PROFILE': {
|
|
||||||
return updateState({ architectureProfile: action.payload })
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'UPDATE_ARCHITECTURE_PROFILE': {
|
|
||||||
if (!state.architectureProfile) return state
|
|
||||||
return updateState({
|
|
||||||
architectureProfile: { ...state.architectureProfile, ...action.payload },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'SET_SECURITY_PROFILE': {
|
|
||||||
return updateState({ securityProfile: action.payload })
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'UPDATE_SECURITY_PROFILE': {
|
|
||||||
if (!state.securityProfile) return state
|
|
||||||
return updateState({
|
|
||||||
securityProfile: { ...state.securityProfile, ...action.payload },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'SET_RISK_PROFILE': {
|
|
||||||
// Automatically calculate protection level and DSFA requirement
|
|
||||||
const profile: RiskProfile = {
|
|
||||||
...action.payload,
|
|
||||||
protectionLevel: calculateProtectionLevel(action.payload.ciaAssessment),
|
|
||||||
dsfaRequired: isDSFARequired(state.dataProfile, action.payload),
|
|
||||||
}
|
|
||||||
return updateState({ riskProfile: profile })
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'UPDATE_RISK_PROFILE': {
|
|
||||||
if (!state.riskProfile) return state
|
|
||||||
const updatedProfile = { ...state.riskProfile, ...action.payload }
|
|
||||||
// Recalculate protection level if CIA assessment changed
|
|
||||||
if (action.payload.ciaAssessment) {
|
|
||||||
updatedProfile.protectionLevel = calculateProtectionLevel(
|
|
||||||
action.payload.ciaAssessment
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// Recalculate DSFA requirement
|
|
||||||
updatedProfile.dsfaRequired = isDSFARequired(state.dataProfile, updatedProfile)
|
|
||||||
return updateState({ riskProfile: updatedProfile })
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'COMPLETE_STEP': {
|
|
||||||
const updatedSteps = state.steps.map((step) =>
|
|
||||||
step.id === action.payload.stepId
|
|
||||||
? {
|
|
||||||
...step,
|
|
||||||
completed: true,
|
|
||||||
data: action.payload.data,
|
|
||||||
validatedAt: new Date(),
|
|
||||||
}
|
|
||||||
: step
|
|
||||||
)
|
|
||||||
return updateState({ steps: updatedSteps })
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'UNCOMPLETE_STEP': {
|
|
||||||
const updatedSteps = state.steps.map((step) =>
|
|
||||||
step.id === action.payload
|
|
||||||
? { ...step, completed: false, validatedAt: null }
|
|
||||||
: step
|
|
||||||
)
|
|
||||||
return updateState({ steps: updatedSteps })
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'ADD_EVIDENCE': {
|
|
||||||
return updateState({
|
|
||||||
documents: [...state.documents, action.payload],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'UPDATE_EVIDENCE': {
|
|
||||||
const updatedDocuments = state.documents.map((doc) =>
|
|
||||||
doc.id === action.payload.id ? { ...doc, ...action.payload.data } : doc
|
|
||||||
)
|
|
||||||
return updateState({ documents: updatedDocuments })
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'DELETE_EVIDENCE': {
|
|
||||||
return updateState({
|
|
||||||
documents: state.documents.filter((doc) => doc.id !== action.payload),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'SET_DERIVED_TOMS': {
|
|
||||||
return updateState({ derivedTOMs: action.payload })
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'UPDATE_DERIVED_TOM': {
|
|
||||||
const updatedTOMs = state.derivedTOMs.map((tom) =>
|
|
||||||
tom.id === action.payload.id ? { ...tom, ...action.payload.data } : tom
|
|
||||||
)
|
|
||||||
return updateState({ derivedTOMs: updatedTOMs })
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'SET_GAP_ANALYSIS': {
|
|
||||||
return updateState({ gapAnalysis: action.payload })
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'ADD_EXPORT': {
|
|
||||||
return updateState({
|
|
||||||
exports: [...state.exports, action.payload],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'BULK_UPDATE_TOMS': {
|
|
||||||
let updatedTOMs = [...state.derivedTOMs]
|
|
||||||
for (const update of action.payload.updates) {
|
|
||||||
updatedTOMs = updatedTOMs.map((tom) =>
|
|
||||||
tom.id === update.id ? { ...tom, ...update.data } : tom
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return updateState({ derivedTOMs: updatedTOMs })
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'LOAD_STATE': {
|
|
||||||
return action.payload
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// CONTEXT VALUE INTERFACE
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
interface TOMGeneratorContextValue {
|
|
||||||
state: TOMGeneratorState
|
|
||||||
dispatch: React.Dispatch<TOMGeneratorAction>
|
|
||||||
|
|
||||||
// Navigation
|
|
||||||
currentStepIndex: number
|
|
||||||
totalSteps: number
|
|
||||||
canGoNext: boolean
|
|
||||||
canGoPrevious: boolean
|
|
||||||
goToStep: (stepId: TOMGeneratorStepId) => void
|
|
||||||
goToNextStep: () => void
|
|
||||||
goToPreviousStep: () => void
|
|
||||||
completeCurrentStep: (data: unknown) => void
|
|
||||||
|
|
||||||
// Profile setters
|
|
||||||
setCompanyProfile: (profile: CompanyProfile) => void
|
|
||||||
updateCompanyProfile: (data: Partial<CompanyProfile>) => void
|
|
||||||
setDataProfile: (profile: DataProfile) => void
|
|
||||||
updateDataProfile: (data: Partial<DataProfile>) => void
|
|
||||||
setArchitectureProfile: (profile: ArchitectureProfile) => void
|
|
||||||
updateArchitectureProfile: (data: Partial<ArchitectureProfile>) => void
|
|
||||||
setSecurityProfile: (profile: SecurityProfile) => void
|
|
||||||
updateSecurityProfile: (data: Partial<SecurityProfile>) => void
|
|
||||||
setRiskProfile: (profile: RiskProfile) => void
|
|
||||||
updateRiskProfile: (data: Partial<RiskProfile>) => void
|
|
||||||
|
|
||||||
// Evidence management
|
|
||||||
addEvidence: (document: EvidenceDocument) => void
|
|
||||||
updateEvidence: (id: string, data: Partial<EvidenceDocument>) => void
|
|
||||||
deleteEvidence: (id: string) => void
|
|
||||||
|
|
||||||
// TOM derivation
|
|
||||||
deriveTOMs: () => void
|
|
||||||
updateDerivedTOM: (id: string, data: Partial<DerivedTOM>) => void
|
|
||||||
bulkUpdateTOMs: (updates: Array<{ id: string; data: Partial<DerivedTOM> }>) => void
|
|
||||||
|
|
||||||
// Gap analysis
|
|
||||||
runGapAnalysis: () => void
|
|
||||||
|
|
||||||
// Export
|
|
||||||
addExport: (record: ExportRecord) => void
|
|
||||||
|
|
||||||
// Persistence
|
|
||||||
saveState: () => Promise<void>
|
|
||||||
loadState: () => Promise<void>
|
|
||||||
resetState: () => void
|
|
||||||
|
|
||||||
// Status
|
|
||||||
isStepCompleted: (stepId: TOMGeneratorStepId) => boolean
|
|
||||||
getCompletionPercentage: () => number
|
|
||||||
isLoading: boolean
|
|
||||||
error: string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// CONTEXT
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
const TOMGeneratorContext = createContext<TOMGeneratorContextValue | null>(null)
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// STORAGE KEYS
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
const STORAGE_KEY_PREFIX = 'tom-generator-state-'
|
|
||||||
|
|
||||||
function getStorageKey(tenantId: string): string {
|
|
||||||
return `${STORAGE_KEY_PREFIX}${tenantId}`
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// PROVIDER COMPONENT
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
interface TOMGeneratorProviderProps {
|
|
||||||
children: ReactNode
|
|
||||||
tenantId: string
|
|
||||||
initialState?: TOMGeneratorState
|
|
||||||
enablePersistence?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export function TOMGeneratorProvider({
|
|
||||||
children,
|
|
||||||
tenantId,
|
|
||||||
initialState,
|
|
||||||
enablePersistence = true,
|
|
||||||
}: TOMGeneratorProviderProps) {
|
|
||||||
const [state, dispatch] = useReducer(
|
|
||||||
tomGeneratorReducer,
|
|
||||||
initialState ?? createInitialTOMGeneratorState(tenantId)
|
|
||||||
)
|
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = React.useState(false)
|
|
||||||
const [error, setError] = React.useState<string | null>(null)
|
|
||||||
|
|
||||||
const rulesEngineRef = useRef<TOMRulesEngine | null>(null)
|
|
||||||
|
|
||||||
// Initialize rules engine
|
|
||||||
useEffect(() => {
|
|
||||||
if (!rulesEngineRef.current) {
|
|
||||||
rulesEngineRef.current = new TOMRulesEngine()
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// Load state from localStorage on mount
|
|
||||||
useEffect(() => {
|
|
||||||
if (enablePersistence && typeof window !== 'undefined') {
|
|
||||||
try {
|
|
||||||
const stored = localStorage.getItem(getStorageKey(tenantId))
|
|
||||||
if (stored) {
|
|
||||||
const parsed = JSON.parse(stored)
|
|
||||||
// Convert date strings back to Date objects
|
|
||||||
if (parsed.createdAt) parsed.createdAt = new Date(parsed.createdAt)
|
|
||||||
if (parsed.updatedAt) parsed.updatedAt = new Date(parsed.updatedAt)
|
|
||||||
if (parsed.steps) {
|
|
||||||
parsed.steps = parsed.steps.map((step: WizardStep) => ({
|
|
||||||
...step,
|
|
||||||
validatedAt: step.validatedAt ? new Date(step.validatedAt) : null,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
if (parsed.documents) {
|
|
||||||
parsed.documents = parsed.documents.map((doc: EvidenceDocument) => ({
|
|
||||||
...doc,
|
|
||||||
uploadedAt: new Date(doc.uploadedAt),
|
|
||||||
validFrom: doc.validFrom ? new Date(doc.validFrom) : null,
|
|
||||||
validUntil: doc.validUntil ? new Date(doc.validUntil) : null,
|
|
||||||
aiAnalysis: doc.aiAnalysis
|
|
||||||
? {
|
|
||||||
...doc.aiAnalysis,
|
|
||||||
analyzedAt: new Date(doc.aiAnalysis.analyzedAt),
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
if (parsed.derivedTOMs) {
|
|
||||||
parsed.derivedTOMs = parsed.derivedTOMs.map((tom: DerivedTOM) => ({
|
|
||||||
...tom,
|
|
||||||
implementationDate: tom.implementationDate
|
|
||||||
? new Date(tom.implementationDate)
|
|
||||||
: null,
|
|
||||||
reviewDate: tom.reviewDate ? new Date(tom.reviewDate) : null,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
if (parsed.gapAnalysis?.generatedAt) {
|
|
||||||
parsed.gapAnalysis.generatedAt = new Date(parsed.gapAnalysis.generatedAt)
|
|
||||||
}
|
|
||||||
if (parsed.exports) {
|
|
||||||
parsed.exports = parsed.exports.map((exp: ExportRecord) => ({
|
|
||||||
...exp,
|
|
||||||
generatedAt: new Date(exp.generatedAt),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
dispatch({ type: 'LOAD_STATE', payload: parsed })
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to load TOM Generator state from localStorage:', e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [tenantId, enablePersistence])
|
|
||||||
|
|
||||||
// Save state to localStorage on changes
|
|
||||||
useEffect(() => {
|
|
||||||
if (enablePersistence && typeof window !== 'undefined') {
|
|
||||||
try {
|
|
||||||
localStorage.setItem(getStorageKey(tenantId), JSON.stringify(state))
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to save TOM Generator state to localStorage:', e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [state, tenantId, enablePersistence])
|
|
||||||
|
|
||||||
// Navigation helpers
|
|
||||||
const currentStepIndex = getStepIndex(state.currentStep)
|
|
||||||
const totalSteps = TOM_GENERATOR_STEPS.length
|
|
||||||
|
|
||||||
const canGoNext = currentStepIndex < totalSteps - 1
|
|
||||||
const canGoPrevious = currentStepIndex > 0
|
|
||||||
|
|
||||||
const goToStep = useCallback((stepId: TOMGeneratorStepId) => {
|
|
||||||
dispatch({ type: 'SET_CURRENT_STEP', payload: stepId })
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const goToNextStep = useCallback(() => {
|
|
||||||
if (canGoNext) {
|
|
||||||
const nextStep = TOM_GENERATOR_STEPS[currentStepIndex + 1]
|
|
||||||
dispatch({ type: 'SET_CURRENT_STEP', payload: nextStep.id })
|
|
||||||
}
|
|
||||||
}, [canGoNext, currentStepIndex])
|
|
||||||
|
|
||||||
const goToPreviousStep = useCallback(() => {
|
|
||||||
if (canGoPrevious) {
|
|
||||||
const prevStep = TOM_GENERATOR_STEPS[currentStepIndex - 1]
|
|
||||||
dispatch({ type: 'SET_CURRENT_STEP', payload: prevStep.id })
|
|
||||||
}
|
|
||||||
}, [canGoPrevious, currentStepIndex])
|
|
||||||
|
|
||||||
const completeCurrentStep = useCallback(
|
|
||||||
(data: unknown) => {
|
|
||||||
dispatch({
|
|
||||||
type: 'COMPLETE_STEP',
|
|
||||||
payload: { stepId: state.currentStep, data },
|
|
||||||
})
|
|
||||||
},
|
|
||||||
[state.currentStep]
|
|
||||||
)
|
|
||||||
|
|
||||||
// Profile setters
|
|
||||||
const setCompanyProfile = useCallback((profile: CompanyProfile) => {
|
|
||||||
dispatch({ type: 'SET_COMPANY_PROFILE', payload: profile })
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const updateCompanyProfile = useCallback((data: Partial<CompanyProfile>) => {
|
|
||||||
dispatch({ type: 'UPDATE_COMPANY_PROFILE', payload: data })
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const setDataProfile = useCallback((profile: DataProfile) => {
|
|
||||||
dispatch({ type: 'SET_DATA_PROFILE', payload: profile })
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const updateDataProfile = useCallback((data: Partial<DataProfile>) => {
|
|
||||||
dispatch({ type: 'UPDATE_DATA_PROFILE', payload: data })
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const setArchitectureProfile = useCallback((profile: ArchitectureProfile) => {
|
|
||||||
dispatch({ type: 'SET_ARCHITECTURE_PROFILE', payload: profile })
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const updateArchitectureProfile = useCallback(
|
|
||||||
(data: Partial<ArchitectureProfile>) => {
|
|
||||||
dispatch({ type: 'UPDATE_ARCHITECTURE_PROFILE', payload: data })
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
|
|
||||||
const setSecurityProfile = useCallback((profile: SecurityProfile) => {
|
|
||||||
dispatch({ type: 'SET_SECURITY_PROFILE', payload: profile })
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const updateSecurityProfile = useCallback((data: Partial<SecurityProfile>) => {
|
|
||||||
dispatch({ type: 'UPDATE_SECURITY_PROFILE', payload: data })
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const setRiskProfile = useCallback((profile: RiskProfile) => {
|
|
||||||
dispatch({ type: 'SET_RISK_PROFILE', payload: profile })
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const updateRiskProfile = useCallback((data: Partial<RiskProfile>) => {
|
|
||||||
dispatch({ type: 'UPDATE_RISK_PROFILE', payload: data })
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// Evidence management
|
|
||||||
const addEvidence = useCallback((document: EvidenceDocument) => {
|
|
||||||
dispatch({ type: 'ADD_EVIDENCE', payload: document })
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const updateEvidence = useCallback(
|
|
||||||
(id: string, data: Partial<EvidenceDocument>) => {
|
|
||||||
dispatch({ type: 'UPDATE_EVIDENCE', payload: { id, data } })
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
|
|
||||||
const deleteEvidence = useCallback((id: string) => {
|
|
||||||
dispatch({ type: 'DELETE_EVIDENCE', payload: id })
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// TOM derivation
|
|
||||||
const deriveTOMs = useCallback(() => {
|
|
||||||
if (!rulesEngineRef.current) return
|
|
||||||
|
|
||||||
const derivedTOMs = rulesEngineRef.current.deriveAllTOMs({
|
|
||||||
companyProfile: state.companyProfile,
|
|
||||||
dataProfile: state.dataProfile,
|
|
||||||
architectureProfile: state.architectureProfile,
|
|
||||||
securityProfile: state.securityProfile,
|
|
||||||
riskProfile: state.riskProfile,
|
|
||||||
})
|
|
||||||
|
|
||||||
dispatch({ type: 'SET_DERIVED_TOMS', payload: derivedTOMs })
|
|
||||||
}, [
|
|
||||||
state.companyProfile,
|
|
||||||
state.dataProfile,
|
|
||||||
state.architectureProfile,
|
|
||||||
state.securityProfile,
|
|
||||||
state.riskProfile,
|
|
||||||
])
|
|
||||||
|
|
||||||
const updateDerivedTOM = useCallback(
|
|
||||||
(id: string, data: Partial<DerivedTOM>) => {
|
|
||||||
dispatch({ type: 'UPDATE_DERIVED_TOM', payload: { id, data } })
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
|
|
||||||
const bulkUpdateTOMs = useCallback(
|
|
||||||
(updates: Array<{ id: string; data: Partial<DerivedTOM> }>) => {
|
|
||||||
for (const { id, data } of updates) {
|
|
||||||
dispatch({ type: 'UPDATE_DERIVED_TOM', payload: { id, data } })
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
|
|
||||||
// Gap analysis
|
|
||||||
const runGapAnalysis = useCallback(() => {
|
|
||||||
if (!rulesEngineRef.current) return
|
|
||||||
|
|
||||||
const result = rulesEngineRef.current.performGapAnalysis(
|
|
||||||
state.derivedTOMs,
|
|
||||||
state.documents
|
|
||||||
)
|
|
||||||
|
|
||||||
dispatch({ type: 'SET_GAP_ANALYSIS', payload: result })
|
|
||||||
}, [state.derivedTOMs, state.documents])
|
|
||||||
|
|
||||||
// Export
|
|
||||||
const addExport = useCallback((record: ExportRecord) => {
|
|
||||||
dispatch({ type: 'ADD_EXPORT', payload: record })
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// Persistence
|
|
||||||
const saveState = useCallback(async () => {
|
|
||||||
setIsLoading(true)
|
|
||||||
setError(null)
|
|
||||||
try {
|
|
||||||
// API call to save state
|
|
||||||
const response = await fetch('/api/sdk/v1/tom-generator/state', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ tenantId, state }),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Failed to save state')
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
setError(e instanceof Error ? e.message : 'Unknown error')
|
|
||||||
throw e
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
|
||||||
}, [tenantId, state])
|
|
||||||
|
|
||||||
const loadState = useCallback(async () => {
|
|
||||||
setIsLoading(true)
|
|
||||||
setError(null)
|
|
||||||
try {
|
|
||||||
const response = await fetch(
|
|
||||||
`/api/sdk/v1/tom-generator/state?tenantId=${tenantId}`
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Failed to load state')
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
if (data.state) {
|
|
||||||
dispatch({ type: 'LOAD_STATE', payload: data.state })
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
setError(e instanceof Error ? e.message : 'Unknown error')
|
|
||||||
throw e
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
|
||||||
}, [tenantId])
|
|
||||||
|
|
||||||
const resetState = useCallback(() => {
|
|
||||||
dispatch({ type: 'RESET', payload: { tenantId } })
|
|
||||||
}, [tenantId])
|
|
||||||
|
|
||||||
// Status helpers
|
|
||||||
const isStepCompleted = useCallback(
|
|
||||||
(stepId: TOMGeneratorStepId) => {
|
|
||||||
const step = state.steps.find((s) => s.id === stepId)
|
|
||||||
return step?.completed ?? false
|
|
||||||
},
|
|
||||||
[state.steps]
|
|
||||||
)
|
|
||||||
|
|
||||||
const getCompletionPercentage = useCallback(() => {
|
|
||||||
const completedSteps = state.steps.filter((s) => s.completed).length
|
|
||||||
return Math.round((completedSteps / totalSteps) * 100)
|
|
||||||
}, [state.steps, totalSteps])
|
|
||||||
|
|
||||||
const contextValue: TOMGeneratorContextValue = {
|
|
||||||
state,
|
|
||||||
dispatch,
|
|
||||||
|
|
||||||
currentStepIndex,
|
|
||||||
totalSteps,
|
|
||||||
canGoNext,
|
|
||||||
canGoPrevious,
|
|
||||||
goToStep,
|
|
||||||
goToNextStep,
|
|
||||||
goToPreviousStep,
|
|
||||||
completeCurrentStep,
|
|
||||||
|
|
||||||
setCompanyProfile,
|
|
||||||
updateCompanyProfile,
|
|
||||||
setDataProfile,
|
|
||||||
updateDataProfile,
|
|
||||||
setArchitectureProfile,
|
|
||||||
updateArchitectureProfile,
|
|
||||||
setSecurityProfile,
|
|
||||||
updateSecurityProfile,
|
|
||||||
setRiskProfile,
|
|
||||||
updateRiskProfile,
|
|
||||||
|
|
||||||
addEvidence,
|
|
||||||
updateEvidence,
|
|
||||||
deleteEvidence,
|
|
||||||
|
|
||||||
deriveTOMs,
|
|
||||||
updateDerivedTOM,
|
|
||||||
bulkUpdateTOMs,
|
|
||||||
|
|
||||||
runGapAnalysis,
|
|
||||||
|
|
||||||
addExport,
|
|
||||||
|
|
||||||
saveState,
|
|
||||||
loadState,
|
|
||||||
resetState,
|
|
||||||
|
|
||||||
isStepCompleted,
|
|
||||||
getCompletionPercentage,
|
|
||||||
isLoading,
|
|
||||||
error,
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TOMGeneratorContext.Provider value={contextValue}>
|
|
||||||
{children}
|
|
||||||
</TOMGeneratorContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// HOOK
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
export function useTOMGenerator(): TOMGeneratorContextValue {
|
|
||||||
const context = useContext(TOMGeneratorContext)
|
|
||||||
if (!context) {
|
|
||||||
throw new Error(
|
|
||||||
'useTOMGenerator must be used within a TOMGeneratorProvider'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return context
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// EXPORTS
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
export { TOMGeneratorContext }
|
|
||||||
export type { TOMGeneratorAction, TOMGeneratorContextValue }
|
|
||||||
|
|||||||
20
admin-compliance/lib/sdk/tom-generator/hooks.tsx
Normal file
20
admin-compliance/lib/sdk/tom-generator/hooks.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// TOM Generator Hook
|
||||||
|
// Custom hook for consuming the TOM Generator context
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
import { useContext } from 'react'
|
||||||
|
import { TOMGeneratorContext } from './provider'
|
||||||
|
import type { TOMGeneratorContextValue } from './provider'
|
||||||
|
|
||||||
|
export function useTOMGenerator(): TOMGeneratorContextValue {
|
||||||
|
const context = useContext(TOMGeneratorContext)
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(
|
||||||
|
'useTOMGenerator must be used within a TOMGeneratorProvider'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
}
|
||||||
473
admin-compliance/lib/sdk/tom-generator/provider.tsx
Normal file
473
admin-compliance/lib/sdk/tom-generator/provider.tsx
Normal file
@@ -0,0 +1,473 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// TOM Generator Provider
|
||||||
|
// Context provider component for the TOM Generator Wizard
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
import React, {
|
||||||
|
createContext,
|
||||||
|
useReducer,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
ReactNode,
|
||||||
|
} from 'react'
|
||||||
|
import {
|
||||||
|
TOMGeneratorState,
|
||||||
|
TOMGeneratorStepId,
|
||||||
|
CompanyProfile,
|
||||||
|
DataProfile,
|
||||||
|
ArchitectureProfile,
|
||||||
|
SecurityProfile,
|
||||||
|
RiskProfile,
|
||||||
|
EvidenceDocument,
|
||||||
|
DerivedTOM,
|
||||||
|
ExportRecord,
|
||||||
|
WizardStep,
|
||||||
|
createInitialTOMGeneratorState,
|
||||||
|
TOM_GENERATOR_STEPS,
|
||||||
|
getStepIndex,
|
||||||
|
} from './types'
|
||||||
|
import { TOMRulesEngine } from './rules-engine'
|
||||||
|
import { tomGeneratorReducer, TOMGeneratorAction } from './reducer'
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// CONTEXT VALUE INTERFACE
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export interface TOMGeneratorContextValue {
|
||||||
|
state: TOMGeneratorState
|
||||||
|
dispatch: React.Dispatch<TOMGeneratorAction>
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
currentStepIndex: number
|
||||||
|
totalSteps: number
|
||||||
|
canGoNext: boolean
|
||||||
|
canGoPrevious: boolean
|
||||||
|
goToStep: (stepId: TOMGeneratorStepId) => void
|
||||||
|
goToNextStep: () => void
|
||||||
|
goToPreviousStep: () => void
|
||||||
|
completeCurrentStep: (data: unknown) => void
|
||||||
|
|
||||||
|
// Profile setters
|
||||||
|
setCompanyProfile: (profile: CompanyProfile) => void
|
||||||
|
updateCompanyProfile: (data: Partial<CompanyProfile>) => void
|
||||||
|
setDataProfile: (profile: DataProfile) => void
|
||||||
|
updateDataProfile: (data: Partial<DataProfile>) => void
|
||||||
|
setArchitectureProfile: (profile: ArchitectureProfile) => void
|
||||||
|
updateArchitectureProfile: (data: Partial<ArchitectureProfile>) => void
|
||||||
|
setSecurityProfile: (profile: SecurityProfile) => void
|
||||||
|
updateSecurityProfile: (data: Partial<SecurityProfile>) => void
|
||||||
|
setRiskProfile: (profile: RiskProfile) => void
|
||||||
|
updateRiskProfile: (data: Partial<RiskProfile>) => void
|
||||||
|
|
||||||
|
// Evidence management
|
||||||
|
addEvidence: (document: EvidenceDocument) => void
|
||||||
|
updateEvidence: (id: string, data: Partial<EvidenceDocument>) => void
|
||||||
|
deleteEvidence: (id: string) => void
|
||||||
|
|
||||||
|
// TOM derivation
|
||||||
|
deriveTOMs: () => void
|
||||||
|
updateDerivedTOM: (id: string, data: Partial<DerivedTOM>) => void
|
||||||
|
bulkUpdateTOMs: (updates: Array<{ id: string; data: Partial<DerivedTOM> }>) => void
|
||||||
|
|
||||||
|
// Gap analysis
|
||||||
|
runGapAnalysis: () => void
|
||||||
|
|
||||||
|
// Export
|
||||||
|
addExport: (record: ExportRecord) => void
|
||||||
|
|
||||||
|
// Persistence
|
||||||
|
saveState: () => Promise<void>
|
||||||
|
loadState: () => Promise<void>
|
||||||
|
resetState: () => void
|
||||||
|
|
||||||
|
// Status
|
||||||
|
isStepCompleted: (stepId: TOMGeneratorStepId) => boolean
|
||||||
|
getCompletionPercentage: () => number
|
||||||
|
isLoading: boolean
|
||||||
|
error: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// CONTEXT
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export const TOMGeneratorContext = createContext<TOMGeneratorContextValue | null>(null)
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// STORAGE KEYS
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
const STORAGE_KEY_PREFIX = 'tom-generator-state-'
|
||||||
|
|
||||||
|
function getStorageKey(tenantId: string): string {
|
||||||
|
return `${STORAGE_KEY_PREFIX}${tenantId}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// PROVIDER COMPONENT
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
interface TOMGeneratorProviderProps {
|
||||||
|
children: ReactNode
|
||||||
|
tenantId: string
|
||||||
|
initialState?: TOMGeneratorState
|
||||||
|
enablePersistence?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TOMGeneratorProvider({
|
||||||
|
children,
|
||||||
|
tenantId,
|
||||||
|
initialState,
|
||||||
|
enablePersistence = true,
|
||||||
|
}: TOMGeneratorProviderProps) {
|
||||||
|
const [state, dispatch] = useReducer(
|
||||||
|
tomGeneratorReducer,
|
||||||
|
initialState ?? createInitialTOMGeneratorState(tenantId)
|
||||||
|
)
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = React.useState(false)
|
||||||
|
const [error, setError] = React.useState<string | null>(null)
|
||||||
|
|
||||||
|
const rulesEngineRef = useRef<TOMRulesEngine | null>(null)
|
||||||
|
|
||||||
|
// Initialize rules engine
|
||||||
|
useEffect(() => {
|
||||||
|
if (!rulesEngineRef.current) {
|
||||||
|
rulesEngineRef.current = new TOMRulesEngine()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Load state from localStorage on mount
|
||||||
|
useEffect(() => {
|
||||||
|
if (enablePersistence && typeof window !== 'undefined') {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem(getStorageKey(tenantId))
|
||||||
|
if (stored) {
|
||||||
|
const parsed = JSON.parse(stored)
|
||||||
|
if (parsed.createdAt) parsed.createdAt = new Date(parsed.createdAt)
|
||||||
|
if (parsed.updatedAt) parsed.updatedAt = new Date(parsed.updatedAt)
|
||||||
|
if (parsed.steps) {
|
||||||
|
parsed.steps = parsed.steps.map((step: WizardStep) => ({
|
||||||
|
...step,
|
||||||
|
validatedAt: step.validatedAt ? new Date(step.validatedAt) : null,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
if (parsed.documents) {
|
||||||
|
parsed.documents = parsed.documents.map((doc: EvidenceDocument) => ({
|
||||||
|
...doc,
|
||||||
|
uploadedAt: new Date(doc.uploadedAt),
|
||||||
|
validFrom: doc.validFrom ? new Date(doc.validFrom) : null,
|
||||||
|
validUntil: doc.validUntil ? new Date(doc.validUntil) : null,
|
||||||
|
aiAnalysis: doc.aiAnalysis
|
||||||
|
? {
|
||||||
|
...doc.aiAnalysis,
|
||||||
|
analyzedAt: new Date(doc.aiAnalysis.analyzedAt),
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
if (parsed.derivedTOMs) {
|
||||||
|
parsed.derivedTOMs = parsed.derivedTOMs.map((tom: DerivedTOM) => ({
|
||||||
|
...tom,
|
||||||
|
implementationDate: tom.implementationDate
|
||||||
|
? new Date(tom.implementationDate)
|
||||||
|
: null,
|
||||||
|
reviewDate: tom.reviewDate ? new Date(tom.reviewDate) : null,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
if (parsed.gapAnalysis?.generatedAt) {
|
||||||
|
parsed.gapAnalysis.generatedAt = new Date(parsed.gapAnalysis.generatedAt)
|
||||||
|
}
|
||||||
|
if (parsed.exports) {
|
||||||
|
parsed.exports = parsed.exports.map((exp: ExportRecord) => ({
|
||||||
|
...exp,
|
||||||
|
generatedAt: new Date(exp.generatedAt),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
dispatch({ type: 'LOAD_STATE', payload: parsed })
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load TOM Generator state from localStorage:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [tenantId, enablePersistence])
|
||||||
|
|
||||||
|
// Save state to localStorage on changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (enablePersistence && typeof window !== 'undefined') {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(getStorageKey(tenantId), JSON.stringify(state))
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to save TOM Generator state to localStorage:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [state, tenantId, enablePersistence])
|
||||||
|
|
||||||
|
// Navigation helpers
|
||||||
|
const currentStepIndex = getStepIndex(state.currentStep)
|
||||||
|
const totalSteps = TOM_GENERATOR_STEPS.length
|
||||||
|
|
||||||
|
const canGoNext = currentStepIndex < totalSteps - 1
|
||||||
|
const canGoPrevious = currentStepIndex > 0
|
||||||
|
|
||||||
|
const goToStep = useCallback((stepId: TOMGeneratorStepId) => {
|
||||||
|
dispatch({ type: 'SET_CURRENT_STEP', payload: stepId })
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const goToNextStep = useCallback(() => {
|
||||||
|
if (canGoNext) {
|
||||||
|
const nextStep = TOM_GENERATOR_STEPS[currentStepIndex + 1]
|
||||||
|
dispatch({ type: 'SET_CURRENT_STEP', payload: nextStep.id })
|
||||||
|
}
|
||||||
|
}, [canGoNext, currentStepIndex])
|
||||||
|
|
||||||
|
const goToPreviousStep = useCallback(() => {
|
||||||
|
if (canGoPrevious) {
|
||||||
|
const prevStep = TOM_GENERATOR_STEPS[currentStepIndex - 1]
|
||||||
|
dispatch({ type: 'SET_CURRENT_STEP', payload: prevStep.id })
|
||||||
|
}
|
||||||
|
}, [canGoPrevious, currentStepIndex])
|
||||||
|
|
||||||
|
const completeCurrentStep = useCallback(
|
||||||
|
(data: unknown) => {
|
||||||
|
dispatch({
|
||||||
|
type: 'COMPLETE_STEP',
|
||||||
|
payload: { stepId: state.currentStep, data },
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[state.currentStep]
|
||||||
|
)
|
||||||
|
|
||||||
|
// Profile setters
|
||||||
|
const setCompanyProfile = useCallback((profile: CompanyProfile) => {
|
||||||
|
dispatch({ type: 'SET_COMPANY_PROFILE', payload: profile })
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const updateCompanyProfile = useCallback((data: Partial<CompanyProfile>) => {
|
||||||
|
dispatch({ type: 'UPDATE_COMPANY_PROFILE', payload: data })
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const setDataProfile = useCallback((profile: DataProfile) => {
|
||||||
|
dispatch({ type: 'SET_DATA_PROFILE', payload: profile })
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const updateDataProfile = useCallback((data: Partial<DataProfile>) => {
|
||||||
|
dispatch({ type: 'UPDATE_DATA_PROFILE', payload: data })
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const setArchitectureProfile = useCallback((profile: ArchitectureProfile) => {
|
||||||
|
dispatch({ type: 'SET_ARCHITECTURE_PROFILE', payload: profile })
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const updateArchitectureProfile = useCallback(
|
||||||
|
(data: Partial<ArchitectureProfile>) => {
|
||||||
|
dispatch({ type: 'UPDATE_ARCHITECTURE_PROFILE', payload: data })
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
const setSecurityProfile = useCallback((profile: SecurityProfile) => {
|
||||||
|
dispatch({ type: 'SET_SECURITY_PROFILE', payload: profile })
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const updateSecurityProfile = useCallback((data: Partial<SecurityProfile>) => {
|
||||||
|
dispatch({ type: 'UPDATE_SECURITY_PROFILE', payload: data })
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const setRiskProfile = useCallback((profile: RiskProfile) => {
|
||||||
|
dispatch({ type: 'SET_RISK_PROFILE', payload: profile })
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const updateRiskProfile = useCallback((data: Partial<RiskProfile>) => {
|
||||||
|
dispatch({ type: 'UPDATE_RISK_PROFILE', payload: data })
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Evidence management
|
||||||
|
const addEvidence = useCallback((document: EvidenceDocument) => {
|
||||||
|
dispatch({ type: 'ADD_EVIDENCE', payload: document })
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const updateEvidence = useCallback(
|
||||||
|
(id: string, data: Partial<EvidenceDocument>) => {
|
||||||
|
dispatch({ type: 'UPDATE_EVIDENCE', payload: { id, data } })
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
const deleteEvidence = useCallback((id: string) => {
|
||||||
|
dispatch({ type: 'DELETE_EVIDENCE', payload: id })
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// TOM derivation
|
||||||
|
const deriveTOMs = useCallback(() => {
|
||||||
|
if (!rulesEngineRef.current) return
|
||||||
|
|
||||||
|
const derivedTOMs = rulesEngineRef.current.deriveAllTOMs({
|
||||||
|
companyProfile: state.companyProfile,
|
||||||
|
dataProfile: state.dataProfile,
|
||||||
|
architectureProfile: state.architectureProfile,
|
||||||
|
securityProfile: state.securityProfile,
|
||||||
|
riskProfile: state.riskProfile,
|
||||||
|
})
|
||||||
|
|
||||||
|
dispatch({ type: 'SET_DERIVED_TOMS', payload: derivedTOMs })
|
||||||
|
}, [
|
||||||
|
state.companyProfile,
|
||||||
|
state.dataProfile,
|
||||||
|
state.architectureProfile,
|
||||||
|
state.securityProfile,
|
||||||
|
state.riskProfile,
|
||||||
|
])
|
||||||
|
|
||||||
|
const updateDerivedTOM = useCallback(
|
||||||
|
(id: string, data: Partial<DerivedTOM>) => {
|
||||||
|
dispatch({ type: 'UPDATE_DERIVED_TOM', payload: { id, data } })
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
const bulkUpdateTOMs = useCallback(
|
||||||
|
(updates: Array<{ id: string; data: Partial<DerivedTOM> }>) => {
|
||||||
|
for (const { id, data } of updates) {
|
||||||
|
dispatch({ type: 'UPDATE_DERIVED_TOM', payload: { id, data } })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
// Gap analysis
|
||||||
|
const runGapAnalysis = useCallback(() => {
|
||||||
|
if (!rulesEngineRef.current) return
|
||||||
|
|
||||||
|
const result = rulesEngineRef.current.performGapAnalysis(
|
||||||
|
state.derivedTOMs,
|
||||||
|
state.documents
|
||||||
|
)
|
||||||
|
|
||||||
|
dispatch({ type: 'SET_GAP_ANALYSIS', payload: result })
|
||||||
|
}, [state.derivedTOMs, state.documents])
|
||||||
|
|
||||||
|
// Export
|
||||||
|
const addExport = useCallback((record: ExportRecord) => {
|
||||||
|
dispatch({ type: 'ADD_EXPORT', payload: record })
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Persistence
|
||||||
|
const saveState = useCallback(async () => {
|
||||||
|
setIsLoading(true)
|
||||||
|
setError(null)
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/sdk/v1/tom-generator/state', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ tenantId, state }),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to save state')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setError(e instanceof Error ? e.message : 'Unknown error')
|
||||||
|
throw e
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}, [tenantId, state])
|
||||||
|
|
||||||
|
const loadState = useCallback(async () => {
|
||||||
|
setIsLoading(true)
|
||||||
|
setError(null)
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/sdk/v1/tom-generator/state?tenantId=${tenantId}`
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to load state')
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
if (data.state) {
|
||||||
|
dispatch({ type: 'LOAD_STATE', payload: data.state })
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setError(e instanceof Error ? e.message : 'Unknown error')
|
||||||
|
throw e
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}, [tenantId])
|
||||||
|
|
||||||
|
const resetState = useCallback(() => {
|
||||||
|
dispatch({ type: 'RESET', payload: { tenantId } })
|
||||||
|
}, [tenantId])
|
||||||
|
|
||||||
|
// Status helpers
|
||||||
|
const isStepCompleted = useCallback(
|
||||||
|
(stepId: TOMGeneratorStepId) => {
|
||||||
|
const step = state.steps.find((s) => s.id === stepId)
|
||||||
|
return step?.completed ?? false
|
||||||
|
},
|
||||||
|
[state.steps]
|
||||||
|
)
|
||||||
|
|
||||||
|
const getCompletionPercentage = useCallback(() => {
|
||||||
|
const completedSteps = state.steps.filter((s) => s.completed).length
|
||||||
|
return Math.round((completedSteps / totalSteps) * 100)
|
||||||
|
}, [state.steps, totalSteps])
|
||||||
|
|
||||||
|
const contextValue: TOMGeneratorContextValue = {
|
||||||
|
state,
|
||||||
|
dispatch,
|
||||||
|
|
||||||
|
currentStepIndex,
|
||||||
|
totalSteps,
|
||||||
|
canGoNext,
|
||||||
|
canGoPrevious,
|
||||||
|
goToStep,
|
||||||
|
goToNextStep,
|
||||||
|
goToPreviousStep,
|
||||||
|
completeCurrentStep,
|
||||||
|
|
||||||
|
setCompanyProfile,
|
||||||
|
updateCompanyProfile,
|
||||||
|
setDataProfile,
|
||||||
|
updateDataProfile,
|
||||||
|
setArchitectureProfile,
|
||||||
|
updateArchitectureProfile,
|
||||||
|
setSecurityProfile,
|
||||||
|
updateSecurityProfile,
|
||||||
|
setRiskProfile,
|
||||||
|
updateRiskProfile,
|
||||||
|
|
||||||
|
addEvidence,
|
||||||
|
updateEvidence,
|
||||||
|
deleteEvidence,
|
||||||
|
|
||||||
|
deriveTOMs,
|
||||||
|
updateDerivedTOM,
|
||||||
|
bulkUpdateTOMs,
|
||||||
|
|
||||||
|
runGapAnalysis,
|
||||||
|
|
||||||
|
addExport,
|
||||||
|
|
||||||
|
saveState,
|
||||||
|
loadState,
|
||||||
|
resetState,
|
||||||
|
|
||||||
|
isStepCompleted,
|
||||||
|
getCompletionPercentage,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TOMGeneratorContext.Provider value={contextValue}>
|
||||||
|
{children}
|
||||||
|
</TOMGeneratorContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
238
admin-compliance/lib/sdk/tom-generator/reducer.ts
Normal file
238
admin-compliance/lib/sdk/tom-generator/reducer.ts
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
// =============================================================================
|
||||||
|
// TOM Generator Reducer
|
||||||
|
// Action types and state reducer for the TOM Generator Wizard
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
import {
|
||||||
|
TOMGeneratorState,
|
||||||
|
TOMGeneratorStepId,
|
||||||
|
CompanyProfile,
|
||||||
|
DataProfile,
|
||||||
|
ArchitectureProfile,
|
||||||
|
SecurityProfile,
|
||||||
|
RiskProfile,
|
||||||
|
EvidenceDocument,
|
||||||
|
DerivedTOM,
|
||||||
|
GapAnalysisResult,
|
||||||
|
ExportRecord,
|
||||||
|
WizardStep,
|
||||||
|
createInitialTOMGeneratorState,
|
||||||
|
calculateProtectionLevel,
|
||||||
|
isDSFARequired,
|
||||||
|
hasSpecialCategories,
|
||||||
|
} from './types'
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// ACTION TYPES
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export type TOMGeneratorAction =
|
||||||
|
| { type: 'INITIALIZE'; payload: { tenantId: string; state?: TOMGeneratorState } }
|
||||||
|
| { type: 'RESET'; payload: { tenantId: string } }
|
||||||
|
| { type: 'SET_CURRENT_STEP'; payload: TOMGeneratorStepId }
|
||||||
|
| { type: 'SET_COMPANY_PROFILE'; payload: CompanyProfile }
|
||||||
|
| { type: 'UPDATE_COMPANY_PROFILE'; payload: Partial<CompanyProfile> }
|
||||||
|
| { type: 'SET_DATA_PROFILE'; payload: DataProfile }
|
||||||
|
| { type: 'UPDATE_DATA_PROFILE'; payload: Partial<DataProfile> }
|
||||||
|
| { type: 'SET_ARCHITECTURE_PROFILE'; payload: ArchitectureProfile }
|
||||||
|
| { type: 'UPDATE_ARCHITECTURE_PROFILE'; payload: Partial<ArchitectureProfile> }
|
||||||
|
| { type: 'SET_SECURITY_PROFILE'; payload: SecurityProfile }
|
||||||
|
| { type: 'UPDATE_SECURITY_PROFILE'; payload: Partial<SecurityProfile> }
|
||||||
|
| { type: 'SET_RISK_PROFILE'; payload: RiskProfile }
|
||||||
|
| { type: 'UPDATE_RISK_PROFILE'; payload: Partial<RiskProfile> }
|
||||||
|
| { type: 'COMPLETE_STEP'; payload: { stepId: TOMGeneratorStepId; data: unknown } }
|
||||||
|
| { type: 'UNCOMPLETE_STEP'; payload: TOMGeneratorStepId }
|
||||||
|
| { type: 'ADD_EVIDENCE'; payload: EvidenceDocument }
|
||||||
|
| { type: 'UPDATE_EVIDENCE'; payload: { id: string; data: Partial<EvidenceDocument> } }
|
||||||
|
| { type: 'DELETE_EVIDENCE'; payload: string }
|
||||||
|
| { type: 'SET_DERIVED_TOMS'; payload: DerivedTOM[] }
|
||||||
|
| { type: 'UPDATE_DERIVED_TOM'; payload: { id: string; data: Partial<DerivedTOM> } }
|
||||||
|
| { type: 'SET_GAP_ANALYSIS'; payload: GapAnalysisResult }
|
||||||
|
| { type: 'ADD_EXPORT'; payload: ExportRecord }
|
||||||
|
| { type: 'BULK_UPDATE_TOMS'; payload: { updates: Array<{ id: string; data: Partial<DerivedTOM> }> } }
|
||||||
|
| { type: 'LOAD_STATE'; payload: TOMGeneratorState }
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// REDUCER
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export function tomGeneratorReducer(
|
||||||
|
state: TOMGeneratorState,
|
||||||
|
action: TOMGeneratorAction
|
||||||
|
): TOMGeneratorState {
|
||||||
|
const updateState = (updates: Partial<TOMGeneratorState>): TOMGeneratorState => ({
|
||||||
|
...state,
|
||||||
|
...updates,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
|
||||||
|
switch (action.type) {
|
||||||
|
case 'INITIALIZE': {
|
||||||
|
if (action.payload.state) {
|
||||||
|
return action.payload.state
|
||||||
|
}
|
||||||
|
return createInitialTOMGeneratorState(action.payload.tenantId)
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'RESET': {
|
||||||
|
return createInitialTOMGeneratorState(action.payload.tenantId)
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'SET_CURRENT_STEP': {
|
||||||
|
return updateState({ currentStep: action.payload })
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'SET_COMPANY_PROFILE': {
|
||||||
|
return updateState({ companyProfile: action.payload })
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'UPDATE_COMPANY_PROFILE': {
|
||||||
|
if (!state.companyProfile) return state
|
||||||
|
return updateState({
|
||||||
|
companyProfile: { ...state.companyProfile, ...action.payload },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'SET_DATA_PROFILE': {
|
||||||
|
const profile: DataProfile = {
|
||||||
|
...action.payload,
|
||||||
|
hasSpecialCategories: hasSpecialCategories(action.payload.categories),
|
||||||
|
}
|
||||||
|
return updateState({ dataProfile: profile })
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'UPDATE_DATA_PROFILE': {
|
||||||
|
if (!state.dataProfile) return state
|
||||||
|
const updatedProfile = { ...state.dataProfile, ...action.payload }
|
||||||
|
if (action.payload.categories) {
|
||||||
|
updatedProfile.hasSpecialCategories = hasSpecialCategories(
|
||||||
|
action.payload.categories
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return updateState({ dataProfile: updatedProfile })
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'SET_ARCHITECTURE_PROFILE': {
|
||||||
|
return updateState({ architectureProfile: action.payload })
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'UPDATE_ARCHITECTURE_PROFILE': {
|
||||||
|
if (!state.architectureProfile) return state
|
||||||
|
return updateState({
|
||||||
|
architectureProfile: { ...state.architectureProfile, ...action.payload },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'SET_SECURITY_PROFILE': {
|
||||||
|
return updateState({ securityProfile: action.payload })
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'UPDATE_SECURITY_PROFILE': {
|
||||||
|
if (!state.securityProfile) return state
|
||||||
|
return updateState({
|
||||||
|
securityProfile: { ...state.securityProfile, ...action.payload },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'SET_RISK_PROFILE': {
|
||||||
|
const profile: RiskProfile = {
|
||||||
|
...action.payload,
|
||||||
|
protectionLevel: calculateProtectionLevel(action.payload.ciaAssessment),
|
||||||
|
dsfaRequired: isDSFARequired(state.dataProfile, action.payload),
|
||||||
|
}
|
||||||
|
return updateState({ riskProfile: profile })
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'UPDATE_RISK_PROFILE': {
|
||||||
|
if (!state.riskProfile) return state
|
||||||
|
const updatedProfile = { ...state.riskProfile, ...action.payload }
|
||||||
|
if (action.payload.ciaAssessment) {
|
||||||
|
updatedProfile.protectionLevel = calculateProtectionLevel(
|
||||||
|
action.payload.ciaAssessment
|
||||||
|
)
|
||||||
|
}
|
||||||
|
updatedProfile.dsfaRequired = isDSFARequired(state.dataProfile, updatedProfile)
|
||||||
|
return updateState({ riskProfile: updatedProfile })
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'COMPLETE_STEP': {
|
||||||
|
const updatedSteps = state.steps.map((step) =>
|
||||||
|
step.id === action.payload.stepId
|
||||||
|
? {
|
||||||
|
...step,
|
||||||
|
completed: true,
|
||||||
|
data: action.payload.data,
|
||||||
|
validatedAt: new Date(),
|
||||||
|
}
|
||||||
|
: step
|
||||||
|
)
|
||||||
|
return updateState({ steps: updatedSteps })
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'UNCOMPLETE_STEP': {
|
||||||
|
const updatedSteps = state.steps.map((step) =>
|
||||||
|
step.id === action.payload
|
||||||
|
? { ...step, completed: false, validatedAt: null }
|
||||||
|
: step
|
||||||
|
)
|
||||||
|
return updateState({ steps: updatedSteps })
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'ADD_EVIDENCE': {
|
||||||
|
return updateState({
|
||||||
|
documents: [...state.documents, action.payload],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'UPDATE_EVIDENCE': {
|
||||||
|
const updatedDocuments = state.documents.map((doc) =>
|
||||||
|
doc.id === action.payload.id ? { ...doc, ...action.payload.data } : doc
|
||||||
|
)
|
||||||
|
return updateState({ documents: updatedDocuments })
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'DELETE_EVIDENCE': {
|
||||||
|
return updateState({
|
||||||
|
documents: state.documents.filter((doc) => doc.id !== action.payload),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'SET_DERIVED_TOMS': {
|
||||||
|
return updateState({ derivedTOMs: action.payload })
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'UPDATE_DERIVED_TOM': {
|
||||||
|
const updatedTOMs = state.derivedTOMs.map((tom) =>
|
||||||
|
tom.id === action.payload.id ? { ...tom, ...action.payload.data } : tom
|
||||||
|
)
|
||||||
|
return updateState({ derivedTOMs: updatedTOMs })
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'SET_GAP_ANALYSIS': {
|
||||||
|
return updateState({ gapAnalysis: action.payload })
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'ADD_EXPORT': {
|
||||||
|
return updateState({
|
||||||
|
exports: [...state.exports, action.payload],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'BULK_UPDATE_TOMS': {
|
||||||
|
let updatedTOMs = [...state.derivedTOMs]
|
||||||
|
for (const update of action.payload.updates) {
|
||||||
|
updatedTOMs = updatedTOMs.map((tom) =>
|
||||||
|
tom.id === update.id ? { ...tom, ...update.data } : tom
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return updateState({ derivedTOMs: updatedTOMs })
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'LOAD_STATE': {
|
||||||
|
return action.payload
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,30 +1,17 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import React, {
|
import React, {
|
||||||
createContext,
|
|
||||||
useContext,
|
|
||||||
useReducer,
|
useReducer,
|
||||||
useCallback,
|
|
||||||
useMemo,
|
useMemo,
|
||||||
useEffect,
|
useEffect,
|
||||||
useState,
|
useState,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
VendorComplianceState,
|
|
||||||
VendorComplianceAction,
|
|
||||||
VendorComplianceContextValue,
|
VendorComplianceContextValue,
|
||||||
ProcessingActivity,
|
|
||||||
Vendor,
|
|
||||||
ContractDocument,
|
|
||||||
Finding,
|
|
||||||
Control,
|
|
||||||
ControlInstance,
|
|
||||||
RiskAssessment,
|
|
||||||
VendorStatistics,
|
VendorStatistics,
|
||||||
ComplianceStatistics,
|
ComplianceStatistics,
|
||||||
RiskOverview,
|
RiskOverview,
|
||||||
ExportFormat,
|
|
||||||
VendorStatus,
|
VendorStatus,
|
||||||
VendorRole,
|
VendorRole,
|
||||||
RiskLevel,
|
RiskLevel,
|
||||||
@@ -33,185 +20,20 @@ import {
|
|||||||
getRiskLevelFromScore,
|
getRiskLevelFromScore,
|
||||||
} from './types'
|
} from './types'
|
||||||
|
|
||||||
// ==========================================
|
import { initialState, vendorComplianceReducer } from './reducer'
|
||||||
// INITIAL STATE
|
import { VendorComplianceContext } from './hooks'
|
||||||
// ==========================================
|
import { useVendorComplianceActions } from './use-actions'
|
||||||
|
|
||||||
const initialState: VendorComplianceState = {
|
// Re-export hooks and selectors for barrel
|
||||||
processingActivities: [],
|
export {
|
||||||
vendors: [],
|
useVendorCompliance,
|
||||||
contracts: [],
|
useVendor,
|
||||||
findings: [],
|
useProcessingActivity,
|
||||||
controls: [],
|
useVendorContracts,
|
||||||
controlInstances: [],
|
useVendorFindings,
|
||||||
riskAssessments: [],
|
useContractFindings,
|
||||||
isLoading: false,
|
useControlInstancesForEntity,
|
||||||
error: null,
|
} from './hooks'
|
||||||
selectedVendorId: null,
|
|
||||||
selectedActivityId: null,
|
|
||||||
activeTab: 'overview',
|
|
||||||
lastModified: null,
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// REDUCER
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
function vendorComplianceReducer(
|
|
||||||
state: VendorComplianceState,
|
|
||||||
action: VendorComplianceAction
|
|
||||||
): VendorComplianceState {
|
|
||||||
const updateState = (updates: Partial<VendorComplianceState>): VendorComplianceState => ({
|
|
||||||
...state,
|
|
||||||
...updates,
|
|
||||||
lastModified: new Date(),
|
|
||||||
})
|
|
||||||
|
|
||||||
switch (action.type) {
|
|
||||||
// Processing Activities
|
|
||||||
case 'SET_PROCESSING_ACTIVITIES':
|
|
||||||
return updateState({ processingActivities: action.payload })
|
|
||||||
|
|
||||||
case 'ADD_PROCESSING_ACTIVITY':
|
|
||||||
return updateState({
|
|
||||||
processingActivities: [...state.processingActivities, action.payload],
|
|
||||||
})
|
|
||||||
|
|
||||||
case 'UPDATE_PROCESSING_ACTIVITY':
|
|
||||||
return updateState({
|
|
||||||
processingActivities: state.processingActivities.map((activity) =>
|
|
||||||
activity.id === action.payload.id
|
|
||||||
? { ...activity, ...action.payload.data, updatedAt: new Date() }
|
|
||||||
: activity
|
|
||||||
),
|
|
||||||
})
|
|
||||||
|
|
||||||
case 'DELETE_PROCESSING_ACTIVITY':
|
|
||||||
return updateState({
|
|
||||||
processingActivities: state.processingActivities.filter(
|
|
||||||
(activity) => activity.id !== action.payload
|
|
||||||
),
|
|
||||||
})
|
|
||||||
|
|
||||||
// Vendors
|
|
||||||
case 'SET_VENDORS':
|
|
||||||
return updateState({ vendors: action.payload })
|
|
||||||
|
|
||||||
case 'ADD_VENDOR':
|
|
||||||
return updateState({
|
|
||||||
vendors: [...state.vendors, action.payload],
|
|
||||||
})
|
|
||||||
|
|
||||||
case 'UPDATE_VENDOR':
|
|
||||||
return updateState({
|
|
||||||
vendors: state.vendors.map((vendor) =>
|
|
||||||
vendor.id === action.payload.id
|
|
||||||
? { ...vendor, ...action.payload.data, updatedAt: new Date() }
|
|
||||||
: vendor
|
|
||||||
),
|
|
||||||
})
|
|
||||||
|
|
||||||
case 'DELETE_VENDOR':
|
|
||||||
return updateState({
|
|
||||||
vendors: state.vendors.filter((vendor) => vendor.id !== action.payload),
|
|
||||||
})
|
|
||||||
|
|
||||||
// Contracts
|
|
||||||
case 'SET_CONTRACTS':
|
|
||||||
return updateState({ contracts: action.payload })
|
|
||||||
|
|
||||||
case 'ADD_CONTRACT':
|
|
||||||
return updateState({
|
|
||||||
contracts: [...state.contracts, action.payload],
|
|
||||||
})
|
|
||||||
|
|
||||||
case 'UPDATE_CONTRACT':
|
|
||||||
return updateState({
|
|
||||||
contracts: state.contracts.map((contract) =>
|
|
||||||
contract.id === action.payload.id
|
|
||||||
? { ...contract, ...action.payload.data, updatedAt: new Date() }
|
|
||||||
: contract
|
|
||||||
),
|
|
||||||
})
|
|
||||||
|
|
||||||
case 'DELETE_CONTRACT':
|
|
||||||
return updateState({
|
|
||||||
contracts: state.contracts.filter((contract) => contract.id !== action.payload),
|
|
||||||
})
|
|
||||||
|
|
||||||
// Findings
|
|
||||||
case 'SET_FINDINGS':
|
|
||||||
return updateState({ findings: action.payload })
|
|
||||||
|
|
||||||
case 'ADD_FINDINGS':
|
|
||||||
return updateState({
|
|
||||||
findings: [...state.findings, ...action.payload],
|
|
||||||
})
|
|
||||||
|
|
||||||
case 'UPDATE_FINDING':
|
|
||||||
return updateState({
|
|
||||||
findings: state.findings.map((finding) =>
|
|
||||||
finding.id === action.payload.id
|
|
||||||
? { ...finding, ...action.payload.data, updatedAt: new Date() }
|
|
||||||
: finding
|
|
||||||
),
|
|
||||||
})
|
|
||||||
|
|
||||||
// Controls
|
|
||||||
case 'SET_CONTROLS':
|
|
||||||
return updateState({ controls: action.payload })
|
|
||||||
|
|
||||||
case 'SET_CONTROL_INSTANCES':
|
|
||||||
return updateState({ controlInstances: action.payload })
|
|
||||||
|
|
||||||
case 'UPDATE_CONTROL_INSTANCE':
|
|
||||||
return updateState({
|
|
||||||
controlInstances: state.controlInstances.map((instance) =>
|
|
||||||
instance.id === action.payload.id
|
|
||||||
? { ...instance, ...action.payload.data }
|
|
||||||
: instance
|
|
||||||
),
|
|
||||||
})
|
|
||||||
|
|
||||||
// Risk Assessments
|
|
||||||
case 'SET_RISK_ASSESSMENTS':
|
|
||||||
return updateState({ riskAssessments: action.payload })
|
|
||||||
|
|
||||||
case 'UPDATE_RISK_ASSESSMENT':
|
|
||||||
return updateState({
|
|
||||||
riskAssessments: state.riskAssessments.map((assessment) =>
|
|
||||||
assessment.id === action.payload.id
|
|
||||||
? { ...assessment, ...action.payload.data }
|
|
||||||
: assessment
|
|
||||||
),
|
|
||||||
})
|
|
||||||
|
|
||||||
// UI State
|
|
||||||
case 'SET_LOADING':
|
|
||||||
return { ...state, isLoading: action.payload }
|
|
||||||
|
|
||||||
case 'SET_ERROR':
|
|
||||||
return { ...state, error: action.payload }
|
|
||||||
|
|
||||||
case 'SET_SELECTED_VENDOR':
|
|
||||||
return { ...state, selectedVendorId: action.payload }
|
|
||||||
|
|
||||||
case 'SET_SELECTED_ACTIVITY':
|
|
||||||
return { ...state, selectedActivityId: action.payload }
|
|
||||||
|
|
||||||
case 'SET_ACTIVE_TAB':
|
|
||||||
return { ...state, activeTab: action.payload }
|
|
||||||
|
|
||||||
default:
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// CONTEXT
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
const VendorComplianceContext = createContext<VendorComplianceContextValue | null>(null)
|
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// PROVIDER
|
// PROVIDER
|
||||||
@@ -229,6 +51,8 @@ export function VendorComplianceProvider({
|
|||||||
const [state, dispatch] = useReducer(vendorComplianceReducer, initialState)
|
const [state, dispatch] = useReducer(vendorComplianceReducer, initialState)
|
||||||
const [isInitialized, setIsInitialized] = useState(false)
|
const [isInitialized, setIsInitialized] = useState(false)
|
||||||
|
|
||||||
|
const actions = useVendorComplianceActions(state, dispatch)
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// COMPUTED VALUES
|
// COMPUTED VALUES
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -254,7 +78,7 @@ export function VendorComplianceProvider({
|
|||||||
|
|
||||||
const byRiskLevel = vendors.reduce(
|
const byRiskLevel = vendors.reduce(
|
||||||
(acc, v) => {
|
(acc, v) => {
|
||||||
const level = getRiskLevelFromScore(v.residualRiskScore / 4) // Normalize to 1-25
|
const level = getRiskLevelFromScore(v.residualRiskScore / 4)
|
||||||
acc[level] = (acc[level] || 0) + 1
|
acc[level] = (acc[level] || 0) + 1
|
||||||
return acc
|
return acc
|
||||||
},
|
},
|
||||||
@@ -375,496 +199,16 @@ export function VendorComplianceProvider({
|
|||||||
}
|
}
|
||||||
}, [state.vendors, state.findings])
|
}, [state.vendors, state.findings])
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// API CALLS
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
const apiBase = '/api/sdk/v1/vendor-compliance'
|
|
||||||
|
|
||||||
const loadData = useCallback(async () => {
|
|
||||||
dispatch({ type: 'SET_LOADING', payload: true })
|
|
||||||
dispatch({ type: 'SET_ERROR', payload: null })
|
|
||||||
|
|
||||||
try {
|
|
||||||
const [
|
|
||||||
activitiesRes,
|
|
||||||
vendorsRes,
|
|
||||||
contractsRes,
|
|
||||||
findingsRes,
|
|
||||||
controlsRes,
|
|
||||||
controlInstancesRes,
|
|
||||||
] = await Promise.all([
|
|
||||||
fetch(`${apiBase}/processing-activities`),
|
|
||||||
fetch(`${apiBase}/vendors`),
|
|
||||||
fetch(`${apiBase}/contracts`),
|
|
||||||
fetch(`${apiBase}/findings`),
|
|
||||||
fetch(`${apiBase}/controls`),
|
|
||||||
fetch(`${apiBase}/control-instances`),
|
|
||||||
])
|
|
||||||
|
|
||||||
if (activitiesRes.ok) {
|
|
||||||
const data = await activitiesRes.json()
|
|
||||||
dispatch({ type: 'SET_PROCESSING_ACTIVITIES', payload: data.data || [] })
|
|
||||||
}
|
|
||||||
|
|
||||||
if (vendorsRes.ok) {
|
|
||||||
const data = await vendorsRes.json()
|
|
||||||
dispatch({ type: 'SET_VENDORS', payload: data.data || [] })
|
|
||||||
}
|
|
||||||
|
|
||||||
if (contractsRes.ok) {
|
|
||||||
const data = await contractsRes.json()
|
|
||||||
dispatch({ type: 'SET_CONTRACTS', payload: data.data || [] })
|
|
||||||
}
|
|
||||||
|
|
||||||
if (findingsRes.ok) {
|
|
||||||
const data = await findingsRes.json()
|
|
||||||
dispatch({ type: 'SET_FINDINGS', payload: data.data || [] })
|
|
||||||
}
|
|
||||||
|
|
||||||
if (controlsRes.ok) {
|
|
||||||
const data = await controlsRes.json()
|
|
||||||
dispatch({ type: 'SET_CONTROLS', payload: data.data || [] })
|
|
||||||
}
|
|
||||||
|
|
||||||
if (controlInstancesRes.ok) {
|
|
||||||
const data = await controlInstancesRes.json()
|
|
||||||
dispatch({ type: 'SET_CONTROL_INSTANCES', payload: data.data || [] })
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load vendor compliance data:', error)
|
|
||||||
dispatch({
|
|
||||||
type: 'SET_ERROR',
|
|
||||||
payload: 'Fehler beim Laden der Daten',
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
dispatch({ type: 'SET_LOADING', payload: false })
|
|
||||||
}
|
|
||||||
}, [apiBase])
|
|
||||||
|
|
||||||
const refresh = useCallback(async () => {
|
|
||||||
await loadData()
|
|
||||||
}, [loadData])
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// PROCESSING ACTIVITIES ACTIONS
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
const createProcessingActivity = useCallback(
|
|
||||||
async (
|
|
||||||
data: Omit<ProcessingActivity, 'id' | 'tenantId' | 'createdAt' | 'updatedAt'>
|
|
||||||
): Promise<ProcessingActivity> => {
|
|
||||||
const response = await fetch(`${apiBase}/processing-activities`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.json()
|
|
||||||
throw new Error(error.error || 'Fehler beim Erstellen der Verarbeitungstätigkeit')
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await response.json()
|
|
||||||
const activity = result.data
|
|
||||||
|
|
||||||
dispatch({ type: 'ADD_PROCESSING_ACTIVITY', payload: activity })
|
|
||||||
|
|
||||||
return activity
|
|
||||||
},
|
|
||||||
[apiBase]
|
|
||||||
)
|
|
||||||
|
|
||||||
const updateProcessingActivity = useCallback(
|
|
||||||
async (id: string, data: Partial<ProcessingActivity>): Promise<void> => {
|
|
||||||
const response = await fetch(`${apiBase}/processing-activities/${id}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.json()
|
|
||||||
throw new Error(error.error || 'Fehler beim Aktualisieren der Verarbeitungstätigkeit')
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch({ type: 'UPDATE_PROCESSING_ACTIVITY', payload: { id, data } })
|
|
||||||
},
|
|
||||||
[apiBase]
|
|
||||||
)
|
|
||||||
|
|
||||||
const deleteProcessingActivity = useCallback(
|
|
||||||
async (id: string): Promise<void> => {
|
|
||||||
const response = await fetch(`${apiBase}/processing-activities/${id}`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.json()
|
|
||||||
throw new Error(error.error || 'Fehler beim Löschen der Verarbeitungstätigkeit')
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch({ type: 'DELETE_PROCESSING_ACTIVITY', payload: id })
|
|
||||||
},
|
|
||||||
[apiBase]
|
|
||||||
)
|
|
||||||
|
|
||||||
const duplicateProcessingActivity = useCallback(
|
|
||||||
async (id: string): Promise<ProcessingActivity> => {
|
|
||||||
const original = state.processingActivities.find((a) => a.id === id)
|
|
||||||
if (!original) {
|
|
||||||
throw new Error('Verarbeitungstätigkeit nicht gefunden')
|
|
||||||
}
|
|
||||||
|
|
||||||
const { id: _id, vvtId: _vvtId, createdAt: _createdAt, updatedAt: _updatedAt, tenantId: _tenantId, ...rest } = original
|
|
||||||
|
|
||||||
const newActivity = await createProcessingActivity({
|
|
||||||
...rest,
|
|
||||||
vvtId: '', // Will be generated by backend
|
|
||||||
name: {
|
|
||||||
de: `${original.name.de} (Kopie)`,
|
|
||||||
en: `${original.name.en} (Copy)`,
|
|
||||||
},
|
|
||||||
status: 'DRAFT',
|
|
||||||
})
|
|
||||||
|
|
||||||
return newActivity
|
|
||||||
},
|
|
||||||
[state.processingActivities, createProcessingActivity]
|
|
||||||
)
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// VENDOR ACTIONS
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
const createVendor = useCallback(
|
|
||||||
async (
|
|
||||||
data: Omit<Vendor, 'id' | 'tenantId' | 'createdAt' | 'updatedAt'>
|
|
||||||
): Promise<Vendor> => {
|
|
||||||
const response = await fetch(`${apiBase}/vendors`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.json()
|
|
||||||
throw new Error(error.error || 'Fehler beim Erstellen des Vendors')
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await response.json()
|
|
||||||
const vendor = result.data
|
|
||||||
|
|
||||||
dispatch({ type: 'ADD_VENDOR', payload: vendor })
|
|
||||||
|
|
||||||
return vendor
|
|
||||||
},
|
|
||||||
[apiBase]
|
|
||||||
)
|
|
||||||
|
|
||||||
const updateVendor = useCallback(
|
|
||||||
async (id: string, data: Partial<Vendor>): Promise<void> => {
|
|
||||||
const response = await fetch(`${apiBase}/vendors/${id}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.json()
|
|
||||||
throw new Error(error.error || 'Fehler beim Aktualisieren des Vendors')
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch({ type: 'UPDATE_VENDOR', payload: { id, data } })
|
|
||||||
},
|
|
||||||
[apiBase]
|
|
||||||
)
|
|
||||||
|
|
||||||
const deleteVendor = useCallback(
|
|
||||||
async (id: string): Promise<void> => {
|
|
||||||
const response = await fetch(`${apiBase}/vendors/${id}`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.json()
|
|
||||||
throw new Error(error.error || 'Fehler beim Löschen des Vendors')
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch({ type: 'DELETE_VENDOR', payload: id })
|
|
||||||
},
|
|
||||||
[apiBase]
|
|
||||||
)
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// CONTRACT ACTIONS
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
const uploadContract = useCallback(
|
|
||||||
async (
|
|
||||||
vendorId: string,
|
|
||||||
file: File,
|
|
||||||
metadata: Partial<ContractDocument>
|
|
||||||
): Promise<ContractDocument> => {
|
|
||||||
const formData = new FormData()
|
|
||||||
formData.append('file', file)
|
|
||||||
formData.append('vendorId', vendorId)
|
|
||||||
formData.append('metadata', JSON.stringify(metadata))
|
|
||||||
|
|
||||||
const response = await fetch(`${apiBase}/contracts`, {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.json()
|
|
||||||
throw new Error(error.error || 'Fehler beim Hochladen des Vertrags')
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await response.json()
|
|
||||||
const contract = result.data
|
|
||||||
|
|
||||||
dispatch({ type: 'ADD_CONTRACT', payload: contract })
|
|
||||||
|
|
||||||
// Update vendor's contracts list
|
|
||||||
const vendor = state.vendors.find((v) => v.id === vendorId)
|
|
||||||
if (vendor) {
|
|
||||||
dispatch({
|
|
||||||
type: 'UPDATE_VENDOR',
|
|
||||||
payload: {
|
|
||||||
id: vendorId,
|
|
||||||
data: { contracts: [...vendor.contracts, contract.id] },
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return contract
|
|
||||||
},
|
|
||||||
[apiBase, state.vendors]
|
|
||||||
)
|
|
||||||
|
|
||||||
const updateContract = useCallback(
|
|
||||||
async (id: string, data: Partial<ContractDocument>): Promise<void> => {
|
|
||||||
const response = await fetch(`${apiBase}/contracts/${id}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.json()
|
|
||||||
throw new Error(error.error || 'Fehler beim Aktualisieren des Vertrags')
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch({ type: 'UPDATE_CONTRACT', payload: { id, data } })
|
|
||||||
},
|
|
||||||
[apiBase]
|
|
||||||
)
|
|
||||||
|
|
||||||
const deleteContract = useCallback(
|
|
||||||
async (id: string): Promise<void> => {
|
|
||||||
const contract = state.contracts.find((c) => c.id === id)
|
|
||||||
|
|
||||||
const response = await fetch(`${apiBase}/contracts/${id}`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.json()
|
|
||||||
throw new Error(error.error || 'Fehler beim Löschen des Vertrags')
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch({ type: 'DELETE_CONTRACT', payload: id })
|
|
||||||
|
|
||||||
// Update vendor's contracts list
|
|
||||||
if (contract) {
|
|
||||||
const vendor = state.vendors.find((v) => v.id === contract.vendorId)
|
|
||||||
if (vendor) {
|
|
||||||
dispatch({
|
|
||||||
type: 'UPDATE_VENDOR',
|
|
||||||
payload: {
|
|
||||||
id: vendor.id,
|
|
||||||
data: { contracts: vendor.contracts.filter((cId) => cId !== id) },
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[apiBase, state.contracts, state.vendors]
|
|
||||||
)
|
|
||||||
|
|
||||||
const startContractReview = useCallback(
|
|
||||||
async (contractId: string): Promise<void> => {
|
|
||||||
dispatch({
|
|
||||||
type: 'UPDATE_CONTRACT',
|
|
||||||
payload: { id: contractId, data: { reviewStatus: 'IN_PROGRESS' } },
|
|
||||||
})
|
|
||||||
|
|
||||||
const response = await fetch(`${apiBase}/contracts/${contractId}/review`, {
|
|
||||||
method: 'POST',
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
dispatch({
|
|
||||||
type: 'UPDATE_CONTRACT',
|
|
||||||
payload: { id: contractId, data: { reviewStatus: 'FAILED' } },
|
|
||||||
})
|
|
||||||
const error = await response.json()
|
|
||||||
throw new Error(error.error || 'Fehler beim Starten der Vertragsprüfung')
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await response.json()
|
|
||||||
|
|
||||||
// Update contract with review results
|
|
||||||
dispatch({
|
|
||||||
type: 'UPDATE_CONTRACT',
|
|
||||||
payload: {
|
|
||||||
id: contractId,
|
|
||||||
data: {
|
|
||||||
reviewStatus: 'COMPLETED',
|
|
||||||
reviewCompletedAt: new Date(),
|
|
||||||
complianceScore: result.data.complianceScore,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// Add findings
|
|
||||||
if (result.data.findings && result.data.findings.length > 0) {
|
|
||||||
dispatch({ type: 'ADD_FINDINGS', payload: result.data.findings })
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[apiBase]
|
|
||||||
)
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// FINDINGS ACTIONS
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
const updateFinding = useCallback(
|
|
||||||
async (id: string, data: Partial<Finding>): Promise<void> => {
|
|
||||||
const response = await fetch(`${apiBase}/findings/${id}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.json()
|
|
||||||
throw new Error(error.error || 'Fehler beim Aktualisieren des Findings')
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch({ type: 'UPDATE_FINDING', payload: { id, data } })
|
|
||||||
},
|
|
||||||
[apiBase]
|
|
||||||
)
|
|
||||||
|
|
||||||
const resolveFinding = useCallback(
|
|
||||||
async (id: string, resolution: string): Promise<void> => {
|
|
||||||
await updateFinding(id, {
|
|
||||||
status: 'RESOLVED',
|
|
||||||
resolution,
|
|
||||||
resolvedAt: new Date(),
|
|
||||||
})
|
|
||||||
},
|
|
||||||
[updateFinding]
|
|
||||||
)
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// CONTROL ACTIONS
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
const updateControlInstance = useCallback(
|
|
||||||
async (id: string, data: Partial<ControlInstance>): Promise<void> => {
|
|
||||||
const response = await fetch(`${apiBase}/control-instances/${id}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.json()
|
|
||||||
throw new Error(error.error || 'Fehler beim Aktualisieren des Control-Status')
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch({ type: 'UPDATE_CONTROL_INSTANCE', payload: { id, data } })
|
|
||||||
},
|
|
||||||
[apiBase]
|
|
||||||
)
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// EXPORT ACTIONS
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
const exportVVT = useCallback(
|
|
||||||
async (format: ExportFormat, activityIds?: string[]): Promise<string> => {
|
|
||||||
const params = new URLSearchParams({ format })
|
|
||||||
if (activityIds && activityIds.length > 0) {
|
|
||||||
params.append('activityIds', activityIds.join(','))
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(`${apiBase}/export/vvt?${params}`)
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.json()
|
|
||||||
throw new Error(error.error || 'Fehler beim Exportieren des VVT')
|
|
||||||
}
|
|
||||||
|
|
||||||
const blob = await response.blob()
|
|
||||||
const url = URL.createObjectURL(blob)
|
|
||||||
|
|
||||||
return url
|
|
||||||
},
|
|
||||||
[apiBase]
|
|
||||||
)
|
|
||||||
|
|
||||||
const exportVendorAuditPack = useCallback(
|
|
||||||
async (vendorId: string, format: ExportFormat): Promise<string> => {
|
|
||||||
const params = new URLSearchParams({ format, vendorId })
|
|
||||||
|
|
||||||
const response = await fetch(`${apiBase}/export/vendor-audit?${params}`)
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.json()
|
|
||||||
throw new Error(error.error || 'Fehler beim Exportieren des Vendor Audit Packs')
|
|
||||||
}
|
|
||||||
|
|
||||||
const blob = await response.blob()
|
|
||||||
const url = URL.createObjectURL(blob)
|
|
||||||
|
|
||||||
return url
|
|
||||||
},
|
|
||||||
[apiBase]
|
|
||||||
)
|
|
||||||
|
|
||||||
const exportRoPA = useCallback(
|
|
||||||
async (format: ExportFormat): Promise<string> => {
|
|
||||||
const params = new URLSearchParams({ format })
|
|
||||||
|
|
||||||
const response = await fetch(`${apiBase}/export/ropa?${params}`)
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.json()
|
|
||||||
throw new Error(error.error || 'Fehler beim Exportieren des RoPA')
|
|
||||||
}
|
|
||||||
|
|
||||||
const blob = await response.blob()
|
|
||||||
const url = URL.createObjectURL(blob)
|
|
||||||
|
|
||||||
return url
|
|
||||||
},
|
|
||||||
[apiBase]
|
|
||||||
)
|
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// INITIALIZATION
|
// INITIALIZATION
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isInitialized) {
|
if (!isInitialized) {
|
||||||
loadData()
|
actions.loadData()
|
||||||
setIsInitialized(true)
|
setIsInitialized(true)
|
||||||
}
|
}
|
||||||
}, [isInitialized, loadData])
|
}, [isInitialized, actions])
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// CONTEXT VALUE
|
// CONTEXT VALUE
|
||||||
@@ -877,51 +221,9 @@ export function VendorComplianceProvider({
|
|||||||
vendorStats,
|
vendorStats,
|
||||||
complianceStats,
|
complianceStats,
|
||||||
riskOverview,
|
riskOverview,
|
||||||
createProcessingActivity,
|
...actions,
|
||||||
updateProcessingActivity,
|
|
||||||
deleteProcessingActivity,
|
|
||||||
duplicateProcessingActivity,
|
|
||||||
createVendor,
|
|
||||||
updateVendor,
|
|
||||||
deleteVendor,
|
|
||||||
uploadContract,
|
|
||||||
updateContract,
|
|
||||||
deleteContract,
|
|
||||||
startContractReview,
|
|
||||||
updateFinding,
|
|
||||||
resolveFinding,
|
|
||||||
updateControlInstance,
|
|
||||||
exportVVT,
|
|
||||||
exportVendorAuditPack,
|
|
||||||
exportRoPA,
|
|
||||||
loadData,
|
|
||||||
refresh,
|
|
||||||
}),
|
}),
|
||||||
[
|
[state, vendorStats, complianceStats, riskOverview, actions]
|
||||||
state,
|
|
||||||
vendorStats,
|
|
||||||
complianceStats,
|
|
||||||
riskOverview,
|
|
||||||
createProcessingActivity,
|
|
||||||
updateProcessingActivity,
|
|
||||||
deleteProcessingActivity,
|
|
||||||
duplicateProcessingActivity,
|
|
||||||
createVendor,
|
|
||||||
updateVendor,
|
|
||||||
deleteVendor,
|
|
||||||
uploadContract,
|
|
||||||
updateContract,
|
|
||||||
deleteContract,
|
|
||||||
startContractReview,
|
|
||||||
updateFinding,
|
|
||||||
resolveFinding,
|
|
||||||
updateControlInstance,
|
|
||||||
exportVVT,
|
|
||||||
exportVendorAuditPack,
|
|
||||||
exportRoPA,
|
|
||||||
loadData,
|
|
||||||
refresh,
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -930,81 +232,3 @@ export function VendorComplianceProvider({
|
|||||||
</VendorComplianceContext.Provider>
|
</VendorComplianceContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// HOOK
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
export function useVendorCompliance(): VendorComplianceContextValue {
|
|
||||||
const context = useContext(VendorComplianceContext)
|
|
||||||
|
|
||||||
if (!context) {
|
|
||||||
throw new Error(
|
|
||||||
'useVendorCompliance must be used within a VendorComplianceProvider'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return context
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// SELECTORS
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
export function useVendor(vendorId: string | null) {
|
|
||||||
const { vendors } = useVendorCompliance()
|
|
||||||
return useMemo(
|
|
||||||
() => vendors.find((v) => v.id === vendorId) ?? null,
|
|
||||||
[vendors, vendorId]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useProcessingActivity(activityId: string | null) {
|
|
||||||
const { processingActivities } = useVendorCompliance()
|
|
||||||
return useMemo(
|
|
||||||
() => processingActivities.find((a) => a.id === activityId) ?? null,
|
|
||||||
[processingActivities, activityId]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useVendorContracts(vendorId: string | null) {
|
|
||||||
const { contracts } = useVendorCompliance()
|
|
||||||
return useMemo(
|
|
||||||
() => contracts.filter((c) => c.vendorId === vendorId),
|
|
||||||
[contracts, vendorId]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useVendorFindings(vendorId: string | null) {
|
|
||||||
const { findings } = useVendorCompliance()
|
|
||||||
return useMemo(
|
|
||||||
() => findings.filter((f) => f.vendorId === vendorId),
|
|
||||||
[findings, vendorId]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useContractFindings(contractId: string | null) {
|
|
||||||
const { findings } = useVendorCompliance()
|
|
||||||
return useMemo(
|
|
||||||
() => findings.filter((f) => f.contractId === contractId),
|
|
||||||
[findings, contractId]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useControlInstancesForEntity(
|
|
||||||
entityType: 'VENDOR' | 'PROCESSING_ACTIVITY',
|
|
||||||
entityId: string | null
|
|
||||||
) {
|
|
||||||
const { controlInstances, controls } = useVendorCompliance()
|
|
||||||
|
|
||||||
return useMemo(() => {
|
|
||||||
if (!entityId) return []
|
|
||||||
|
|
||||||
return controlInstances
|
|
||||||
.filter((ci) => ci.entityType === entityType && ci.entityId === entityId)
|
|
||||||
.map((ci) => ({
|
|
||||||
...ci,
|
|
||||||
control: controls.find((c) => c.id === ci.controlId),
|
|
||||||
}))
|
|
||||||
}, [controlInstances, controls, entityType, entityId])
|
|
||||||
}
|
|
||||||
|
|||||||
88
admin-compliance/lib/sdk/vendor-compliance/hooks.ts
Normal file
88
admin-compliance/lib/sdk/vendor-compliance/hooks.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useContext, useMemo, createContext } from 'react'
|
||||||
|
import { VendorComplianceContextValue } from './types'
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// CONTEXT
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
export const VendorComplianceContext = createContext<VendorComplianceContextValue | null>(null)
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// HOOK
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
export function useVendorCompliance(): VendorComplianceContextValue {
|
||||||
|
const context = useContext(VendorComplianceContext)
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(
|
||||||
|
'useVendorCompliance must be used within a VendorComplianceProvider'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// SELECTORS
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
export function useVendor(vendorId: string | null) {
|
||||||
|
const { vendors } = useVendorCompliance()
|
||||||
|
return useMemo(
|
||||||
|
() => vendors.find((v) => v.id === vendorId) ?? null,
|
||||||
|
[vendors, vendorId]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useProcessingActivity(activityId: string | null) {
|
||||||
|
const { processingActivities } = useVendorCompliance()
|
||||||
|
return useMemo(
|
||||||
|
() => processingActivities.find((a) => a.id === activityId) ?? null,
|
||||||
|
[processingActivities, activityId]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useVendorContracts(vendorId: string | null) {
|
||||||
|
const { contracts } = useVendorCompliance()
|
||||||
|
return useMemo(
|
||||||
|
() => contracts.filter((c) => c.vendorId === vendorId),
|
||||||
|
[contracts, vendorId]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useVendorFindings(vendorId: string | null) {
|
||||||
|
const { findings } = useVendorCompliance()
|
||||||
|
return useMemo(
|
||||||
|
() => findings.filter((f) => f.vendorId === vendorId),
|
||||||
|
[findings, vendorId]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useContractFindings(contractId: string | null) {
|
||||||
|
const { findings } = useVendorCompliance()
|
||||||
|
return useMemo(
|
||||||
|
() => findings.filter((f) => f.contractId === contractId),
|
||||||
|
[findings, contractId]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useControlInstancesForEntity(
|
||||||
|
entityType: 'VENDOR' | 'PROCESSING_ACTIVITY',
|
||||||
|
entityId: string | null
|
||||||
|
) {
|
||||||
|
const { controlInstances, controls } = useVendorCompliance()
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
if (!entityId) return []
|
||||||
|
|
||||||
|
return controlInstances
|
||||||
|
.filter((ci) => ci.entityType === entityType && ci.entityId === entityId)
|
||||||
|
.map((ci) => ({
|
||||||
|
...ci,
|
||||||
|
control: controls.find((c) => c.id === ci.controlId),
|
||||||
|
}))
|
||||||
|
}, [controlInstances, controls, entityType, entityId])
|
||||||
|
}
|
||||||
178
admin-compliance/lib/sdk/vendor-compliance/reducer.ts
Normal file
178
admin-compliance/lib/sdk/vendor-compliance/reducer.ts
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
import {
|
||||||
|
VendorComplianceState,
|
||||||
|
VendorComplianceAction,
|
||||||
|
} from './types'
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// INITIAL STATE
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
export const initialState: VendorComplianceState = {
|
||||||
|
processingActivities: [],
|
||||||
|
vendors: [],
|
||||||
|
contracts: [],
|
||||||
|
findings: [],
|
||||||
|
controls: [],
|
||||||
|
controlInstances: [],
|
||||||
|
riskAssessments: [],
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
selectedVendorId: null,
|
||||||
|
selectedActivityId: null,
|
||||||
|
activeTab: 'overview',
|
||||||
|
lastModified: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// REDUCER
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
export function vendorComplianceReducer(
|
||||||
|
state: VendorComplianceState,
|
||||||
|
action: VendorComplianceAction
|
||||||
|
): VendorComplianceState {
|
||||||
|
const updateState = (updates: Partial<VendorComplianceState>): VendorComplianceState => ({
|
||||||
|
...state,
|
||||||
|
...updates,
|
||||||
|
lastModified: new Date(),
|
||||||
|
})
|
||||||
|
|
||||||
|
switch (action.type) {
|
||||||
|
// Processing Activities
|
||||||
|
case 'SET_PROCESSING_ACTIVITIES':
|
||||||
|
return updateState({ processingActivities: action.payload })
|
||||||
|
|
||||||
|
case 'ADD_PROCESSING_ACTIVITY':
|
||||||
|
return updateState({
|
||||||
|
processingActivities: [...state.processingActivities, action.payload],
|
||||||
|
})
|
||||||
|
|
||||||
|
case 'UPDATE_PROCESSING_ACTIVITY':
|
||||||
|
return updateState({
|
||||||
|
processingActivities: state.processingActivities.map((activity) =>
|
||||||
|
activity.id === action.payload.id
|
||||||
|
? { ...activity, ...action.payload.data, updatedAt: new Date() }
|
||||||
|
: activity
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
case 'DELETE_PROCESSING_ACTIVITY':
|
||||||
|
return updateState({
|
||||||
|
processingActivities: state.processingActivities.filter(
|
||||||
|
(activity) => activity.id !== action.payload
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Vendors
|
||||||
|
case 'SET_VENDORS':
|
||||||
|
return updateState({ vendors: action.payload })
|
||||||
|
|
||||||
|
case 'ADD_VENDOR':
|
||||||
|
return updateState({
|
||||||
|
vendors: [...state.vendors, action.payload],
|
||||||
|
})
|
||||||
|
|
||||||
|
case 'UPDATE_VENDOR':
|
||||||
|
return updateState({
|
||||||
|
vendors: state.vendors.map((vendor) =>
|
||||||
|
vendor.id === action.payload.id
|
||||||
|
? { ...vendor, ...action.payload.data, updatedAt: new Date() }
|
||||||
|
: vendor
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
case 'DELETE_VENDOR':
|
||||||
|
return updateState({
|
||||||
|
vendors: state.vendors.filter((vendor) => vendor.id !== action.payload),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Contracts
|
||||||
|
case 'SET_CONTRACTS':
|
||||||
|
return updateState({ contracts: action.payload })
|
||||||
|
|
||||||
|
case 'ADD_CONTRACT':
|
||||||
|
return updateState({
|
||||||
|
contracts: [...state.contracts, action.payload],
|
||||||
|
})
|
||||||
|
|
||||||
|
case 'UPDATE_CONTRACT':
|
||||||
|
return updateState({
|
||||||
|
contracts: state.contracts.map((contract) =>
|
||||||
|
contract.id === action.payload.id
|
||||||
|
? { ...contract, ...action.payload.data, updatedAt: new Date() }
|
||||||
|
: contract
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
case 'DELETE_CONTRACT':
|
||||||
|
return updateState({
|
||||||
|
contracts: state.contracts.filter((contract) => contract.id !== action.payload),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Findings
|
||||||
|
case 'SET_FINDINGS':
|
||||||
|
return updateState({ findings: action.payload })
|
||||||
|
|
||||||
|
case 'ADD_FINDINGS':
|
||||||
|
return updateState({
|
||||||
|
findings: [...state.findings, ...action.payload],
|
||||||
|
})
|
||||||
|
|
||||||
|
case 'UPDATE_FINDING':
|
||||||
|
return updateState({
|
||||||
|
findings: state.findings.map((finding) =>
|
||||||
|
finding.id === action.payload.id
|
||||||
|
? { ...finding, ...action.payload.data, updatedAt: new Date() }
|
||||||
|
: finding
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Controls
|
||||||
|
case 'SET_CONTROLS':
|
||||||
|
return updateState({ controls: action.payload })
|
||||||
|
|
||||||
|
case 'SET_CONTROL_INSTANCES':
|
||||||
|
return updateState({ controlInstances: action.payload })
|
||||||
|
|
||||||
|
case 'UPDATE_CONTROL_INSTANCE':
|
||||||
|
return updateState({
|
||||||
|
controlInstances: state.controlInstances.map((instance) =>
|
||||||
|
instance.id === action.payload.id
|
||||||
|
? { ...instance, ...action.payload.data }
|
||||||
|
: instance
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Risk Assessments
|
||||||
|
case 'SET_RISK_ASSESSMENTS':
|
||||||
|
return updateState({ riskAssessments: action.payload })
|
||||||
|
|
||||||
|
case 'UPDATE_RISK_ASSESSMENT':
|
||||||
|
return updateState({
|
||||||
|
riskAssessments: state.riskAssessments.map((assessment) =>
|
||||||
|
assessment.id === action.payload.id
|
||||||
|
? { ...assessment, ...action.payload.data }
|
||||||
|
: assessment
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
// UI State
|
||||||
|
case 'SET_LOADING':
|
||||||
|
return { ...state, isLoading: action.payload }
|
||||||
|
|
||||||
|
case 'SET_ERROR':
|
||||||
|
return { ...state, error: action.payload }
|
||||||
|
|
||||||
|
case 'SET_SELECTED_VENDOR':
|
||||||
|
return { ...state, selectedVendorId: action.payload }
|
||||||
|
|
||||||
|
case 'SET_SELECTED_ACTIVITY':
|
||||||
|
return { ...state, selectedActivityId: action.payload }
|
||||||
|
|
||||||
|
case 'SET_ACTIVE_TAB':
|
||||||
|
return { ...state, activeTab: action.payload }
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
}
|
||||||
448
admin-compliance/lib/sdk/vendor-compliance/use-actions.ts
Normal file
448
admin-compliance/lib/sdk/vendor-compliance/use-actions.ts
Normal file
@@ -0,0 +1,448 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useCallback } from 'react'
|
||||||
|
|
||||||
|
import {
|
||||||
|
VendorComplianceState,
|
||||||
|
VendorComplianceAction,
|
||||||
|
ProcessingActivity,
|
||||||
|
Vendor,
|
||||||
|
ContractDocument,
|
||||||
|
Finding,
|
||||||
|
ControlInstance,
|
||||||
|
ExportFormat,
|
||||||
|
} from './types'
|
||||||
|
|
||||||
|
const API_BASE = '/api/sdk/v1/vendor-compliance'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encapsulates all vendor-compliance API action callbacks.
|
||||||
|
* Called from the provider so that dispatch/state stay internal.
|
||||||
|
*/
|
||||||
|
export function useVendorComplianceActions(
|
||||||
|
state: VendorComplianceState,
|
||||||
|
dispatch: React.Dispatch<VendorComplianceAction>
|
||||||
|
) {
|
||||||
|
// ==========================================
|
||||||
|
// DATA LOADING
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
const loadData = useCallback(async () => {
|
||||||
|
dispatch({ type: 'SET_LOADING', payload: true })
|
||||||
|
dispatch({ type: 'SET_ERROR', payload: null })
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [
|
||||||
|
activitiesRes, vendorsRes, contractsRes,
|
||||||
|
findingsRes, controlsRes, controlInstancesRes,
|
||||||
|
] = await Promise.all([
|
||||||
|
fetch(`${API_BASE}/processing-activities`),
|
||||||
|
fetch(`${API_BASE}/vendors`),
|
||||||
|
fetch(`${API_BASE}/contracts`),
|
||||||
|
fetch(`${API_BASE}/findings`),
|
||||||
|
fetch(`${API_BASE}/controls`),
|
||||||
|
fetch(`${API_BASE}/control-instances`),
|
||||||
|
])
|
||||||
|
|
||||||
|
if (activitiesRes.ok) {
|
||||||
|
const data = await activitiesRes.json()
|
||||||
|
dispatch({ type: 'SET_PROCESSING_ACTIVITIES', payload: data.data || [] })
|
||||||
|
}
|
||||||
|
if (vendorsRes.ok) {
|
||||||
|
const data = await vendorsRes.json()
|
||||||
|
dispatch({ type: 'SET_VENDORS', payload: data.data || [] })
|
||||||
|
}
|
||||||
|
if (contractsRes.ok) {
|
||||||
|
const data = await contractsRes.json()
|
||||||
|
dispatch({ type: 'SET_CONTRACTS', payload: data.data || [] })
|
||||||
|
}
|
||||||
|
if (findingsRes.ok) {
|
||||||
|
const data = await findingsRes.json()
|
||||||
|
dispatch({ type: 'SET_FINDINGS', payload: data.data || [] })
|
||||||
|
}
|
||||||
|
if (controlsRes.ok) {
|
||||||
|
const data = await controlsRes.json()
|
||||||
|
dispatch({ type: 'SET_CONTROLS', payload: data.data || [] })
|
||||||
|
}
|
||||||
|
if (controlInstancesRes.ok) {
|
||||||
|
const data = await controlInstancesRes.json()
|
||||||
|
dispatch({ type: 'SET_CONTROL_INSTANCES', payload: data.data || [] })
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load vendor compliance data:', error)
|
||||||
|
dispatch({ type: 'SET_ERROR', payload: 'Fehler beim Laden der Daten' })
|
||||||
|
} finally {
|
||||||
|
dispatch({ type: 'SET_LOADING', payload: false })
|
||||||
|
}
|
||||||
|
}, [dispatch])
|
||||||
|
|
||||||
|
const refresh = useCallback(async () => {
|
||||||
|
await loadData()
|
||||||
|
}, [loadData])
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// PROCESSING ACTIVITIES
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
const createProcessingActivity = useCallback(
|
||||||
|
async (
|
||||||
|
data: Omit<ProcessingActivity, 'id' | 'tenantId' | 'createdAt' | 'updatedAt'>
|
||||||
|
): Promise<ProcessingActivity> => {
|
||||||
|
const response = await fetch(`${API_BASE}/processing-activities`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json()
|
||||||
|
throw new Error(error.error || 'Fehler beim Erstellen der Verarbeitungstätigkeit')
|
||||||
|
}
|
||||||
|
const result = await response.json()
|
||||||
|
dispatch({ type: 'ADD_PROCESSING_ACTIVITY', payload: result.data })
|
||||||
|
return result.data
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
)
|
||||||
|
|
||||||
|
const updateProcessingActivity = useCallback(
|
||||||
|
async (id: string, data: Partial<ProcessingActivity>): Promise<void> => {
|
||||||
|
const response = await fetch(`${API_BASE}/processing-activities/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json()
|
||||||
|
throw new Error(error.error || 'Fehler beim Aktualisieren der Verarbeitungstätigkeit')
|
||||||
|
}
|
||||||
|
dispatch({ type: 'UPDATE_PROCESSING_ACTIVITY', payload: { id, data } })
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
)
|
||||||
|
|
||||||
|
const deleteProcessingActivity = useCallback(
|
||||||
|
async (id: string): Promise<void> => {
|
||||||
|
const response = await fetch(`${API_BASE}/processing-activities/${id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json()
|
||||||
|
throw new Error(error.error || 'Fehler beim Löschen der Verarbeitungstätigkeit')
|
||||||
|
}
|
||||||
|
dispatch({ type: 'DELETE_PROCESSING_ACTIVITY', payload: id })
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
)
|
||||||
|
|
||||||
|
const duplicateProcessingActivity = useCallback(
|
||||||
|
async (id: string): Promise<ProcessingActivity> => {
|
||||||
|
const original = state.processingActivities.find((a) => a.id === id)
|
||||||
|
if (!original) {
|
||||||
|
throw new Error('Verarbeitungstätigkeit nicht gefunden')
|
||||||
|
}
|
||||||
|
const { id: _id, vvtId: _vvtId, createdAt: _createdAt, updatedAt: _updatedAt, tenantId: _tenantId, ...rest } = original
|
||||||
|
return createProcessingActivity({
|
||||||
|
...rest,
|
||||||
|
vvtId: '',
|
||||||
|
name: {
|
||||||
|
de: `${original.name.de} (Kopie)`,
|
||||||
|
en: `${original.name.en} (Copy)`,
|
||||||
|
},
|
||||||
|
status: 'DRAFT',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[state.processingActivities, createProcessingActivity]
|
||||||
|
)
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// VENDORS
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
const createVendor = useCallback(
|
||||||
|
async (
|
||||||
|
data: Omit<Vendor, 'id' | 'tenantId' | 'createdAt' | 'updatedAt'>
|
||||||
|
): Promise<Vendor> => {
|
||||||
|
const response = await fetch(`${API_BASE}/vendors`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json()
|
||||||
|
throw new Error(error.error || 'Fehler beim Erstellen des Vendors')
|
||||||
|
}
|
||||||
|
const result = await response.json()
|
||||||
|
dispatch({ type: 'ADD_VENDOR', payload: result.data })
|
||||||
|
return result.data
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
)
|
||||||
|
|
||||||
|
const updateVendor = useCallback(
|
||||||
|
async (id: string, data: Partial<Vendor>): Promise<void> => {
|
||||||
|
const response = await fetch(`${API_BASE}/vendors/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json()
|
||||||
|
throw new Error(error.error || 'Fehler beim Aktualisieren des Vendors')
|
||||||
|
}
|
||||||
|
dispatch({ type: 'UPDATE_VENDOR', payload: { id, data } })
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
)
|
||||||
|
|
||||||
|
const deleteVendor = useCallback(
|
||||||
|
async (id: string): Promise<void> => {
|
||||||
|
const response = await fetch(`${API_BASE}/vendors/${id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json()
|
||||||
|
throw new Error(error.error || 'Fehler beim Löschen des Vendors')
|
||||||
|
}
|
||||||
|
dispatch({ type: 'DELETE_VENDOR', payload: id })
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
)
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// CONTRACTS
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
const uploadContract = useCallback(
|
||||||
|
async (
|
||||||
|
vendorId: string,
|
||||||
|
file: File,
|
||||||
|
metadata: Partial<ContractDocument>
|
||||||
|
): Promise<ContractDocument> => {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
formData.append('vendorId', vendorId)
|
||||||
|
formData.append('metadata', JSON.stringify(metadata))
|
||||||
|
|
||||||
|
const response = await fetch(`${API_BASE}/contracts`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json()
|
||||||
|
throw new Error(error.error || 'Fehler beim Hochladen des Vertrags')
|
||||||
|
}
|
||||||
|
const result = await response.json()
|
||||||
|
const contract = result.data
|
||||||
|
dispatch({ type: 'ADD_CONTRACT', payload: contract })
|
||||||
|
|
||||||
|
const vendor = state.vendors.find((v) => v.id === vendorId)
|
||||||
|
if (vendor) {
|
||||||
|
dispatch({
|
||||||
|
type: 'UPDATE_VENDOR',
|
||||||
|
payload: { id: vendorId, data: { contracts: [...vendor.contracts, contract.id] } },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return contract
|
||||||
|
},
|
||||||
|
[dispatch, state.vendors]
|
||||||
|
)
|
||||||
|
|
||||||
|
const updateContract = useCallback(
|
||||||
|
async (id: string, data: Partial<ContractDocument>): Promise<void> => {
|
||||||
|
const response = await fetch(`${API_BASE}/contracts/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json()
|
||||||
|
throw new Error(error.error || 'Fehler beim Aktualisieren des Vertrags')
|
||||||
|
}
|
||||||
|
dispatch({ type: 'UPDATE_CONTRACT', payload: { id, data } })
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
)
|
||||||
|
|
||||||
|
const deleteContract = useCallback(
|
||||||
|
async (id: string): Promise<void> => {
|
||||||
|
const contract = state.contracts.find((c) => c.id === id)
|
||||||
|
const response = await fetch(`${API_BASE}/contracts/${id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json()
|
||||||
|
throw new Error(error.error || 'Fehler beim Löschen des Vertrags')
|
||||||
|
}
|
||||||
|
dispatch({ type: 'DELETE_CONTRACT', payload: id })
|
||||||
|
|
||||||
|
if (contract) {
|
||||||
|
const vendor = state.vendors.find((v) => v.id === contract.vendorId)
|
||||||
|
if (vendor) {
|
||||||
|
dispatch({
|
||||||
|
type: 'UPDATE_VENDOR',
|
||||||
|
payload: { id: vendor.id, data: { contracts: vendor.contracts.filter((cId) => cId !== id) } },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[dispatch, state.contracts, state.vendors]
|
||||||
|
)
|
||||||
|
|
||||||
|
const startContractReview = useCallback(
|
||||||
|
async (contractId: string): Promise<void> => {
|
||||||
|
dispatch({
|
||||||
|
type: 'UPDATE_CONTRACT',
|
||||||
|
payload: { id: contractId, data: { reviewStatus: 'IN_PROGRESS' } },
|
||||||
|
})
|
||||||
|
const response = await fetch(`${API_BASE}/contracts/${contractId}/review`, {
|
||||||
|
method: 'POST',
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
dispatch({
|
||||||
|
type: 'UPDATE_CONTRACT',
|
||||||
|
payload: { id: contractId, data: { reviewStatus: 'FAILED' } },
|
||||||
|
})
|
||||||
|
const error = await response.json()
|
||||||
|
throw new Error(error.error || 'Fehler beim Starten der Vertragsprüfung')
|
||||||
|
}
|
||||||
|
const result = await response.json()
|
||||||
|
dispatch({
|
||||||
|
type: 'UPDATE_CONTRACT',
|
||||||
|
payload: {
|
||||||
|
id: contractId,
|
||||||
|
data: {
|
||||||
|
reviewStatus: 'COMPLETED',
|
||||||
|
reviewCompletedAt: new Date(),
|
||||||
|
complianceScore: result.data.complianceScore,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if (result.data.findings && result.data.findings.length > 0) {
|
||||||
|
dispatch({ type: 'ADD_FINDINGS', payload: result.data.findings })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
)
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// FINDINGS
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
const updateFinding = useCallback(
|
||||||
|
async (id: string, data: Partial<Finding>): Promise<void> => {
|
||||||
|
const response = await fetch(`${API_BASE}/findings/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json()
|
||||||
|
throw new Error(error.error || 'Fehler beim Aktualisieren des Findings')
|
||||||
|
}
|
||||||
|
dispatch({ type: 'UPDATE_FINDING', payload: { id, data } })
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
)
|
||||||
|
|
||||||
|
const resolveFinding = useCallback(
|
||||||
|
async (id: string, resolution: string): Promise<void> => {
|
||||||
|
await updateFinding(id, {
|
||||||
|
status: 'RESOLVED',
|
||||||
|
resolution,
|
||||||
|
resolvedAt: new Date(),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[updateFinding]
|
||||||
|
)
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// CONTROLS
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
const updateControlInstance = useCallback(
|
||||||
|
async (id: string, data: Partial<ControlInstance>): Promise<void> => {
|
||||||
|
const response = await fetch(`${API_BASE}/control-instances/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json()
|
||||||
|
throw new Error(error.error || 'Fehler beim Aktualisieren des Control-Status')
|
||||||
|
}
|
||||||
|
dispatch({ type: 'UPDATE_CONTROL_INSTANCE', payload: { id, data } })
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
)
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// EXPORTS
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
const exportVVT = useCallback(
|
||||||
|
async (format: ExportFormat, activityIds?: string[]): Promise<string> => {
|
||||||
|
const params = new URLSearchParams({ format })
|
||||||
|
if (activityIds && activityIds.length > 0) {
|
||||||
|
params.append('activityIds', activityIds.join(','))
|
||||||
|
}
|
||||||
|
const response = await fetch(`${API_BASE}/export/vvt?${params}`)
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json()
|
||||||
|
throw new Error(error.error || 'Fehler beim Exportieren des VVT')
|
||||||
|
}
|
||||||
|
const blob = await response.blob()
|
||||||
|
return URL.createObjectURL(blob)
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
const exportVendorAuditPack = useCallback(
|
||||||
|
async (vendorId: string, format: ExportFormat): Promise<string> => {
|
||||||
|
const params = new URLSearchParams({ format, vendorId })
|
||||||
|
const response = await fetch(`${API_BASE}/export/vendor-audit?${params}`)
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json()
|
||||||
|
throw new Error(error.error || 'Fehler beim Exportieren des Vendor Audit Packs')
|
||||||
|
}
|
||||||
|
const blob = await response.blob()
|
||||||
|
return URL.createObjectURL(blob)
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
const exportRoPA = useCallback(
|
||||||
|
async (format: ExportFormat): Promise<string> => {
|
||||||
|
const params = new URLSearchParams({ format })
|
||||||
|
const response = await fetch(`${API_BASE}/export/ropa?${params}`)
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json()
|
||||||
|
throw new Error(error.error || 'Fehler beim Exportieren des RoPA')
|
||||||
|
}
|
||||||
|
const blob = await response.blob()
|
||||||
|
return URL.createObjectURL(blob)
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
loadData,
|
||||||
|
refresh,
|
||||||
|
createProcessingActivity,
|
||||||
|
updateProcessingActivity,
|
||||||
|
deleteProcessingActivity,
|
||||||
|
duplicateProcessingActivity,
|
||||||
|
createVendor,
|
||||||
|
updateVendor,
|
||||||
|
deleteVendor,
|
||||||
|
uploadContract,
|
||||||
|
updateContract,
|
||||||
|
deleteContract,
|
||||||
|
startContractReview,
|
||||||
|
updateFinding,
|
||||||
|
resolveFinding,
|
||||||
|
updateControlInstance,
|
||||||
|
exportVVT,
|
||||||
|
exportVendorAuditPack,
|
||||||
|
exportRoPA,
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user