refactor: Consolidate standalone services into admin-v2, add new SDK modules
Remove standalone services (ai-compliance-sdk root, developer-portal, dsms-gateway, dsms-node, night-scheduler) and legacy compliance/dsgvo pages. Add new SDK pipeline modules (academy, document-crawler, dsb-portal, incidents, whistleblower, reporting, sso, multi-tenant, industry-templates). Add drafting engine, legal corpus files (AT/CH/DE), pitch-deck, blog and Förderantrag pages. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -55,7 +55,7 @@ export const MODULE_REGISTRY: BackendModule[] = [
|
||||
]
|
||||
},
|
||||
frontend: {
|
||||
adminV2Page: '/compliance/consent',
|
||||
adminV2Page: '/sdk/consent-management',
|
||||
oldAdminPage: '/admin/consent',
|
||||
status: 'connected'
|
||||
},
|
||||
@@ -84,7 +84,7 @@ export const MODULE_REGISTRY: BackendModule[] = [
|
||||
]
|
||||
},
|
||||
frontend: {
|
||||
adminV2Page: '/compliance/workflow',
|
||||
adminV2Page: '/sdk/workflow',
|
||||
oldAdminPage: '/admin/consent (Versions Tab)',
|
||||
status: 'connected'
|
||||
},
|
||||
@@ -108,7 +108,7 @@ export const MODULE_REGISTRY: BackendModule[] = [
|
||||
]
|
||||
},
|
||||
frontend: {
|
||||
adminV2Page: '/compliance/einwilligungen',
|
||||
adminV2Page: '/sdk/einwilligungen',
|
||||
oldAdminPage: '/admin/consent (Users Tab)',
|
||||
status: 'connected',
|
||||
},
|
||||
@@ -132,7 +132,7 @@ export const MODULE_REGISTRY: BackendModule[] = [
|
||||
]
|
||||
},
|
||||
frontend: {
|
||||
adminV2Page: '/compliance/dsr',
|
||||
adminV2Page: '/sdk/dsr',
|
||||
oldAdminPage: '/admin/dsr',
|
||||
status: 'connected'
|
||||
},
|
||||
@@ -155,7 +155,7 @@ export const MODULE_REGISTRY: BackendModule[] = [
|
||||
]
|
||||
},
|
||||
frontend: {
|
||||
adminV2Page: '/compliance/dsms',
|
||||
adminV2Page: '/sdk/dsms',
|
||||
oldAdminPage: '/admin/dsms',
|
||||
status: 'connected'
|
||||
},
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
/**
|
||||
* Navigation Structure for Admin v2
|
||||
*
|
||||
* 7 main categories with color-coded modules
|
||||
* DSGVO (Datenschutz) and Compliance (Audit & GRC) are now separate
|
||||
* Main categories with color-coded modules.
|
||||
* All DSGVO and Compliance modules are now consolidated under the SDK.
|
||||
*/
|
||||
|
||||
export type CategoryId = 'dsgvo' | 'compliance' | 'compliance-sdk' | 'ai' | 'infrastructure' | 'education' | 'communication' | 'development' | 'sdk-docs'
|
||||
export type CategoryId = 'compliance-sdk' | 'ai' | 'education' | 'website' | 'sdk-docs'
|
||||
|
||||
export interface NavModule {
|
||||
id: string
|
||||
@@ -31,236 +31,7 @@ export interface NavCategory {
|
||||
|
||||
export const navigation: NavCategory[] = [
|
||||
// =========================================================================
|
||||
// DSGVO - Datenschutz-spezifische Module
|
||||
// =========================================================================
|
||||
{
|
||||
id: 'dsgvo',
|
||||
name: 'DSGVO',
|
||||
icon: 'shield-check',
|
||||
color: '#7c3aed', // Violet
|
||||
colorClass: 'dsgvo',
|
||||
description: 'Datenschutz & Betroffenenrechte',
|
||||
modules: [
|
||||
{
|
||||
id: 'consent',
|
||||
name: 'Consent Verwaltung',
|
||||
href: '/dsgvo/consent',
|
||||
description: 'Rechtliche Dokumente & Versionen',
|
||||
purpose: 'Verwalten Sie rechtliche Dokumente (AGB, Datenschutz, Cookie-Richtlinien) und deren Versionen. Jede Einwilligung eines Benutzers basiert auf diesen Dokumenten.',
|
||||
audience: ['DSB', 'Entwickler'],
|
||||
gdprArticles: ['Art. 7 (Einwilligung)', 'Art. 13/14 (Informationspflichten)'],
|
||||
oldAdminPath: '/admin/consent',
|
||||
},
|
||||
{
|
||||
id: 'dsr',
|
||||
name: 'Datenschutzanfragen (DSR)',
|
||||
href: '/dsgvo/dsr',
|
||||
description: 'DSGVO Art. 15-21 Anfragen',
|
||||
purpose: 'Bearbeiten Sie Betroffenenanfragen wie Auskunft, Loeschung und Datenportabilitaet.',
|
||||
audience: ['DSB', 'Support'],
|
||||
gdprArticles: ['Art. 15-21'],
|
||||
oldAdminPath: '/admin/dsr',
|
||||
},
|
||||
{
|
||||
id: 'einwilligungen',
|
||||
name: 'Einwilligungen',
|
||||
href: '/dsgvo/einwilligungen',
|
||||
description: 'Nutzer-Consent Uebersicht',
|
||||
purpose: 'Zentrale Uebersicht aller Nutzer-Einwilligungen (Marketing, Statistik, Cookies).',
|
||||
audience: ['DSB', 'Compliance Officer', 'Marketing'],
|
||||
gdprArticles: ['Art. 6 (Rechtmaessigkeit)', 'Art. 7 (Einwilligung)'],
|
||||
},
|
||||
{
|
||||
id: 'vvt',
|
||||
name: 'Verarbeitungsverzeichnis',
|
||||
href: '/dsgvo/vvt',
|
||||
description: 'Art. 30 DSGVO Dokumentation',
|
||||
purpose: 'Verzeichnis aller Verarbeitungstaetigkeiten mit Rechtsgrundlagen und Loeschfristen.',
|
||||
audience: ['DSB', 'Auditoren'],
|
||||
gdprArticles: ['Art. 30 (Verzeichnis von Verarbeitungstaetigkeiten)'],
|
||||
},
|
||||
{
|
||||
id: 'dsfa',
|
||||
name: 'DSFA',
|
||||
href: '/dsgvo/dsfa',
|
||||
description: 'Datenschutz-Folgenabschaetzung',
|
||||
purpose: 'Risikoanalyse fuer Verarbeitungen mit hohem Risiko gemaess Art. 35 DSGVO.',
|
||||
audience: ['DSB', 'Projektleiter'],
|
||||
gdprArticles: ['Art. 35 (Datenschutz-Folgenabschaetzung)'],
|
||||
},
|
||||
{
|
||||
id: 'tom',
|
||||
name: 'TOMs',
|
||||
href: '/dsgvo/tom',
|
||||
description: 'Technische & Organisatorische Massnahmen',
|
||||
purpose: 'Dokumentation aller Sicherheitsmassnahmen gemaess Art. 32 DSGVO.',
|
||||
audience: ['DSB', 'IT-Sicherheit', 'Auditoren'],
|
||||
gdprArticles: ['Art. 32 (Sicherheit der Verarbeitung)'],
|
||||
},
|
||||
{
|
||||
id: 'loeschfristen',
|
||||
name: 'Loeschfristen',
|
||||
href: '/dsgvo/loeschfristen',
|
||||
description: 'Datenaufbewahrung & Deadlines',
|
||||
purpose: 'Verwaltung von Aufbewahrungsfristen und automatischen Loeschungen.',
|
||||
audience: ['DSB', 'IT-Admin'],
|
||||
gdprArticles: ['Art. 5 (Speicherbegrenzung)', 'Art. 17 (Recht auf Loeschung)'],
|
||||
},
|
||||
{
|
||||
id: 'advisory-board',
|
||||
name: 'Advisory Board',
|
||||
href: '/dsgvo/advisory-board',
|
||||
description: 'KI-Use-Case Compliance-Pruefung',
|
||||
purpose: 'Bewertung geplanter KI-Use-Cases auf DSGVO-Konformitaet. Deterministische Rule Engine analysiert Machbarkeit, Risiko und Komplexitaet mit konkreten Architektur-Empfehlungen.',
|
||||
audience: ['DSB', 'Projektleiter', 'Entwickler'],
|
||||
gdprArticles: ['Art. 5', 'Art. 6', 'Art. 9', 'Art. 22', 'Art. 35'],
|
||||
},
|
||||
{
|
||||
id: 'escalations',
|
||||
name: 'Eskalations-Queue',
|
||||
href: '/dsgvo/escalations',
|
||||
description: 'DSB Review & Freigabe-Workflow',
|
||||
purpose: 'Verwaltung von Eskalationen aus dem Advisory Board (E1-E3). DSB und Team-Leads pruefen risikoreiche Use-Cases und erteilen Freigaben oder Ablehnungen mit Auflagen.',
|
||||
audience: ['DSB', 'Team-Leads', 'Legal'],
|
||||
gdprArticles: ['Art. 5', 'Art. 22', 'Art. 35', 'Art. 36'],
|
||||
},
|
||||
],
|
||||
},
|
||||
// =========================================================================
|
||||
// Compliance - Audit, GRC & Regulierung
|
||||
// =========================================================================
|
||||
{
|
||||
id: 'compliance',
|
||||
name: 'Compliance',
|
||||
icon: 'clipboard-check',
|
||||
color: '#9333ea', // Purple
|
||||
colorClass: 'compliance',
|
||||
description: 'Audit, Controls, Risiken & Regulierung',
|
||||
modules: [
|
||||
{
|
||||
id: 'hub',
|
||||
name: 'Compliance Hub',
|
||||
href: '/compliance/hub',
|
||||
description: 'Zentrales Compliance Dashboard',
|
||||
purpose: 'Zentrale Uebersicht aller Compliance-Aktivitaeten mit Score, Statistiken und Quick-Links zu allen Modulen.',
|
||||
audience: ['DSB', 'CISO', 'Compliance Officer', 'Auditoren'],
|
||||
gdprArticles: ['Art. 5 (Rechenschaftspflicht)', 'Art. 24 (Verantwortung)'],
|
||||
},
|
||||
{
|
||||
id: 'audit-checklist',
|
||||
name: 'Audit Checkliste',
|
||||
href: '/compliance/audit-checklist',
|
||||
description: '476 Anforderungen pruefen',
|
||||
purpose: 'Systematische Pruefung aller Compliance-Anforderungen mit Haupt- und Nebenabweichungen.',
|
||||
audience: ['Auditoren', 'DSB', 'CISO'],
|
||||
},
|
||||
{
|
||||
id: 'requirements',
|
||||
name: 'Requirements',
|
||||
href: '/compliance/requirements',
|
||||
description: '558+ Anforderungen aus 19 Verordnungen',
|
||||
purpose: 'Alle Compliance-Anforderungen (DSGVO, AI Act, CRA, BSI) mit Implementation-Status und Original-URLs.',
|
||||
audience: ['DSB', 'Compliance Officer', 'Entwickler', 'Auditoren'],
|
||||
},
|
||||
{
|
||||
id: 'controls',
|
||||
name: 'Controls',
|
||||
href: '/compliance/controls',
|
||||
description: '474 Control-Mappings',
|
||||
purpose: 'Alle technischen und organisatorischen Kontrollen mit Status und Nachweisen.',
|
||||
audience: ['CISO', 'Compliance Officer', 'Auditoren'],
|
||||
},
|
||||
{
|
||||
id: 'evidence',
|
||||
name: 'Evidence',
|
||||
href: '/compliance/evidence',
|
||||
description: 'Nachweise & Dokumentation',
|
||||
purpose: 'Verwalten Sie Nachweise fuer Controls (Screenshots, Logs, Policies).',
|
||||
audience: ['Compliance Officer', 'Auditoren'],
|
||||
},
|
||||
{
|
||||
id: 'risks',
|
||||
name: 'Risiken',
|
||||
href: '/compliance/risks',
|
||||
description: 'Risk Matrix & Register',
|
||||
purpose: '5x5 Risikomatrix mit Behandlungsplaenen und Verantwortlichen.',
|
||||
audience: ['CISO', 'Compliance Officer', 'Management'],
|
||||
},
|
||||
{
|
||||
id: 'audit-report',
|
||||
name: 'Audit Report',
|
||||
href: '/compliance/audit-report',
|
||||
description: 'PDF Audit-Berichte',
|
||||
purpose: 'Erstellen und verwalten Sie Audit-Sessions mit Haupt-/Nebenabweichungen und PDF-Export.',
|
||||
audience: ['DSB', 'Auditoren', 'Compliance Officer'],
|
||||
gdprArticles: ['Art. 5 (Rechenschaftspflicht)', 'Art. 24 (Verantwortung)', 'Art. 39 (Aufgaben des DSB)'],
|
||||
oldAdminPath: '/admin/docs/audit',
|
||||
},
|
||||
{
|
||||
id: 'quality',
|
||||
name: 'Qualitaet & Audit',
|
||||
href: '/compliance/quality',
|
||||
description: 'KI-Compliance & Traceability',
|
||||
purpose: 'Stichproben und Traceability fuer Compliance-Auditoren. Chunk-Suche, Requirements und Controls fuer KI-Systeme.',
|
||||
audience: ['Auditoren', 'Compliance-Beauftragte', 'QA'],
|
||||
},
|
||||
{
|
||||
id: 'modules',
|
||||
name: 'Service Registry',
|
||||
href: '/compliance/modules',
|
||||
description: '30+ Service-Module',
|
||||
purpose: 'Uebersicht aller Services mit Compliance-Status und Regulierungs-Mapping.',
|
||||
audience: ['Entwickler', 'Compliance Officer'],
|
||||
},
|
||||
{
|
||||
id: 'dsms',
|
||||
name: 'DSMS',
|
||||
href: '/compliance/dsms',
|
||||
description: 'Datenschutz-Management-System',
|
||||
purpose: 'Zentrales Management aller Datenschutz-relevanten Prozesse und Dokumentationen.',
|
||||
audience: ['DSB'],
|
||||
oldAdminPath: '/admin/dsms',
|
||||
},
|
||||
{
|
||||
id: 'workflow',
|
||||
name: 'Workflow',
|
||||
href: '/compliance/workflow',
|
||||
description: 'Freigabe-Workflows',
|
||||
purpose: 'Konfigurieren Sie Freigabe-Prozesse fuer Dokumente und Aenderungen.',
|
||||
audience: ['DSB', 'Entwickler'],
|
||||
oldAdminPath: '/admin/workflow',
|
||||
},
|
||||
{
|
||||
id: 'source-policy',
|
||||
name: 'Quellen-Policy',
|
||||
href: '/compliance/source-policy',
|
||||
description: 'Datenquellen & Compliance',
|
||||
purpose: 'Whitelist-basiertes Datenquellen-Management mit Operations-Matrix und PII-Blocklist. Nur offizielle Open-Data-Portale und amtliche Quellen (§5 UrhG). Training mit externen Daten ist VERBOTEN.',
|
||||
audience: ['DSB', 'Compliance Officer', 'Auditor'],
|
||||
gdprArticles: ['Art. 5 (Rechtmaessigkeit)', 'Art. 6 (Rechtsgrundlage)'],
|
||||
},
|
||||
{
|
||||
id: 'ai-act',
|
||||
name: 'EU-AI-Act',
|
||||
href: '/compliance/ai-act',
|
||||
description: 'KI-Risikoklassifizierung',
|
||||
purpose: 'Selbstbewertung und Dokumentation der Risikokategorien aller KI-Module gemaess EU-AI-Act. Definiert Warnlinien fuer Features, die nicht implementiert werden duerfen. Exportierbares Compliance-Memo fuer Auditoren und Investoren.',
|
||||
audience: ['Management', 'DSB', 'Compliance Officer', 'Auditor', 'Investoren'],
|
||||
gdprArticles: ['EU-AI-Act Art. 52', 'EU-AI-Act Art. 69', 'EU-AI-Act Anhang III'],
|
||||
},
|
||||
{
|
||||
id: 'obligations',
|
||||
name: 'Pflichten-Uebersicht',
|
||||
href: '/compliance/obligations',
|
||||
description: 'Regulatorische Pflichten (NIS2, DSGVO, AI Act)',
|
||||
purpose: 'Aggregierte Uebersicht aller regulatorischen Pflichten aus NIS2, DSGVO, AI Act und weiteren Vorschriften. Basierend auf Unternehmensdaten (Groesse, Branche) werden automatisch anwendbare Pflichten, Fristen und Sanktionen ermittelt. C-Level-Export als PDF-Memo.',
|
||||
audience: ['Geschaeftsfuehrung', 'DSB', 'CISO', 'Compliance Officer'],
|
||||
gdprArticles: ['NIS2 Art. 21', 'BSIG-E § 30-33', 'Art. 5 DSGVO', 'EU-AI-Act'],
|
||||
},
|
||||
],
|
||||
},
|
||||
// =========================================================================
|
||||
// Compliance SDK - Datenschutz-Werkzeuge & Kataloge
|
||||
// Compliance SDK - Alle Datenschutz-, Compliance- und SDK-Module
|
||||
// =========================================================================
|
||||
{
|
||||
id: 'compliance-sdk',
|
||||
@@ -268,7 +39,7 @@ export const navigation: NavCategory[] = [
|
||||
icon: 'shield',
|
||||
color: '#8b5cf6', // Violet-500
|
||||
colorClass: 'compliance-sdk',
|
||||
description: 'SDK-Kataloge, Risiken & Massnahmen',
|
||||
description: 'DSGVO, Audit, GRC & SDK-Werkzeuge',
|
||||
modules: [
|
||||
{
|
||||
id: 'catalog-manager',
|
||||
@@ -292,7 +63,7 @@ export const navigation: NavCategory[] = [
|
||||
description: 'LLM, OCR, RAG & Machine Learning',
|
||||
modules: [
|
||||
// -----------------------------------------------------------------------
|
||||
// KI-Daten-Pipeline: Magic Help ⟷ OCR → Indexierung → Suche
|
||||
// KI-Daten-Pipeline: Magic Help -> OCR -> Indexierung -> Suche
|
||||
// -----------------------------------------------------------------------
|
||||
{
|
||||
id: 'magic-help',
|
||||
@@ -391,69 +162,6 @@ export const navigation: NavCategory[] = [
|
||||
],
|
||||
},
|
||||
// =========================================================================
|
||||
// Infrastruktur & DevOps
|
||||
// =========================================================================
|
||||
{
|
||||
id: 'infrastructure',
|
||||
name: 'Infrastruktur & DevOps',
|
||||
icon: 'server',
|
||||
color: '#f97316', // Orange
|
||||
colorClass: 'infrastructure',
|
||||
description: 'GPU, Security, CI/CD & Monitoring',
|
||||
modules: [
|
||||
// DevOps Pipeline Group (CI/CD -> Tests -> SBOM -> Security)
|
||||
{
|
||||
id: 'ci-cd',
|
||||
name: 'CI/CD',
|
||||
href: '/infrastructure/ci-cd',
|
||||
description: 'Pipelines, Deployments & Container',
|
||||
purpose: 'CI/CD Dashboard mit Gitea Actions Pipelines, Deployment-Status und Container-Management.',
|
||||
audience: ['DevOps', 'Entwickler'],
|
||||
subgroup: 'DevOps Pipeline',
|
||||
},
|
||||
{
|
||||
id: 'tests',
|
||||
name: 'Test Dashboard',
|
||||
href: '/infrastructure/tests',
|
||||
description: 'Test-Suites, Coverage & CI/CD',
|
||||
purpose: 'Zentrales Dashboard fuer alle 280+ Tests. Unit (Go, Python), Integration, E2E (Playwright) und BQAS Quality Tests. Aggregiert Tests aus allen Services ohne physische Migration.',
|
||||
audience: ['Entwickler', 'QA', 'DevOps'],
|
||||
subgroup: 'DevOps Pipeline',
|
||||
},
|
||||
{
|
||||
id: 'sbom',
|
||||
name: 'SBOM',
|
||||
href: '/infrastructure/sbom',
|
||||
description: 'Software Bill of Materials',
|
||||
purpose: 'Verwalten Sie alle Software-Abhaengigkeiten und deren Lizenzen.',
|
||||
audience: ['DevOps', 'Compliance'],
|
||||
oldAdminPath: '/admin/sbom',
|
||||
subgroup: 'DevOps Pipeline',
|
||||
},
|
||||
{
|
||||
id: 'security',
|
||||
name: 'Security',
|
||||
href: '/infrastructure/security',
|
||||
description: 'DevSecOps Dashboard & Scans',
|
||||
purpose: 'Security-Scans, Vulnerability-Reports und OWASP-Compliance.',
|
||||
audience: ['DevOps', 'Security'],
|
||||
oldAdminPath: '/admin/security',
|
||||
subgroup: 'DevOps Pipeline',
|
||||
},
|
||||
// Infrastructure Group
|
||||
{
|
||||
id: 'middleware',
|
||||
name: 'Middleware',
|
||||
href: '/infrastructure/middleware',
|
||||
description: 'Middleware Stack & API Gateway',
|
||||
purpose: 'Ueberwachen und testen Sie den Middleware-Stack und API Gateway.',
|
||||
audience: ['DevOps'],
|
||||
oldAdminPath: '/admin/middleware',
|
||||
subgroup: 'Infrastructure',
|
||||
},
|
||||
],
|
||||
},
|
||||
// =========================================================================
|
||||
// Bildung & Schule
|
||||
// =========================================================================
|
||||
{
|
||||
@@ -482,14 +190,6 @@ export const navigation: NavCategory[] = [
|
||||
audience: ['Entwickler'],
|
||||
oldAdminPath: '/admin/zeugnisse-crawler',
|
||||
},
|
||||
{
|
||||
id: 'foerderantrag',
|
||||
name: 'Foerderantrag-Wizard',
|
||||
href: '/education/foerderantrag',
|
||||
description: 'DigitalPakt & Landesfoerderung',
|
||||
purpose: '8-Schritt-Wizard fuer Schulfoerderantraege. Erstellt antragsfaehige Dokumente (Antragsschreiben, Kostenplan, Datenschutzkonzept) mit KI-Unterstuetzung. BreakPilot-Presets fuer schnellen Start.',
|
||||
audience: ['Schulleitung', 'IT-Beauftragte', 'Schultraeger'],
|
||||
},
|
||||
{
|
||||
id: 'abitur-archiv',
|
||||
name: 'Abitur-Archiv',
|
||||
@@ -507,139 +207,36 @@ export const navigation: NavCategory[] = [
|
||||
audience: ['Lehrer', 'Entwickler'],
|
||||
oldAdminPath: '/admin/klausur-korrektur',
|
||||
},
|
||||
{
|
||||
id: 'companion',
|
||||
name: 'Companion',
|
||||
href: '/education/companion',
|
||||
description: 'Unterrichts-Timer & Phasen',
|
||||
purpose: 'Strukturierter Unterricht mit 5-Phasen-Modell (E-A-S-T-R). Visual Timer, Hausaufgaben-Tracking und Reflexion.',
|
||||
audience: ['Lehrer'],
|
||||
oldAdminPath: '/admin/companion',
|
||||
},
|
||||
],
|
||||
},
|
||||
// =========================================================================
|
||||
// Kommunikation & Alerts
|
||||
// Website
|
||||
// =========================================================================
|
||||
{
|
||||
id: 'communication',
|
||||
name: 'Kommunikation & Alerts',
|
||||
icon: 'mail',
|
||||
color: '#22c55e', // Green
|
||||
colorClass: 'communication',
|
||||
description: 'Matrix, E-Mail & Benachrichtigungen',
|
||||
id: 'website',
|
||||
name: 'Website',
|
||||
icon: 'globe',
|
||||
color: '#0ea5e9', // Sky-500
|
||||
colorClass: 'website',
|
||||
description: 'Website Content & Management',
|
||||
modules: [
|
||||
{
|
||||
id: 'video-chat',
|
||||
name: 'Video & Chat',
|
||||
href: '/communication/video-chat',
|
||||
description: 'Matrix & Jitsi Monitoring',
|
||||
purpose: 'Dashboard fuer Matrix Synapse (E2EE Messaging) und Jitsi Meet (Videokonferenzen). Ueberwachen Sie Service-Status, aktive Meetings, Traffic und SysEleven Ressourcenplanung.',
|
||||
audience: ['Admins', 'DevOps', 'Support'],
|
||||
oldAdminPath: '/admin/communication',
|
||||
},
|
||||
{
|
||||
id: 'matrix',
|
||||
name: 'Voice Service',
|
||||
href: '/communication/matrix',
|
||||
description: 'Voice-First Interface & Architektur',
|
||||
purpose: 'Konfigurieren und testen Sie den Voice-Service (PersonaPlex-7B, TaskOrchestrator). Dokumentation der Voice-First Architektur mit DSGVO-Compliance.',
|
||||
audience: ['Entwickler', 'Admins'],
|
||||
oldAdminPath: '/admin/voice',
|
||||
},
|
||||
{
|
||||
id: 'mail',
|
||||
name: 'Unified Inbox',
|
||||
href: '/communication/mail',
|
||||
description: 'E-Mail-Konten & KI-Analyse',
|
||||
purpose: 'Verwalten Sie E-Mail-Konten und nutzen Sie KI zur Kategorisierung.',
|
||||
audience: ['Support', 'Admins'],
|
||||
oldAdminPath: '/admin/mail',
|
||||
},
|
||||
{
|
||||
id: 'alerts',
|
||||
name: 'Alerts Monitoring',
|
||||
href: '/communication/alerts',
|
||||
description: 'Google Alerts & Feed-Ueberwachung',
|
||||
purpose: 'Ueberwachen Sie Google Alerts und RSS-Feeds fuer relevante Neuigkeiten.',
|
||||
audience: ['Marketing', 'Admins'],
|
||||
oldAdminPath: '/admin/alerts',
|
||||
},
|
||||
],
|
||||
},
|
||||
// =========================================================================
|
||||
// Entwicklung & Produkte
|
||||
// =========================================================================
|
||||
{
|
||||
id: 'development',
|
||||
name: 'Entwicklung & Produkte',
|
||||
icon: 'code',
|
||||
color: '#64748b', // Slate
|
||||
colorClass: 'development',
|
||||
description: 'Workflow, Game, Docs & Brandbook',
|
||||
modules: [
|
||||
{
|
||||
id: 'workflow',
|
||||
name: 'Dev Workflow',
|
||||
href: '/development/workflow',
|
||||
description: 'Git, CI/CD & Team-Regeln',
|
||||
purpose: 'Entwicklungs-Workflow mit Git, CI/CD Pipeline und Team-Konventionen. Pflichtlektuere fuer alle Entwickler.',
|
||||
audience: ['Entwickler', 'DevOps'],
|
||||
},
|
||||
{
|
||||
id: 'game',
|
||||
name: 'Breakpilot Drive',
|
||||
href: '/development/game',
|
||||
description: 'Lernspiel Management',
|
||||
purpose: 'Verwalten Sie Spielinhalte, Level und Lernziele fuer Breakpilot Drive.',
|
||||
audience: ['Content Manager', 'Entwickler'],
|
||||
oldAdminPath: '/admin/game',
|
||||
},
|
||||
{
|
||||
id: 'unity-bridge',
|
||||
name: 'Unity Bridge',
|
||||
href: '/development/unity-bridge',
|
||||
description: 'Unity Editor Steuerung',
|
||||
purpose: 'Steuern Sie den Unity Editor remote fuer Game-Development.',
|
||||
audience: ['Entwickler'],
|
||||
oldAdminPath: '/admin/unity-bridge',
|
||||
},
|
||||
{
|
||||
id: 'docs',
|
||||
name: 'Developer Docs',
|
||||
href: '/development/docs',
|
||||
description: 'API & Architektur',
|
||||
purpose: 'Durchsuchen Sie die API-Dokumentation und Architektur-Diagramme.',
|
||||
audience: ['Entwickler'],
|
||||
oldAdminPath: '/admin/docs',
|
||||
},
|
||||
{
|
||||
id: 'brandbook',
|
||||
name: 'Brandbook',
|
||||
href: '/development/brandbook',
|
||||
description: 'Corporate Design',
|
||||
purpose: 'Referenz fuer Logos, Farben, Typografie und Design-Richtlinien.',
|
||||
audience: ['Designer', 'Marketing'],
|
||||
oldAdminPath: '/admin/brandbook',
|
||||
},
|
||||
{
|
||||
id: 'screen-flow',
|
||||
name: 'Screen Flow',
|
||||
href: '/development/screen-flow',
|
||||
description: 'UI Screen-Verbindungen',
|
||||
purpose: 'Visualisieren Sie die Navigation und Screen-Verbindungen der App.',
|
||||
audience: ['Designer', 'Entwickler'],
|
||||
oldAdminPath: '/admin/screen-flow',
|
||||
},
|
||||
{
|
||||
id: 'content',
|
||||
id: 'uebersetzungen',
|
||||
name: 'Uebersetzungen',
|
||||
href: '/development/content',
|
||||
href: '/website/uebersetzungen',
|
||||
description: 'Website Content & Sprachen',
|
||||
purpose: 'Verwalten Sie Website-Inhalte und Uebersetzungen.',
|
||||
audience: ['Content Manager'],
|
||||
oldAdminPath: '/admin/content',
|
||||
},
|
||||
{
|
||||
id: 'manager',
|
||||
name: 'Website Manager',
|
||||
href: '/website/manager',
|
||||
description: 'CMS Dashboard',
|
||||
purpose: 'Visuelles CMS-Dashboard fuer die BreakPilot Website. Alle Sektionen bearbeiten mit Live-Preview.',
|
||||
audience: ['Content Manager', 'Entwickler'],
|
||||
},
|
||||
],
|
||||
},
|
||||
// =========================================================================
|
||||
|
||||
@@ -23,7 +23,7 @@ export const roles: Role[] = [
|
||||
name: 'Entwickler',
|
||||
description: 'Voller Zugriff auf alle Bereiche',
|
||||
icon: 'code',
|
||||
visibleCategories: ['dsgvo', 'compliance', 'compliance-sdk', 'ai', 'infrastructure', 'education', 'communication', 'development'],
|
||||
visibleCategories: ['compliance-sdk', 'ai', 'education', 'website'],
|
||||
color: 'bg-primary-100 border-primary-300 text-primary-700',
|
||||
},
|
||||
{
|
||||
@@ -31,7 +31,7 @@ export const roles: Role[] = [
|
||||
name: 'Manager',
|
||||
description: 'Executive Uebersicht',
|
||||
icon: 'chart',
|
||||
visibleCategories: ['dsgvo', 'compliance', 'compliance-sdk', 'communication'],
|
||||
visibleCategories: ['compliance-sdk', 'website'],
|
||||
color: 'bg-blue-100 border-blue-300 text-blue-700',
|
||||
},
|
||||
{
|
||||
@@ -39,7 +39,7 @@ export const roles: Role[] = [
|
||||
name: 'Auditor',
|
||||
description: 'Compliance Pruefung',
|
||||
icon: 'clipboard',
|
||||
visibleCategories: ['dsgvo', 'compliance', 'compliance-sdk'],
|
||||
visibleCategories: ['compliance-sdk'],
|
||||
color: 'bg-amber-100 border-amber-300 text-amber-700',
|
||||
},
|
||||
{
|
||||
@@ -47,7 +47,7 @@ export const roles: Role[] = [
|
||||
name: 'DSB',
|
||||
description: 'Datenschutzbeauftragter',
|
||||
icon: 'shield',
|
||||
visibleCategories: ['dsgvo', 'compliance', 'compliance-sdk'],
|
||||
visibleCategories: ['compliance-sdk'],
|
||||
color: 'bg-purple-100 border-purple-300 text-purple-700',
|
||||
},
|
||||
]
|
||||
|
||||
663
admin-v2/lib/sdk/academy/api.ts
Normal file
663
admin-v2/lib/sdk/academy/api.ts
Normal file
@@ -0,0 +1,663 @@
|
||||
/**
|
||||
* Academy API Client
|
||||
*
|
||||
* API client for the Compliance E-Learning Academy module
|
||||
* Connects to the ai-compliance-sdk backend via Next.js proxy
|
||||
*/
|
||||
|
||||
import {
|
||||
Course,
|
||||
CourseCategory,
|
||||
CourseCreateRequest,
|
||||
CourseUpdateRequest,
|
||||
Enrollment,
|
||||
EnrollmentStatus,
|
||||
EnrollmentListResponse,
|
||||
EnrollUserRequest,
|
||||
UpdateProgressRequest,
|
||||
Certificate,
|
||||
AcademyStatistics,
|
||||
SubmitQuizRequest,
|
||||
SubmitQuizResponse,
|
||||
GenerateCourseRequest,
|
||||
GenerateCourseResponse,
|
||||
VideoStatus,
|
||||
isEnrollmentOverdue
|
||||
} from './types'
|
||||
|
||||
// =============================================================================
|
||||
// CONFIGURATION
|
||||
// =============================================================================
|
||||
|
||||
const ACADEMY_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)
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// COURSE CRUD
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Alle Kurse abrufen
|
||||
*/
|
||||
export async function fetchCourses(): Promise<Course[]> {
|
||||
return fetchWithTimeout<Course[]>(
|
||||
`${ACADEMY_API_BASE}/api/v1/academy/courses`
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Einzelnen Kurs abrufen
|
||||
*/
|
||||
export async function fetchCourse(id: string): Promise<Course> {
|
||||
return fetchWithTimeout<Course>(
|
||||
`${ACADEMY_API_BASE}/api/v1/academy/courses/${id}`
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Neuen Kurs erstellen
|
||||
*/
|
||||
export async function createCourse(request: CourseCreateRequest): Promise<Course> {
|
||||
return fetchWithTimeout<Course>(
|
||||
`${ACADEMY_API_BASE}/api/v1/academy/courses`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify(request)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Kurs aktualisieren
|
||||
*/
|
||||
export async function updateCourse(id: string, update: CourseUpdateRequest): Promise<Course> {
|
||||
return fetchWithTimeout<Course>(
|
||||
`${ACADEMY_API_BASE}/api/v1/academy/courses/${id}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(update)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Kurs loeschen
|
||||
*/
|
||||
export async function deleteCourse(id: string): Promise<void> {
|
||||
await fetchWithTimeout<void>(
|
||||
`${ACADEMY_API_BASE}/api/v1/academy/courses/${id}`,
|
||||
{
|
||||
method: 'DELETE'
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ENROLLMENTS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Einschreibungen abrufen (optional gefiltert nach Kurs-ID)
|
||||
*/
|
||||
export async function fetchEnrollments(courseId?: string): Promise<Enrollment[]> {
|
||||
const params = new URLSearchParams()
|
||||
if (courseId) {
|
||||
params.set('courseId', courseId)
|
||||
}
|
||||
const queryString = params.toString()
|
||||
const url = `${ACADEMY_API_BASE}/api/v1/academy/enrollments${queryString ? `?${queryString}` : ''}`
|
||||
|
||||
return fetchWithTimeout<Enrollment[]>(url)
|
||||
}
|
||||
|
||||
/**
|
||||
* Benutzer in einen Kurs einschreiben
|
||||
*/
|
||||
export async function enrollUser(request: EnrollUserRequest): Promise<Enrollment> {
|
||||
return fetchWithTimeout<Enrollment>(
|
||||
`${ACADEMY_API_BASE}/api/v1/academy/enrollments`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify(request)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fortschritt einer Einschreibung aktualisieren
|
||||
*/
|
||||
export async function updateProgress(enrollmentId: string, update: UpdateProgressRequest): Promise<Enrollment> {
|
||||
return fetchWithTimeout<Enrollment>(
|
||||
`${ACADEMY_API_BASE}/api/v1/academy/enrollments/${enrollmentId}/progress`,
|
||||
{
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(update)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Einschreibung als abgeschlossen markieren
|
||||
*/
|
||||
export async function completeEnrollment(enrollmentId: string): Promise<Enrollment> {
|
||||
return fetchWithTimeout<Enrollment>(
|
||||
`${ACADEMY_API_BASE}/api/v1/academy/enrollments/${enrollmentId}/complete`,
|
||||
{
|
||||
method: 'POST'
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CERTIFICATES
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Zertifikat abrufen
|
||||
*/
|
||||
export async function fetchCertificate(id: string): Promise<Certificate> {
|
||||
return fetchWithTimeout<Certificate>(
|
||||
`${ACADEMY_API_BASE}/api/v1/academy/certificates/${id}`
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Zertifikat generieren nach erfolgreichem Kursabschluss
|
||||
*/
|
||||
export async function generateCertificate(enrollmentId: string): Promise<Certificate> {
|
||||
return fetchWithTimeout<Certificate>(
|
||||
`${ACADEMY_API_BASE}/api/v1/academy/enrollments/${enrollmentId}/certificate`,
|
||||
{
|
||||
method: 'POST'
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// QUIZ
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Quiz-Antworten einreichen und auswerten
|
||||
*/
|
||||
export async function submitQuiz(lessonId: string, answers: SubmitQuizRequest): Promise<SubmitQuizResponse> {
|
||||
return fetchWithTimeout<SubmitQuizResponse>(
|
||||
`${ACADEMY_API_BASE}/api/v1/academy/lessons/${lessonId}/quiz`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify(answers)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// STATISTICS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Academy-Statistiken abrufen
|
||||
*/
|
||||
export async function fetchAcademyStatistics(): Promise<AcademyStatistics> {
|
||||
return fetchWithTimeout<AcademyStatistics>(
|
||||
`${ACADEMY_API_BASE}/api/v1/academy/statistics`
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// AI GENERATION
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* KI-generiert einen kompletten Kurs
|
||||
*/
|
||||
export async function generateCourse(request: GenerateCourseRequest): Promise<GenerateCourseResponse> {
|
||||
return fetchWithTimeout<GenerateCourseResponse>(
|
||||
`${ACADEMY_API_BASE}/api/v1/academy/courses/generate`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify(request)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Einzelne Lektion neu generieren
|
||||
*/
|
||||
export async function regenerateLesson(lessonId: string): Promise<{ lessonId: string; status: string }> {
|
||||
return fetchWithTimeout<{ lessonId: string; status: string }>(
|
||||
`${ACADEMY_API_BASE}/api/v1/academy/lessons/${lessonId}/regenerate`,
|
||||
{
|
||||
method: 'POST'
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// VIDEO GENERATION
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Videos fuer alle Lektionen eines Kurses generieren
|
||||
*/
|
||||
export async function generateVideos(courseId: string): Promise<VideoStatus> {
|
||||
return fetchWithTimeout<VideoStatus>(
|
||||
`${ACADEMY_API_BASE}/api/v1/academy/courses/${courseId}/generate-videos`,
|
||||
{
|
||||
method: 'POST'
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Video-Generierungs-Status abrufen
|
||||
*/
|
||||
export async function getVideoStatus(courseId: string): Promise<VideoStatus> {
|
||||
return fetchWithTimeout<VideoStatus>(
|
||||
`${ACADEMY_API_BASE}/api/v1/academy/courses/${courseId}/video-status`
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CERTIFICATES (Extended)
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Zertifikat als PDF herunterladen
|
||||
*/
|
||||
export async function downloadCertificatePDF(certificateId: string): Promise<Blob> {
|
||||
const controller = new AbortController()
|
||||
const timeoutId = setTimeout(() => controller.abort(), API_TIMEOUT)
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${ACADEMY_API_BASE}/api/v1/academy/certificates/${certificateId}/pdf`,
|
||||
{
|
||||
signal: controller.signal,
|
||||
headers: getAuthHeaders()
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`)
|
||||
}
|
||||
|
||||
return response.blob()
|
||||
} finally {
|
||||
clearTimeout(timeoutId)
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SDK PROXY FUNCTION (wraps fetchCourses + fetchAcademyStatistics)
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Kurse und Statistiken laden - mit Fallback auf Mock-Daten
|
||||
*/
|
||||
export async function fetchSDKAcademyList(): Promise<{
|
||||
courses: Course[]
|
||||
enrollments: Enrollment[]
|
||||
statistics: AcademyStatistics
|
||||
}> {
|
||||
try {
|
||||
const [courses, enrollments, statistics] = await Promise.all([
|
||||
fetchCourses(),
|
||||
fetchEnrollments(),
|
||||
fetchAcademyStatistics()
|
||||
])
|
||||
|
||||
return { courses, enrollments, statistics }
|
||||
} catch (error) {
|
||||
console.error('Failed to load Academy data from backend, using mock data:', error)
|
||||
|
||||
// Fallback to mock data
|
||||
const courses = createMockCourses()
|
||||
const enrollments = createMockEnrollments()
|
||||
const statistics = createMockStatistics(courses, enrollments)
|
||||
|
||||
return { courses, enrollments, statistics }
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MOCK DATA (Fallback / Demo)
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Demo-Kurse mit deutschen Titeln erstellen
|
||||
*/
|
||||
export function createMockCourses(): Course[] {
|
||||
const now = new Date()
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'course-001',
|
||||
title: 'DSGVO-Grundlagen fuer Mitarbeiter',
|
||||
description: 'Umfassende Einfuehrung in die Datenschutz-Grundverordnung. Dieses Pflichttraining vermittelt die wichtigsten Grundsaetze des Datenschutzes, Betroffenenrechte und die korrekte Handhabung personenbezogener Daten im Arbeitsalltag.',
|
||||
category: 'dsgvo_basics',
|
||||
durationMinutes: 90,
|
||||
requiredForRoles: ['all'],
|
||||
createdAt: new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
updatedAt: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
lessons: [
|
||||
{
|
||||
id: 'lesson-001-01',
|
||||
courseId: 'course-001',
|
||||
order: 1,
|
||||
title: 'Was ist die DSGVO?',
|
||||
type: 'text',
|
||||
contentMarkdown: '# Was ist die DSGVO?\n\nDie Datenschutz-Grundverordnung (DSGVO) ist eine Verordnung der Europaeischen Union...',
|
||||
durationMinutes: 15
|
||||
},
|
||||
{
|
||||
id: 'lesson-001-02',
|
||||
courseId: 'course-001',
|
||||
order: 2,
|
||||
title: 'Die 7 Grundsaetze der DSGVO',
|
||||
type: 'video',
|
||||
contentMarkdown: 'Videoerklaerung der Grundsaetze: Rechtmaessigkeit, Zweckbindung, Datenminimierung, Richtigkeit, Speicherbegrenzung, Integritaet und Vertraulichkeit, Rechenschaftspflicht.',
|
||||
durationMinutes: 20,
|
||||
videoUrl: '/videos/dsgvo-grundsaetze.mp4'
|
||||
},
|
||||
{
|
||||
id: 'lesson-001-03',
|
||||
courseId: 'course-001',
|
||||
order: 3,
|
||||
title: 'Betroffenenrechte (Art. 15-21)',
|
||||
type: 'text',
|
||||
contentMarkdown: '# Betroffenenrechte\n\nUebersicht der Betroffenenrechte: Auskunft, Berichtigung, Loeschung, Einschraenkung, Datenuebertragbarkeit, Widerspruch.',
|
||||
durationMinutes: 20
|
||||
},
|
||||
{
|
||||
id: 'lesson-001-04',
|
||||
courseId: 'course-001',
|
||||
order: 4,
|
||||
title: 'Personenbezogene Daten im Arbeitsalltag',
|
||||
type: 'video',
|
||||
contentMarkdown: 'Praxisbeispiele fuer den korrekten Umgang mit personenbezogenen Daten am Arbeitsplatz.',
|
||||
durationMinutes: 15,
|
||||
videoUrl: '/videos/dsgvo-praxis.mp4'
|
||||
},
|
||||
{
|
||||
id: 'lesson-001-05',
|
||||
courseId: 'course-001',
|
||||
order: 5,
|
||||
title: 'Wissenstest: DSGVO-Grundlagen',
|
||||
type: 'quiz',
|
||||
contentMarkdown: 'Testen Sie Ihr Wissen zu den DSGVO-Grundlagen.',
|
||||
durationMinutes: 20
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'course-002',
|
||||
title: 'IT-Sicherheit & Cybersecurity Awareness',
|
||||
description: 'Sensibilisierung fuer IT-Sicherheitsrisiken und Best Practices im Umgang mit Phishing, Passwoertern, Social Engineering und sicherer Kommunikation.',
|
||||
category: 'it_security',
|
||||
durationMinutes: 60,
|
||||
requiredForRoles: ['all'],
|
||||
createdAt: new Date(now.getTime() - 60 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
updatedAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
lessons: [
|
||||
{
|
||||
id: 'lesson-002-01',
|
||||
courseId: 'course-002',
|
||||
order: 1,
|
||||
title: 'Phishing erkennen und vermeiden',
|
||||
type: 'video',
|
||||
contentMarkdown: 'Wie erkennt man Phishing-E-Mails und was tut man im Verdachtsfall?',
|
||||
durationMinutes: 15,
|
||||
videoUrl: '/videos/phishing-awareness.mp4'
|
||||
},
|
||||
{
|
||||
id: 'lesson-002-02',
|
||||
courseId: 'course-002',
|
||||
order: 2,
|
||||
title: 'Sichere Passwoerter und MFA',
|
||||
type: 'text',
|
||||
contentMarkdown: '# Sichere Passwoerter\n\nRichtlinien fuer starke Passwoerter, Passwort-Manager und Multi-Faktor-Authentifizierung.',
|
||||
durationMinutes: 15
|
||||
},
|
||||
{
|
||||
id: 'lesson-002-03',
|
||||
courseId: 'course-002',
|
||||
order: 3,
|
||||
title: 'Social Engineering und Manipulation',
|
||||
type: 'text',
|
||||
contentMarkdown: '# Social Engineering\n\nMethoden von Angreifern zur Manipulation von Mitarbeitern und Schutzmassnahmen.',
|
||||
durationMinutes: 15
|
||||
},
|
||||
{
|
||||
id: 'lesson-002-04',
|
||||
courseId: 'course-002',
|
||||
order: 4,
|
||||
title: 'Wissenstest: IT-Sicherheit',
|
||||
type: 'quiz',
|
||||
contentMarkdown: 'Testen Sie Ihr Wissen zur IT-Sicherheit.',
|
||||
durationMinutes: 15
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'course-003',
|
||||
title: 'AI Literacy - Sicherer Umgang mit KI',
|
||||
description: 'Grundlagen kuenstlicher Intelligenz, EU AI Act, verantwortungsvoller Einsatz von KI-Werkzeugen und Risiken bei der Nutzung von Large Language Models (LLMs) im Unternehmen.',
|
||||
category: 'ai_literacy',
|
||||
durationMinutes: 75,
|
||||
requiredForRoles: ['admin', 'data_protection_officer'],
|
||||
createdAt: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
updatedAt: new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
lessons: [
|
||||
{
|
||||
id: 'lesson-003-01',
|
||||
courseId: 'course-003',
|
||||
order: 1,
|
||||
title: 'Was ist Kuenstliche Intelligenz?',
|
||||
type: 'text',
|
||||
contentMarkdown: '# Was ist KI?\n\nGrundlagen von Machine Learning, Deep Learning und Large Language Models in verstaendlicher Sprache.',
|
||||
durationMinutes: 15
|
||||
},
|
||||
{
|
||||
id: 'lesson-003-02',
|
||||
courseId: 'course-003',
|
||||
order: 2,
|
||||
title: 'Der EU AI Act - Was bedeutet er fuer uns?',
|
||||
type: 'video',
|
||||
contentMarkdown: 'Ueberblick ueber den EU AI Act, Risikoklassen und Anforderungen fuer Unternehmen.',
|
||||
durationMinutes: 20,
|
||||
videoUrl: '/videos/eu-ai-act.mp4'
|
||||
},
|
||||
{
|
||||
id: 'lesson-003-03',
|
||||
courseId: 'course-003',
|
||||
order: 3,
|
||||
title: 'KI-Werkzeuge sicher nutzen',
|
||||
type: 'text',
|
||||
contentMarkdown: '# KI-Werkzeuge sicher nutzen\n\nRichtlinien fuer den Einsatz von ChatGPT, Copilot & Co.',
|
||||
durationMinutes: 20
|
||||
},
|
||||
{
|
||||
id: 'lesson-003-04',
|
||||
courseId: 'course-003',
|
||||
order: 4,
|
||||
title: 'Wissenstest: AI Literacy',
|
||||
type: 'quiz',
|
||||
contentMarkdown: 'Testen Sie Ihr Wissen zum sicheren Umgang mit KI.',
|
||||
durationMinutes: 20
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Demo-Einschreibungen erstellen
|
||||
*/
|
||||
export function createMockEnrollments(): Enrollment[] {
|
||||
const now = new Date()
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'enr-001',
|
||||
courseId: 'course-001',
|
||||
userId: 'user-001',
|
||||
userName: 'Maria Fischer',
|
||||
userEmail: 'maria.fischer@example.de',
|
||||
status: 'in_progress',
|
||||
progress: 40,
|
||||
startedAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
deadline: new Date(now.getTime() + 20 * 24 * 60 * 60 * 1000).toISOString()
|
||||
},
|
||||
{
|
||||
id: 'enr-002',
|
||||
courseId: 'course-002',
|
||||
userId: 'user-002',
|
||||
userName: 'Stefan Mueller',
|
||||
userEmail: 'stefan.mueller@example.de',
|
||||
status: 'completed',
|
||||
progress: 100,
|
||||
startedAt: new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
completedAt: new Date(now.getTime() - 12 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
certificateId: 'cert-001',
|
||||
deadline: new Date(now.getTime() + 10 * 24 * 60 * 60 * 1000).toISOString()
|
||||
},
|
||||
{
|
||||
id: 'enr-003',
|
||||
courseId: 'course-001',
|
||||
userId: 'user-003',
|
||||
userName: 'Laura Schneider',
|
||||
userEmail: 'laura.schneider@example.de',
|
||||
status: 'not_started',
|
||||
progress: 0,
|
||||
startedAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
deadline: new Date(now.getTime() + 28 * 24 * 60 * 60 * 1000).toISOString()
|
||||
},
|
||||
{
|
||||
id: 'enr-004',
|
||||
courseId: 'course-003',
|
||||
userId: 'user-004',
|
||||
userName: 'Thomas Wagner',
|
||||
userEmail: 'thomas.wagner@example.de',
|
||||
status: 'expired',
|
||||
progress: 25,
|
||||
startedAt: new Date(now.getTime() - 60 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
deadline: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString()
|
||||
},
|
||||
{
|
||||
id: 'enr-005',
|
||||
courseId: 'course-002',
|
||||
userId: 'user-005',
|
||||
userName: 'Julia Becker',
|
||||
userEmail: 'julia.becker@example.de',
|
||||
status: 'in_progress',
|
||||
progress: 50,
|
||||
startedAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
deadline: new Date(now.getTime() + 5 * 24 * 60 * 60 * 1000).toISOString()
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Demo-Statistiken aus Kursen und Einschreibungen berechnen
|
||||
*/
|
||||
export function createMockStatistics(courses?: Course[], enrollments?: Enrollment[]): AcademyStatistics {
|
||||
const c = courses || createMockCourses()
|
||||
const e = enrollments || createMockEnrollments()
|
||||
|
||||
const completedCount = e.filter(en => en.status === 'completed').length
|
||||
const completionRate = e.length > 0 ? Math.round((completedCount / e.length) * 100) : 0
|
||||
const overdueCount = e.filter(en => isEnrollmentOverdue(en)).length
|
||||
|
||||
return {
|
||||
totalCourses: c.length,
|
||||
totalEnrollments: e.length,
|
||||
completionRate,
|
||||
overdueCount,
|
||||
byCategory: {
|
||||
dsgvo_basics: c.filter(co => co.category === 'dsgvo_basics').length,
|
||||
it_security: c.filter(co => co.category === 'it_security').length,
|
||||
ai_literacy: c.filter(co => co.category === 'ai_literacy').length,
|
||||
whistleblower_protection: c.filter(co => co.category === 'whistleblower_protection').length,
|
||||
custom: c.filter(co => co.category === 'custom').length,
|
||||
},
|
||||
byStatus: {
|
||||
not_started: e.filter(en => en.status === 'not_started').length,
|
||||
in_progress: e.filter(en => en.status === 'in_progress').length,
|
||||
completed: e.filter(en => en.status === 'completed').length,
|
||||
expired: e.filter(en => en.status === 'expired').length,
|
||||
}
|
||||
}
|
||||
}
|
||||
6
admin-v2/lib/sdk/academy/index.ts
Normal file
6
admin-v2/lib/sdk/academy/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Academy Module Exports
|
||||
*/
|
||||
|
||||
export * from './types'
|
||||
export * from './api'
|
||||
318
admin-v2/lib/sdk/academy/types.ts
Normal file
318
admin-v2/lib/sdk/academy/types.ts
Normal file
@@ -0,0 +1,318 @@
|
||||
/**
|
||||
* Academy (E-Learning / Compliance Academy) Types
|
||||
*
|
||||
* TypeScript definitions for the E-Learning Academy module
|
||||
* Provides course management, enrollment tracking, and certificate generation
|
||||
* for DSGVO, IT-Security, AI Literacy, and Whistleblower compliance training
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// ENUMS & CONSTANTS
|
||||
// =============================================================================
|
||||
|
||||
export type CourseCategory =
|
||||
| 'dsgvo_basics' // DSGVO-Grundlagen
|
||||
| 'it_security' // IT-Sicherheit
|
||||
| 'ai_literacy' // AI Literacy
|
||||
| 'whistleblower_protection' // Hinweisgeberschutz
|
||||
| 'custom' // Benutzerdefiniert
|
||||
|
||||
export type EnrollmentStatus =
|
||||
| 'not_started' // Nicht gestartet
|
||||
| 'in_progress' // In Bearbeitung
|
||||
| 'completed' // Abgeschlossen
|
||||
| 'expired' // Abgelaufen
|
||||
|
||||
export type LessonType = 'video' | 'text' | 'quiz'
|
||||
|
||||
// =============================================================================
|
||||
// COURSE CATEGORY METADATA
|
||||
// =============================================================================
|
||||
|
||||
export interface CourseCategoryInfo {
|
||||
label: string
|
||||
description: string
|
||||
icon: string
|
||||
color: string
|
||||
bgColor: string
|
||||
}
|
||||
|
||||
export const COURSE_CATEGORY_INFO: Record<CourseCategory, CourseCategoryInfo> = {
|
||||
dsgvo_basics: {
|
||||
label: 'DSGVO-Grundlagen',
|
||||
description: 'Grundlagenwissen zur Datenschutz-Grundverordnung fuer alle Mitarbeiter',
|
||||
icon: 'Shield',
|
||||
color: 'text-blue-700',
|
||||
bgColor: 'bg-blue-100'
|
||||
},
|
||||
it_security: {
|
||||
label: 'IT-Sicherheit',
|
||||
description: 'Cybersecurity Awareness und sichere IT-Nutzung im Arbeitsalltag',
|
||||
icon: 'Lock',
|
||||
color: 'text-red-700',
|
||||
bgColor: 'bg-red-100'
|
||||
},
|
||||
ai_literacy: {
|
||||
label: 'AI Literacy',
|
||||
description: 'Sicherer und verantwortungsvoller Umgang mit kuenstlicher Intelligenz',
|
||||
icon: 'Brain',
|
||||
color: 'text-purple-700',
|
||||
bgColor: 'bg-purple-100'
|
||||
},
|
||||
whistleblower_protection: {
|
||||
label: 'Hinweisgeberschutz',
|
||||
description: 'Hinweisgeberschutzgesetz (HinSchG) und interne Meldestellen',
|
||||
icon: 'Megaphone',
|
||||
color: 'text-orange-700',
|
||||
bgColor: 'bg-orange-100'
|
||||
},
|
||||
custom: {
|
||||
label: 'Benutzerdefiniert',
|
||||
description: 'Individuell erstellte Schulungsinhalte und unternehmensspezifische Kurse',
|
||||
icon: 'Pencil',
|
||||
color: 'text-gray-700',
|
||||
bgColor: 'bg-gray-100'
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ENROLLMENT STATUS METADATA
|
||||
// =============================================================================
|
||||
|
||||
export const ENROLLMENT_STATUS_INFO: Record<EnrollmentStatus, { label: string; color: string; bgColor: string; borderColor: string }> = {
|
||||
not_started: {
|
||||
label: 'Nicht gestartet',
|
||||
color: 'text-gray-700',
|
||||
bgColor: 'bg-gray-100',
|
||||
borderColor: 'border-gray-200'
|
||||
},
|
||||
in_progress: {
|
||||
label: 'In Bearbeitung',
|
||||
color: 'text-yellow-700',
|
||||
bgColor: 'bg-yellow-100',
|
||||
borderColor: 'border-yellow-200'
|
||||
},
|
||||
completed: {
|
||||
label: 'Abgeschlossen',
|
||||
color: 'text-green-700',
|
||||
bgColor: 'bg-green-100',
|
||||
borderColor: 'border-green-200'
|
||||
},
|
||||
expired: {
|
||||
label: 'Abgelaufen',
|
||||
color: 'text-red-700',
|
||||
bgColor: 'bg-red-100',
|
||||
borderColor: 'border-red-200'
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MAIN INTERFACES
|
||||
// =============================================================================
|
||||
|
||||
export interface Course {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
category: CourseCategory
|
||||
lessons: Lesson[]
|
||||
durationMinutes: number
|
||||
requiredForRoles: string[]
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface Lesson {
|
||||
id: string
|
||||
courseId: string
|
||||
title: string
|
||||
type: LessonType
|
||||
contentMarkdown: string
|
||||
videoUrl?: string
|
||||
order: number
|
||||
durationMinutes: number
|
||||
}
|
||||
|
||||
export interface QuizQuestion {
|
||||
id: string
|
||||
lessonId: string
|
||||
question: string
|
||||
options: string[]
|
||||
correctOptionIndex: number
|
||||
explanation: string
|
||||
}
|
||||
|
||||
export interface Enrollment {
|
||||
id: string
|
||||
courseId: string
|
||||
userId: string
|
||||
userName: string
|
||||
userEmail: string
|
||||
status: EnrollmentStatus
|
||||
progress: number // 0-100
|
||||
startedAt: string
|
||||
completedAt?: string
|
||||
certificateId?: string
|
||||
deadline: string
|
||||
}
|
||||
|
||||
export interface Certificate {
|
||||
id: string
|
||||
enrollmentId: string
|
||||
courseId: string
|
||||
userId: string
|
||||
userName: string
|
||||
courseName: string
|
||||
issuedAt: string
|
||||
validUntil: string
|
||||
pdfUrl: string
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// STATISTICS
|
||||
// =============================================================================
|
||||
|
||||
export interface AcademyStatistics {
|
||||
totalCourses: number
|
||||
totalEnrollments: number
|
||||
completionRate: number // 0-100
|
||||
overdueCount: number
|
||||
byCategory: Record<CourseCategory, number>
|
||||
byStatus: Record<EnrollmentStatus, number>
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// API TYPES (REQUEST / RESPONSE)
|
||||
// =============================================================================
|
||||
|
||||
export interface CourseListResponse {
|
||||
courses: Course[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
export interface EnrollmentListResponse {
|
||||
enrollments: Enrollment[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
export interface CourseCreateRequest {
|
||||
title: string
|
||||
description: string
|
||||
category: CourseCategory
|
||||
durationMinutes: number
|
||||
requiredForRoles?: string[]
|
||||
}
|
||||
|
||||
export interface CourseUpdateRequest {
|
||||
title?: string
|
||||
description?: string
|
||||
category?: CourseCategory
|
||||
durationMinutes?: number
|
||||
requiredForRoles?: string[]
|
||||
}
|
||||
|
||||
export interface EnrollUserRequest {
|
||||
courseId: string
|
||||
userId: string
|
||||
userName: string
|
||||
userEmail: string
|
||||
deadline: string
|
||||
}
|
||||
|
||||
export interface UpdateProgressRequest {
|
||||
progress: number
|
||||
lessonId?: string
|
||||
}
|
||||
|
||||
export interface SubmitQuizRequest {
|
||||
answers: number[] // Index der ausgewaehlten Antwort pro Frage
|
||||
}
|
||||
|
||||
export interface SubmitQuizResponse {
|
||||
score: number
|
||||
passed: boolean
|
||||
correctAnswers: number
|
||||
totalQuestions: number
|
||||
results: { questionId: string; correct: boolean; explanation: string }[]
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// AI GENERATION TYPES
|
||||
// =============================================================================
|
||||
|
||||
export interface GenerateCourseRequest {
|
||||
tenantId: string
|
||||
topic: string
|
||||
category: CourseCategory
|
||||
targetGroup?: string
|
||||
language?: string
|
||||
useRag?: boolean
|
||||
ragQuery?: string
|
||||
}
|
||||
|
||||
export interface GenerateCourseResponse {
|
||||
course: Course
|
||||
ragSources?: { id: string; content: string; source: string; score: number }[]
|
||||
model: string
|
||||
}
|
||||
|
||||
export interface VideoStatus {
|
||||
courseId: string
|
||||
status: 'not_started' | 'pending' | 'processing' | 'completed' | 'failed'
|
||||
lessons: LessonVideoStatus[]
|
||||
}
|
||||
|
||||
export interface LessonVideoStatus {
|
||||
lessonId: string
|
||||
status: string
|
||||
videoUrl?: string
|
||||
audioUrl?: string
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Berechnet die Abschlussrate fuer eine Liste von Einschreibungen in Prozent (0-100)
|
||||
*/
|
||||
export function getCompletionPercentage(enrollments: Enrollment[]): number {
|
||||
if (enrollments.length === 0) return 0
|
||||
const completed = enrollments.filter(e => e.status === 'completed').length
|
||||
return Math.round((completed / enrollments.length) * 100)
|
||||
}
|
||||
|
||||
/**
|
||||
* Prueft ob eine Einschreibung ueberfaellig ist (Deadline ueberschritten und nicht abgeschlossen)
|
||||
*/
|
||||
export function isEnrollmentOverdue(enrollment: Enrollment): boolean {
|
||||
if (enrollment.status === 'completed' || enrollment.status === 'expired') {
|
||||
return false
|
||||
}
|
||||
const deadlineDate = new Date(enrollment.deadline)
|
||||
const now = new Date()
|
||||
return deadlineDate.getTime() < now.getTime()
|
||||
}
|
||||
|
||||
/**
|
||||
* Berechnet die verbleibenden Tage bis zur Deadline
|
||||
* Negative Werte bedeuten ueberfaellig
|
||||
*/
|
||||
export function getDaysUntilDeadline(deadline: string): number {
|
||||
const deadlineDate = new Date(deadline)
|
||||
const now = new Date()
|
||||
const diff = deadlineDate.getTime() - now.getTime()
|
||||
return Math.ceil(diff / (1000 * 60 * 60 * 24))
|
||||
}
|
||||
|
||||
export function getCategoryInfo(category: CourseCategory): CourseCategoryInfo {
|
||||
return COURSE_CATEGORY_INFO[category]
|
||||
}
|
||||
|
||||
export function getStatusInfo(status: EnrollmentStatus) {
|
||||
return ENROLLMENT_STATUS_INFO[status]
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
import { ConstraintEnforcer } from '../constraint-enforcer'
|
||||
import type { ScopeDecision } from '../../compliance-scope-types'
|
||||
|
||||
describe('ConstraintEnforcer', () => {
|
||||
const enforcer = new ConstraintEnforcer()
|
||||
|
||||
// Helper: minimal valid ScopeDecision
|
||||
function makeDecision(overrides: Partial<ScopeDecision> = {}): ScopeDecision {
|
||||
return {
|
||||
id: 'test-decision',
|
||||
determinedLevel: 'L2',
|
||||
scores: { risk_score: 50, complexity_score: 50, assurance_need: 50, composite_score: 50 },
|
||||
triggeredHardTriggers: [],
|
||||
requiredDocuments: [
|
||||
{ documentType: 'vvt', label: 'VVT', required: true, depth: 'Standard', detailItems: [], estimatedEffort: '2h', triggeredBy: [] },
|
||||
{ documentType: 'tom', label: 'TOM', required: true, depth: 'Standard', detailItems: [], estimatedEffort: '3h', triggeredBy: [] },
|
||||
{ documentType: 'lf', label: 'LF', required: true, depth: 'Basis', detailItems: [], estimatedEffort: '1h', triggeredBy: [] },
|
||||
],
|
||||
riskFlags: [],
|
||||
gaps: [],
|
||||
nextActions: [],
|
||||
reasoning: [],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
...overrides,
|
||||
} as ScopeDecision
|
||||
}
|
||||
|
||||
describe('check - no decision', () => {
|
||||
it('should allow basic documents (vvt, tom, dsi) without decision', () => {
|
||||
const result = enforcer.check('vvt', null)
|
||||
expect(result.allowed).toBe(true)
|
||||
expect(result.adjustments.length).toBeGreaterThan(0)
|
||||
expect(result.checkedRules).toContain('RULE-NO-DECISION')
|
||||
})
|
||||
|
||||
it('should allow tom without decision', () => {
|
||||
const result = enforcer.check('tom', null)
|
||||
expect(result.allowed).toBe(true)
|
||||
})
|
||||
|
||||
it('should allow dsi without decision', () => {
|
||||
const result = enforcer.check('dsi', null)
|
||||
expect(result.allowed).toBe(true)
|
||||
})
|
||||
|
||||
it('should block non-basic documents without decision', () => {
|
||||
const result = enforcer.check('dsfa', null)
|
||||
expect(result.allowed).toBe(false)
|
||||
expect(result.violations.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should block av_vertrag without decision', () => {
|
||||
const result = enforcer.check('av_vertrag', null)
|
||||
expect(result.allowed).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('check - RULE-DOC-REQUIRED', () => {
|
||||
it('should allow required documents', () => {
|
||||
const decision = makeDecision()
|
||||
const result = enforcer.check('vvt', decision)
|
||||
expect(result.allowed).toBe(true)
|
||||
})
|
||||
|
||||
it('should warn but allow optional documents', () => {
|
||||
const decision = makeDecision({
|
||||
requiredDocuments: [
|
||||
{ documentType: 'vvt', label: 'VVT', required: true, depth: 'Standard', detailItems: [], estimatedEffort: '2h', triggeredBy: [] },
|
||||
],
|
||||
})
|
||||
const result = enforcer.check('dsfa', decision)
|
||||
expect(result.allowed).toBe(true) // Only warns, does not block
|
||||
expect(result.adjustments.some(a => a.includes('nicht als Pflicht'))).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('check - RULE-DEPTH-MATCH', () => {
|
||||
it('should block when requested depth exceeds determined level', () => {
|
||||
const decision = makeDecision({ determinedLevel: 'L2' })
|
||||
const result = enforcer.check('vvt', decision, 'L4')
|
||||
expect(result.allowed).toBe(false)
|
||||
expect(result.violations.some(v => v.includes('ueberschreitet'))).toBe(true)
|
||||
})
|
||||
|
||||
it('should allow when requested depth matches level', () => {
|
||||
const decision = makeDecision({ determinedLevel: 'L2' })
|
||||
const result = enforcer.check('vvt', decision, 'L2')
|
||||
expect(result.allowed).toBe(true)
|
||||
})
|
||||
|
||||
it('should adjust when requested depth is below level', () => {
|
||||
const decision = makeDecision({ determinedLevel: 'L3' })
|
||||
const result = enforcer.check('vvt', decision, 'L1')
|
||||
expect(result.allowed).toBe(true)
|
||||
expect(result.adjustments.some(a => a.includes('angehoben'))).toBe(true)
|
||||
})
|
||||
|
||||
it('should allow without requested depth level', () => {
|
||||
const decision = makeDecision({ determinedLevel: 'L3' })
|
||||
const result = enforcer.check('vvt', decision)
|
||||
expect(result.allowed).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('check - RULE-DSFA-ENFORCEMENT', () => {
|
||||
it('should note when DSFA is not required but requested', () => {
|
||||
const decision = makeDecision({ determinedLevel: 'L2' })
|
||||
const result = enforcer.check('dsfa', decision)
|
||||
expect(result.allowed).toBe(true)
|
||||
expect(result.adjustments.some(a => a.includes('nicht verpflichtend'))).toBe(true)
|
||||
})
|
||||
|
||||
it('should allow DSFA when hard triggers require it', () => {
|
||||
const decision = makeDecision({
|
||||
determinedLevel: 'L3',
|
||||
triggeredHardTriggers: [{
|
||||
rule: {
|
||||
id: 'HT-ART9',
|
||||
label: 'Art. 9 Daten',
|
||||
description: '',
|
||||
conditionField: '',
|
||||
conditionOperator: 'EQUALS' as const,
|
||||
conditionValue: null,
|
||||
minimumLevel: 'L3',
|
||||
mandatoryDocuments: ['dsfa'],
|
||||
dsfaRequired: true,
|
||||
legalReference: 'Art. 35 DSGVO',
|
||||
},
|
||||
matchedValue: null,
|
||||
explanation: 'Art. 9 Daten verarbeitet',
|
||||
}],
|
||||
})
|
||||
const result = enforcer.check('dsfa', decision)
|
||||
expect(result.allowed).toBe(true)
|
||||
})
|
||||
|
||||
it('should warn about DSFA when drafting non-DSFA but DSFA is required', () => {
|
||||
const decision = makeDecision({
|
||||
determinedLevel: 'L3',
|
||||
triggeredHardTriggers: [{
|
||||
rule: {
|
||||
id: 'HT-ART9',
|
||||
label: 'Art. 9 Daten',
|
||||
description: '',
|
||||
conditionField: '',
|
||||
conditionOperator: 'EQUALS' as const,
|
||||
conditionValue: null,
|
||||
minimumLevel: 'L3',
|
||||
mandatoryDocuments: ['dsfa'],
|
||||
dsfaRequired: true,
|
||||
legalReference: 'Art. 35 DSGVO',
|
||||
},
|
||||
matchedValue: null,
|
||||
explanation: '',
|
||||
}],
|
||||
requiredDocuments: [
|
||||
{ documentType: 'dsfa', label: 'DSFA', required: true, depth: 'Vollstaendig', detailItems: [], estimatedEffort: '8h', triggeredBy: [] },
|
||||
{ documentType: 'vvt', label: 'VVT', required: true, depth: 'Standard', detailItems: [], estimatedEffort: '2h', triggeredBy: [] },
|
||||
],
|
||||
})
|
||||
const result = enforcer.check('vvt', decision)
|
||||
expect(result.allowed).toBe(true)
|
||||
expect(result.adjustments.some(a => a.includes('DSFA') && a.includes('verpflichtend'))).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('check - RULE-RISK-FLAGS', () => {
|
||||
it('should note critical risk flags', () => {
|
||||
const decision = makeDecision({
|
||||
riskFlags: [
|
||||
{ id: 'rf-1', severity: 'CRITICAL', title: 'Offene Art. 9 Verarbeitung', description: '', recommendation: 'DSFA durchfuehren' },
|
||||
{ id: 'rf-2', severity: 'HIGH', title: 'Fehlende Verschluesselung', description: '', recommendation: 'TOM erstellen' },
|
||||
{ id: 'rf-3', severity: 'LOW', title: 'Dokumentation unvollstaendig', description: '', recommendation: '' },
|
||||
],
|
||||
})
|
||||
const result = enforcer.check('vvt', decision)
|
||||
expect(result.allowed).toBe(true)
|
||||
expect(result.adjustments.some(a => a.includes('2 kritische/hohe Risiko-Flags'))).toBe(true)
|
||||
})
|
||||
|
||||
it('should not flag when no risk flags present', () => {
|
||||
const decision = makeDecision({ riskFlags: [] })
|
||||
const result = enforcer.check('vvt', decision)
|
||||
expect(result.adjustments.every(a => !a.includes('Risiko-Flags'))).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('check - checkedRules tracking', () => {
|
||||
it('should track all checked rules', () => {
|
||||
const decision = makeDecision()
|
||||
const result = enforcer.check('vvt', decision)
|
||||
expect(result.checkedRules).toContain('RULE-DOC-REQUIRED')
|
||||
expect(result.checkedRules).toContain('RULE-DEPTH-MATCH')
|
||||
expect(result.checkedRules).toContain('RULE-DSFA-ENFORCEMENT')
|
||||
expect(result.checkedRules).toContain('RULE-RISK-FLAGS')
|
||||
expect(result.checkedRules).toContain('RULE-HARD-TRIGGER-CONSISTENCY')
|
||||
})
|
||||
})
|
||||
|
||||
describe('checkFromContext', () => {
|
||||
it('should reconstruct decision from DraftContext and check', () => {
|
||||
const context = {
|
||||
decisions: {
|
||||
level: 'L2' as const,
|
||||
scores: { risk_score: 50, complexity_score: 50, assurance_need: 50, composite_score: 50 },
|
||||
hardTriggers: [],
|
||||
requiredDocuments: [
|
||||
{ documentType: 'vvt' as const, depth: 'Standard', detailItems: [] },
|
||||
],
|
||||
},
|
||||
companyProfile: { name: 'Test GmbH', industry: 'IT', employeeCount: 50, businessModel: 'SaaS', isPublicSector: false },
|
||||
constraints: {
|
||||
depthRequirements: { required: true, depth: 'Standard', detailItems: [], estimatedEffort: '2h' },
|
||||
riskFlags: [],
|
||||
boundaries: [],
|
||||
},
|
||||
}
|
||||
const result = enforcer.checkFromContext('vvt', context)
|
||||
expect(result.allowed).toBe(true)
|
||||
expect(result.checkedRules.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,153 @@
|
||||
import { IntentClassifier } from '../intent-classifier'
|
||||
|
||||
describe('IntentClassifier', () => {
|
||||
const classifier = new IntentClassifier()
|
||||
|
||||
describe('classify - Draft mode', () => {
|
||||
it.each([
|
||||
['Erstelle ein VVT fuer unseren Hauptprozess', 'draft'],
|
||||
['Generiere eine TOM-Dokumentation', 'draft'],
|
||||
['Schreibe eine Datenschutzerklaerung', 'draft'],
|
||||
['Verfasse einen Entwurf fuer das Loeschkonzept', 'draft'],
|
||||
['Create a DSFA document', 'draft'],
|
||||
['Draft a privacy policy for us', 'draft'],
|
||||
['Neues VVT anlegen', 'draft'],
|
||||
])('"%s" should classify as %s', (input, expectedMode) => {
|
||||
const result = classifier.classify(input)
|
||||
expect(result.mode).toBe(expectedMode)
|
||||
expect(result.confidence).toBeGreaterThan(0.7)
|
||||
})
|
||||
})
|
||||
|
||||
describe('classify - Validate mode', () => {
|
||||
it.each([
|
||||
['Pruefe die Konsistenz meiner Dokumente', 'validate'],
|
||||
['Ist mein VVT korrekt?', 'validate'],
|
||||
['Validiere die TOM gegen das VVT', 'validate'],
|
||||
['Check die Vollstaendigkeit', 'validate'],
|
||||
['Stimmt das mit der DSFA ueberein?', 'validate'],
|
||||
['Cross-Check VVT und TOM', 'validate'],
|
||||
])('"%s" should classify as %s', (input, expectedMode) => {
|
||||
const result = classifier.classify(input)
|
||||
expect(result.mode).toBe(expectedMode)
|
||||
expect(result.confidence).toBeGreaterThan(0.7)
|
||||
})
|
||||
})
|
||||
|
||||
describe('classify - Ask mode', () => {
|
||||
it.each([
|
||||
['Was fehlt noch in meinem Profil?', 'ask'],
|
||||
['Zeige mir die Luecken', 'ask'],
|
||||
['Welche Dokumente fehlen noch?', 'ask'],
|
||||
['Was ist der naechste Schritt?', 'ask'],
|
||||
['Welche Informationen brauche ich noch?', 'ask'],
|
||||
])('"%s" should classify as %s', (input, expectedMode) => {
|
||||
const result = classifier.classify(input)
|
||||
expect(result.mode).toBe(expectedMode)
|
||||
expect(result.confidence).toBeGreaterThan(0.6)
|
||||
})
|
||||
})
|
||||
|
||||
describe('classify - Explain mode (fallback)', () => {
|
||||
it.each([
|
||||
['Was ist DSGVO?', 'explain'],
|
||||
['Erklaere mir Art. 30', 'explain'],
|
||||
['Hallo', 'explain'],
|
||||
['Danke fuer die Hilfe', 'explain'],
|
||||
])('"%s" should classify as %s (fallback)', (input, expectedMode) => {
|
||||
const result = classifier.classify(input)
|
||||
expect(result.mode).toBe(expectedMode)
|
||||
})
|
||||
})
|
||||
|
||||
describe('classify - confidence thresholds', () => {
|
||||
it('should have high confidence for clear draft intents', () => {
|
||||
const result = classifier.classify('Erstelle ein neues VVT')
|
||||
expect(result.confidence).toBeGreaterThanOrEqual(0.85)
|
||||
})
|
||||
|
||||
it('should have lower confidence for ambiguous inputs', () => {
|
||||
const result = classifier.classify('Hallo')
|
||||
expect(result.confidence).toBeLessThan(0.6)
|
||||
})
|
||||
|
||||
it('should boost confidence with document type detection', () => {
|
||||
const withDoc = classifier.classify('Erstelle VVT')
|
||||
const withoutDoc = classifier.classify('Erstelle etwas')
|
||||
expect(withDoc.confidence).toBeGreaterThanOrEqual(withoutDoc.confidence)
|
||||
})
|
||||
|
||||
it('should boost confidence with multiple pattern matches', () => {
|
||||
const single = classifier.classify('Erstelle Dokument')
|
||||
const multi = classifier.classify('Erstelle und generiere ein neues Dokument')
|
||||
expect(multi.confidence).toBeGreaterThanOrEqual(single.confidence)
|
||||
})
|
||||
})
|
||||
|
||||
describe('detectDocumentType', () => {
|
||||
it.each([
|
||||
['VVT erstellen', 'vvt'],
|
||||
['Verarbeitungsverzeichnis', 'vvt'],
|
||||
['Art. 30 Dokumentation', 'vvt'],
|
||||
['TOM definieren', 'tom'],
|
||||
['technisch organisatorische Massnahmen', 'tom'],
|
||||
['Art. 32 Massnahmen', 'tom'],
|
||||
['DSFA durchfuehren', 'dsfa'],
|
||||
['Datenschutz-Folgenabschaetzung', 'dsfa'],
|
||||
['Art. 35 Pruefung', 'dsfa'],
|
||||
['DPIA erstellen', 'dsfa'],
|
||||
['Datenschutzerklaerung', 'dsi'],
|
||||
['Privacy Policy', 'dsi'],
|
||||
['Art. 13 Information', 'dsi'],
|
||||
['Loeschfristen definieren', 'lf'],
|
||||
['Loeschkonzept erstellen', 'lf'],
|
||||
['Retention Policy', 'lf'],
|
||||
['Auftragsverarbeitung', 'av_vertrag'],
|
||||
['AVV erstellen', 'av_vertrag'],
|
||||
['Art. 28 Vertrag', 'av_vertrag'],
|
||||
['Einwilligung einholen', 'einwilligung'],
|
||||
['Consent Management', 'einwilligung'],
|
||||
['Cookie Banner', 'einwilligung'],
|
||||
])('"%s" should detect document type %s', (input, expectedType) => {
|
||||
const result = classifier.detectDocumentType(input)
|
||||
expect(result).toBe(expectedType)
|
||||
})
|
||||
|
||||
it('should return undefined for unrecognized types', () => {
|
||||
expect(classifier.detectDocumentType('Hallo Welt')).toBeUndefined()
|
||||
expect(classifier.detectDocumentType('Was kostet das?')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('classify - Umlaut handling', () => {
|
||||
it('should handle German umlauts correctly', () => {
|
||||
// With actual umlauts (ä, ö, ü)
|
||||
const result1 = classifier.classify('Prüfe die Vollständigkeit')
|
||||
expect(result1.mode).toBe('validate')
|
||||
|
||||
// With ae/oe/ue substitution
|
||||
const result2 = classifier.classify('Pruefe die Vollstaendigkeit')
|
||||
expect(result2.mode).toBe('validate')
|
||||
})
|
||||
|
||||
it('should handle ß correctly', () => {
|
||||
const result = classifier.classify('Schließe Lücken')
|
||||
// Should still detect via normalized patterns
|
||||
expect(result).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('classify - combined mode + document type', () => {
|
||||
it('should detect both mode and document type', () => {
|
||||
const result = classifier.classify('Erstelle ein VVT fuer unsere Firma')
|
||||
expect(result.mode).toBe('draft')
|
||||
expect(result.detectedDocumentType).toBe('vvt')
|
||||
})
|
||||
|
||||
it('should detect validate + document type', () => {
|
||||
const result = classifier.classify('Pruefe mein TOM auf Konsistenz')
|
||||
expect(result.mode).toBe('validate')
|
||||
expect(result.detectedDocumentType).toBe('tom')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,311 @@
|
||||
import { StateProjector } from '../state-projector'
|
||||
import type { SDKState } from '../../types'
|
||||
|
||||
describe('StateProjector', () => {
|
||||
const projector = new StateProjector()
|
||||
|
||||
// Helper: minimal SDKState
|
||||
function makeState(overrides: Partial<SDKState> = {}): SDKState {
|
||||
return {
|
||||
version: '1.0.0',
|
||||
lastModified: new Date(),
|
||||
tenantId: 'test',
|
||||
userId: 'user1',
|
||||
subscription: 'PROFESSIONAL',
|
||||
customerType: null,
|
||||
companyProfile: null,
|
||||
complianceScope: null,
|
||||
currentPhase: 1,
|
||||
currentStep: 'company-profile',
|
||||
completedSteps: [],
|
||||
checkpoints: {},
|
||||
importedDocuments: [],
|
||||
gapAnalysis: null,
|
||||
useCases: [],
|
||||
activeUseCase: null,
|
||||
screening: null,
|
||||
modules: [],
|
||||
requirements: [],
|
||||
controls: [],
|
||||
evidence: [],
|
||||
checklist: [],
|
||||
risks: [],
|
||||
aiActClassification: null,
|
||||
obligations: [],
|
||||
dsfa: null,
|
||||
toms: [],
|
||||
retentionPolicies: [],
|
||||
vvt: [],
|
||||
documents: [],
|
||||
cookieBanner: null,
|
||||
consents: [],
|
||||
dsrConfig: null,
|
||||
escalationWorkflows: [],
|
||||
preferences: {
|
||||
language: 'de',
|
||||
theme: 'light',
|
||||
compactMode: false,
|
||||
showHints: true,
|
||||
autoSave: true,
|
||||
autoValidate: true,
|
||||
allowParallelWork: true,
|
||||
},
|
||||
...overrides,
|
||||
} as SDKState
|
||||
}
|
||||
|
||||
function makeDecisionState(level: string = 'L2'): SDKState {
|
||||
return makeState({
|
||||
companyProfile: {
|
||||
companyName: 'Test GmbH',
|
||||
industry: 'IT-Dienstleistung',
|
||||
employeeCount: 50,
|
||||
businessModel: 'SaaS',
|
||||
isPublicSector: false,
|
||||
} as any,
|
||||
complianceScope: {
|
||||
decision: {
|
||||
id: 'dec-1',
|
||||
determinedLevel: level,
|
||||
scores: { risk_score: 60, complexity_score: 50, assurance_need: 55, composite_score: 55 },
|
||||
triggeredHardTriggers: [],
|
||||
requiredDocuments: [
|
||||
{ documentType: 'vvt', label: 'VVT', required: true, depth: 'Standard', detailItems: ['Bezeichnung', 'Zweck'], estimatedEffort: '2h', triggeredBy: [] },
|
||||
{ documentType: 'tom', label: 'TOM', required: true, depth: 'Standard', detailItems: ['Verschluesselung'], estimatedEffort: '3h', triggeredBy: [] },
|
||||
{ documentType: 'lf', label: 'LF', required: true, depth: 'Basis', detailItems: [], estimatedEffort: '1h', triggeredBy: [] },
|
||||
],
|
||||
riskFlags: [
|
||||
{ id: 'rf-1', severity: 'MEDIUM', title: 'Cloud-Nutzung', description: '', recommendation: 'AVV pruefen' },
|
||||
],
|
||||
gaps: [
|
||||
{ id: 'gap-1', severity: 'high', title: 'TOM fehlt', description: 'Keine TOM definiert', relatedDocuments: ['tom'] },
|
||||
],
|
||||
nextActions: [],
|
||||
reasoning: [],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
answers: [],
|
||||
} as any,
|
||||
vvt: [{ id: 'vvt-1', name: 'Kundenverwaltung' }] as any[],
|
||||
toms: [],
|
||||
retentionPolicies: [],
|
||||
})
|
||||
}
|
||||
|
||||
describe('projectForDraft', () => {
|
||||
it('should return a DraftContext with correct structure', () => {
|
||||
const state = makeDecisionState()
|
||||
const result = projector.projectForDraft(state, 'vvt')
|
||||
|
||||
expect(result).toHaveProperty('decisions')
|
||||
expect(result).toHaveProperty('companyProfile')
|
||||
expect(result).toHaveProperty('constraints')
|
||||
expect(result.decisions.level).toBe('L2')
|
||||
})
|
||||
|
||||
it('should project company profile', () => {
|
||||
const state = makeDecisionState()
|
||||
const result = projector.projectForDraft(state, 'vvt')
|
||||
|
||||
expect(result.companyProfile.name).toBe('Test GmbH')
|
||||
expect(result.companyProfile.industry).toBe('IT-Dienstleistung')
|
||||
expect(result.companyProfile.employeeCount).toBe(50)
|
||||
})
|
||||
|
||||
it('should provide defaults when no company profile', () => {
|
||||
const state = makeState()
|
||||
const result = projector.projectForDraft(state, 'vvt')
|
||||
|
||||
expect(result.companyProfile.name).toBe('Unbekannt')
|
||||
expect(result.companyProfile.industry).toBe('Unbekannt')
|
||||
expect(result.companyProfile.employeeCount).toBe(0)
|
||||
})
|
||||
|
||||
it('should extract constraints and depth requirements', () => {
|
||||
const state = makeDecisionState()
|
||||
const result = projector.projectForDraft(state, 'vvt')
|
||||
|
||||
expect(result.constraints.depthRequirements).toBeDefined()
|
||||
expect(result.constraints.boundaries.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should extract risk flags', () => {
|
||||
const state = makeDecisionState()
|
||||
const result = projector.projectForDraft(state, 'vvt')
|
||||
|
||||
expect(result.constraints.riskFlags.length).toBe(1)
|
||||
expect(result.constraints.riskFlags[0].title).toBe('Cloud-Nutzung')
|
||||
})
|
||||
|
||||
it('should include existing document data when available', () => {
|
||||
const state = makeDecisionState()
|
||||
const result = projector.projectForDraft(state, 'vvt')
|
||||
|
||||
expect(result.existingDocumentData).toBeDefined()
|
||||
expect((result.existingDocumentData as any).totalCount).toBe(1)
|
||||
})
|
||||
|
||||
it('should return undefined existingDocumentData when none exists', () => {
|
||||
const state = makeDecisionState()
|
||||
const result = projector.projectForDraft(state, 'tom')
|
||||
|
||||
expect(result.existingDocumentData).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should filter required documents', () => {
|
||||
const state = makeDecisionState()
|
||||
const result = projector.projectForDraft(state, 'vvt')
|
||||
|
||||
expect(result.decisions.requiredDocuments.length).toBe(3)
|
||||
expect(result.decisions.requiredDocuments.every(d => d.documentType)).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle empty state gracefully', () => {
|
||||
const state = makeState()
|
||||
const result = projector.projectForDraft(state, 'vvt')
|
||||
|
||||
expect(result.decisions.level).toBe('L1')
|
||||
expect(result.decisions.hardTriggers).toEqual([])
|
||||
expect(result.decisions.requiredDocuments).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('projectForAsk', () => {
|
||||
it('should return a GapContext with correct structure', () => {
|
||||
const state = makeDecisionState()
|
||||
const result = projector.projectForAsk(state)
|
||||
|
||||
expect(result).toHaveProperty('unansweredQuestions')
|
||||
expect(result).toHaveProperty('gaps')
|
||||
expect(result).toHaveProperty('missingDocuments')
|
||||
})
|
||||
|
||||
it('should identify missing documents', () => {
|
||||
const state = makeDecisionState()
|
||||
// vvt exists, tom and lf are missing
|
||||
const result = projector.projectForAsk(state)
|
||||
|
||||
expect(result.missingDocuments.some(d => d.documentType === 'tom')).toBe(true)
|
||||
expect(result.missingDocuments.some(d => d.documentType === 'lf')).toBe(true)
|
||||
})
|
||||
|
||||
it('should not list existing documents as missing', () => {
|
||||
const state = makeDecisionState()
|
||||
const result = projector.projectForAsk(state)
|
||||
|
||||
// vvt exists in state
|
||||
expect(result.missingDocuments.some(d => d.documentType === 'vvt')).toBe(false)
|
||||
})
|
||||
|
||||
it('should include gaps from scope decision', () => {
|
||||
const state = makeDecisionState()
|
||||
const result = projector.projectForAsk(state)
|
||||
|
||||
expect(result.gaps.length).toBe(1)
|
||||
expect(result.gaps[0].title).toBe('TOM fehlt')
|
||||
})
|
||||
|
||||
it('should handle empty state', () => {
|
||||
const state = makeState()
|
||||
const result = projector.projectForAsk(state)
|
||||
|
||||
expect(result.gaps).toEqual([])
|
||||
expect(result.missingDocuments).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('projectForValidate', () => {
|
||||
it('should return a ValidationContext with correct structure', () => {
|
||||
const state = makeDecisionState()
|
||||
const result = projector.projectForValidate(state, ['vvt', 'tom', 'lf'])
|
||||
|
||||
expect(result).toHaveProperty('documents')
|
||||
expect(result).toHaveProperty('crossReferences')
|
||||
expect(result).toHaveProperty('scopeLevel')
|
||||
expect(result).toHaveProperty('depthRequirements')
|
||||
})
|
||||
|
||||
it('should include all requested document types', () => {
|
||||
const state = makeDecisionState()
|
||||
const result = projector.projectForValidate(state, ['vvt', 'tom'])
|
||||
|
||||
expect(result.documents.length).toBe(2)
|
||||
expect(result.documents.map(d => d.type)).toContain('vvt')
|
||||
expect(result.documents.map(d => d.type)).toContain('tom')
|
||||
})
|
||||
|
||||
it('should include cross-references', () => {
|
||||
const state = makeDecisionState()
|
||||
const result = projector.projectForValidate(state, ['vvt', 'tom', 'lf'])
|
||||
|
||||
expect(result.crossReferences).toHaveProperty('vvtCategories')
|
||||
expect(result.crossReferences).toHaveProperty('tomControls')
|
||||
expect(result.crossReferences).toHaveProperty('retentionCategories')
|
||||
expect(result.crossReferences.vvtCategories.length).toBe(1)
|
||||
expect(result.crossReferences.vvtCategories[0]).toBe('Kundenverwaltung')
|
||||
})
|
||||
|
||||
it('should include scope level', () => {
|
||||
const state = makeDecisionState('L3')
|
||||
const result = projector.projectForValidate(state, ['vvt'])
|
||||
|
||||
expect(result.scopeLevel).toBe('L3')
|
||||
})
|
||||
|
||||
it('should include depth requirements per document type', () => {
|
||||
const state = makeDecisionState()
|
||||
const result = projector.projectForValidate(state, ['vvt', 'tom'])
|
||||
|
||||
expect(result.depthRequirements).toHaveProperty('vvt')
|
||||
expect(result.depthRequirements).toHaveProperty('tom')
|
||||
})
|
||||
|
||||
it('should summarize documents', () => {
|
||||
const state = makeDecisionState()
|
||||
const result = projector.projectForValidate(state, ['vvt', 'tom'])
|
||||
|
||||
expect(result.documents[0].contentSummary).toContain('1')
|
||||
expect(result.documents[1].contentSummary).toContain('Keine TOM')
|
||||
})
|
||||
|
||||
it('should handle empty state', () => {
|
||||
const state = makeState()
|
||||
const result = projector.projectForValidate(state, ['vvt', 'tom', 'lf'])
|
||||
|
||||
expect(result.scopeLevel).toBe('L1')
|
||||
expect(result.crossReferences.vvtCategories).toEqual([])
|
||||
expect(result.crossReferences.tomControls).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('token budget estimation', () => {
|
||||
it('projectForDraft should produce compact output', () => {
|
||||
const state = makeDecisionState()
|
||||
const result = projector.projectForDraft(state, 'vvt')
|
||||
const json = JSON.stringify(result)
|
||||
|
||||
// Rough token estimation: ~4 chars per token
|
||||
const estimatedTokens = json.length / 4
|
||||
expect(estimatedTokens).toBeLessThan(2000) // Budget is ~1500
|
||||
})
|
||||
|
||||
it('projectForAsk should produce very compact output', () => {
|
||||
const state = makeDecisionState()
|
||||
const result = projector.projectForAsk(state)
|
||||
const json = JSON.stringify(result)
|
||||
|
||||
const estimatedTokens = json.length / 4
|
||||
expect(estimatedTokens).toBeLessThan(1000) // Budget is ~600
|
||||
})
|
||||
|
||||
it('projectForValidate should stay within budget', () => {
|
||||
const state = makeDecisionState()
|
||||
const result = projector.projectForValidate(state, ['vvt', 'tom', 'lf'])
|
||||
const json = JSON.stringify(result)
|
||||
|
||||
const estimatedTokens = json.length / 4
|
||||
expect(estimatedTokens).toBeLessThan(3000) // Budget is ~2000
|
||||
})
|
||||
})
|
||||
})
|
||||
221
admin-v2/lib/sdk/drafting-engine/constraint-enforcer.ts
Normal file
221
admin-v2/lib/sdk/drafting-engine/constraint-enforcer.ts
Normal file
@@ -0,0 +1,221 @@
|
||||
/**
|
||||
* Constraint Enforcer - Hard Gate vor jedem Draft
|
||||
*
|
||||
* Stellt sicher, dass die Drafting Engine NIEMALS die deterministische
|
||||
* Scope-Engine ueberschreibt. Prueft vor jedem Draft-Vorgang:
|
||||
*
|
||||
* 1. Ist der Dokumenttyp in requiredDocuments?
|
||||
* 2. Passt die Draft-Tiefe zum Level?
|
||||
* 3. Ist eine DSFA erforderlich (Hard Trigger)?
|
||||
* 4. Werden Risiko-Flags beruecksichtigt?
|
||||
*/
|
||||
|
||||
import type { ScopeDecision, ScopeDocumentType, ComplianceDepthLevel } from '../compliance-scope-types'
|
||||
import { DOCUMENT_SCOPE_MATRIX, getDepthLevelNumeric } from '../compliance-scope-types'
|
||||
import type { ConstraintCheckResult, DraftContext } from './types'
|
||||
|
||||
export class ConstraintEnforcer {
|
||||
|
||||
/**
|
||||
* Prueft ob ein Draft fuer den gegebenen Dokumenttyp erlaubt ist.
|
||||
* Dies ist ein HARD GATE - bei Violation wird der Draft blockiert.
|
||||
*/
|
||||
check(
|
||||
documentType: ScopeDocumentType,
|
||||
decision: ScopeDecision | null,
|
||||
requestedDepthLevel?: ComplianceDepthLevel
|
||||
): ConstraintCheckResult {
|
||||
const violations: string[] = []
|
||||
const adjustments: string[] = []
|
||||
const checkedRules: string[] = []
|
||||
|
||||
// Wenn keine Decision vorhanden: Nur Basis-Drafts erlauben
|
||||
if (!decision) {
|
||||
checkedRules.push('RULE-NO-DECISION')
|
||||
if (documentType !== 'vvt' && documentType !== 'tom' && documentType !== 'dsi') {
|
||||
violations.push(
|
||||
'Scope-Evaluierung fehlt. Bitte zuerst das Compliance-Profiling durchfuehren.'
|
||||
)
|
||||
} else {
|
||||
adjustments.push(
|
||||
'Ohne Scope-Evaluierung wird Level L1 (Basis) angenommen.'
|
||||
)
|
||||
}
|
||||
return {
|
||||
allowed: violations.length === 0,
|
||||
violations,
|
||||
adjustments,
|
||||
checkedRules,
|
||||
}
|
||||
}
|
||||
|
||||
const level = decision.determinedLevel
|
||||
const levelNumeric = getDepthLevelNumeric(level)
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Rule 1: Dokumenttyp in requiredDocuments?
|
||||
// -----------------------------------------------------------------------
|
||||
checkedRules.push('RULE-DOC-REQUIRED')
|
||||
const isRequired = decision.requiredDocuments.some(
|
||||
d => d.documentType === documentType && d.required
|
||||
)
|
||||
const scopeReq = DOCUMENT_SCOPE_MATRIX[documentType]?.[level]
|
||||
|
||||
if (!isRequired && scopeReq && !scopeReq.required) {
|
||||
// Nicht blockieren, aber warnen
|
||||
adjustments.push(
|
||||
`Dokument "${documentType}" ist auf Level ${level} nicht als Pflicht eingestuft. ` +
|
||||
`Entwurf ist moeglich, aber optional.`
|
||||
)
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Rule 2: Draft-Tiefe passt zum Level?
|
||||
// -----------------------------------------------------------------------
|
||||
checkedRules.push('RULE-DEPTH-MATCH')
|
||||
if (requestedDepthLevel) {
|
||||
const requestedNumeric = getDepthLevelNumeric(requestedDepthLevel)
|
||||
|
||||
if (requestedNumeric > levelNumeric) {
|
||||
violations.push(
|
||||
`Angefragte Tiefe ${requestedDepthLevel} ueberschreitet das bestimmte Level ${level}. ` +
|
||||
`Die Scope-Engine hat Level ${level} festgelegt. ` +
|
||||
`Ein Draft mit Tiefe ${requestedDepthLevel} ist nicht erlaubt.`
|
||||
)
|
||||
} else if (requestedNumeric < levelNumeric) {
|
||||
adjustments.push(
|
||||
`Angefragte Tiefe ${requestedDepthLevel} liegt unter dem bestimmten Level ${level}. ` +
|
||||
`Draft wird auf Level ${level} angehoben.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Rule 3: DSFA-Enforcement
|
||||
// -----------------------------------------------------------------------
|
||||
checkedRules.push('RULE-DSFA-ENFORCEMENT')
|
||||
if (documentType === 'dsfa') {
|
||||
const dsfaRequired = decision.triggeredHardTriggers.some(
|
||||
t => t.rule.dsfaRequired
|
||||
)
|
||||
|
||||
if (!dsfaRequired && level !== 'L4') {
|
||||
adjustments.push(
|
||||
'DSFA ist laut Scope-Engine nicht verpflichtend. ' +
|
||||
'Entwurf wird als freiwillige Massnahme gekennzeichnet.'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Umgekehrt: Wenn DSFA verpflichtend und Typ != dsfa, ggf. hinweisen
|
||||
if (documentType !== 'dsfa') {
|
||||
const dsfaRequired = decision.triggeredHardTriggers.some(
|
||||
t => t.rule.dsfaRequired
|
||||
)
|
||||
const dsfaInRequired = decision.requiredDocuments.some(
|
||||
d => d.documentType === 'dsfa' && d.required
|
||||
)
|
||||
|
||||
if (dsfaRequired && dsfaInRequired) {
|
||||
// Nur ein Hinweis, kein Block
|
||||
adjustments.push(
|
||||
'Hinweis: Eine DSFA ist laut Scope-Engine verpflichtend. ' +
|
||||
'Bitte sicherstellen, dass auch eine DSFA erstellt wird.'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Rule 4: Risiko-Flags beruecksichtigt?
|
||||
// -----------------------------------------------------------------------
|
||||
checkedRules.push('RULE-RISK-FLAGS')
|
||||
const criticalRisks = decision.riskFlags.filter(
|
||||
f => f.severity === 'CRITICAL' || f.severity === 'HIGH'
|
||||
)
|
||||
|
||||
if (criticalRisks.length > 0) {
|
||||
adjustments.push(
|
||||
`${criticalRisks.length} kritische/hohe Risiko-Flags erkannt. ` +
|
||||
`Draft muss diese adressieren: ${criticalRisks.map(r => r.title).join(', ')}`
|
||||
)
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Rule 5: Hard-Trigger Consistency
|
||||
// -----------------------------------------------------------------------
|
||||
checkedRules.push('RULE-HARD-TRIGGER-CONSISTENCY')
|
||||
for (const trigger of decision.triggeredHardTriggers) {
|
||||
const mandatoryDocs = trigger.rule.mandatoryDocuments
|
||||
if (mandatoryDocs.includes(documentType)) {
|
||||
// Gut - wir erstellen ein mandatory document
|
||||
} else {
|
||||
// Pruefen ob die mandatory documents des Triggers vorhanden sind
|
||||
// (nur Hinweis, kein Block)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
allowed: violations.length === 0,
|
||||
violations,
|
||||
adjustments,
|
||||
checkedRules,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience: Prueft aus einem DraftContext heraus.
|
||||
*/
|
||||
checkFromContext(
|
||||
documentType: ScopeDocumentType,
|
||||
context: DraftContext
|
||||
): ConstraintCheckResult {
|
||||
// Reconstruct a minimal ScopeDecision from context
|
||||
const pseudoDecision: ScopeDecision = {
|
||||
id: 'projected',
|
||||
determinedLevel: context.decisions.level,
|
||||
scores: context.decisions.scores,
|
||||
triggeredHardTriggers: context.decisions.hardTriggers.map(t => ({
|
||||
rule: {
|
||||
id: t.id,
|
||||
label: t.label,
|
||||
description: '',
|
||||
conditionField: '',
|
||||
conditionOperator: 'EQUALS' as const,
|
||||
conditionValue: null,
|
||||
minimumLevel: context.decisions.level,
|
||||
mandatoryDocuments: [],
|
||||
dsfaRequired: false,
|
||||
legalReference: t.legalReference,
|
||||
},
|
||||
matchedValue: null,
|
||||
explanation: '',
|
||||
})),
|
||||
requiredDocuments: context.decisions.requiredDocuments.map(d => ({
|
||||
documentType: d.documentType,
|
||||
label: d.documentType,
|
||||
required: true,
|
||||
depth: d.depth,
|
||||
detailItems: d.detailItems,
|
||||
estimatedEffort: '',
|
||||
triggeredBy: [],
|
||||
})),
|
||||
riskFlags: context.constraints.riskFlags.map(f => ({
|
||||
id: `rf-${f.title}`,
|
||||
severity: f.severity as 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL',
|
||||
title: f.title,
|
||||
description: '',
|
||||
recommendation: f.recommendation,
|
||||
})),
|
||||
gaps: [],
|
||||
nextActions: [],
|
||||
reasoning: [],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
}
|
||||
|
||||
return this.check(documentType, pseudoDecision)
|
||||
}
|
||||
}
|
||||
|
||||
/** Singleton-Instanz */
|
||||
export const constraintEnforcer = new ConstraintEnforcer()
|
||||
241
admin-v2/lib/sdk/drafting-engine/intent-classifier.ts
Normal file
241
admin-v2/lib/sdk/drafting-engine/intent-classifier.ts
Normal file
@@ -0,0 +1,241 @@
|
||||
/**
|
||||
* Intent Classifier - Leichtgewichtiger Pattern-Matcher
|
||||
*
|
||||
* Erkennt den Agent-Modus anhand des Nutzer-Inputs ohne LLM-Call.
|
||||
* Deutsche und englische Muster werden unterstuetzt.
|
||||
*
|
||||
* Confidence-Schwellen:
|
||||
* - >0.8: Hohe Sicherheit, automatisch anwenden
|
||||
* - 0.6-0.8: Mittel, Nutzer kann bestaetigen
|
||||
* - <0.6: Fallback zu 'explain'
|
||||
*/
|
||||
|
||||
import type { AgentMode, IntentClassification } from './types'
|
||||
import type { ScopeDocumentType } from '../compliance-scope-types'
|
||||
|
||||
// ============================================================================
|
||||
// Pattern Definitions
|
||||
// ============================================================================
|
||||
|
||||
interface ModePattern {
|
||||
mode: AgentMode
|
||||
patterns: RegExp[]
|
||||
/** Base-Confidence wenn ein Pattern matched */
|
||||
baseConfidence: number
|
||||
}
|
||||
|
||||
const MODE_PATTERNS: ModePattern[] = [
|
||||
{
|
||||
mode: 'draft',
|
||||
baseConfidence: 0.85,
|
||||
patterns: [
|
||||
/\b(erstell|generier|entw[iu]rf|entwer[ft]|schreib|verfass|formulier|anlege)/i,
|
||||
/\b(draft|create|generate|write|compose)\b/i,
|
||||
/\b(neues?\s+(?:vvt|tom|dsfa|dokument|loeschkonzept|datenschutzerklaerung))\b/i,
|
||||
/\b(vorlage|template)\s+(erstell|generier)/i,
|
||||
/\bfuer\s+(?:uns|mich|unser)\b.*\b(erstell|schreib)/i,
|
||||
],
|
||||
},
|
||||
{
|
||||
mode: 'validate',
|
||||
baseConfidence: 0.80,
|
||||
patterns: [
|
||||
/\b(pruef|validier|check|kontrollier|ueberpruef)\b/i,
|
||||
/\b(korrekt|richtig|vollstaendig|konsistent|komplett)\b.*\?/i,
|
||||
/\b(stimmt|passt)\b.*\b(das|mein|unser)\b/i,
|
||||
/\b(validate|verify|check|review)\b/i,
|
||||
/\b(fehler|luecken?|maengel)\b.*\b(find|such|zeig)\b/i,
|
||||
/\bcross[\s-]?check\b/i,
|
||||
/\b(vvt|tom|dsfa)\b.*\b(konsisten[tz]|widerspruch|uebereinstimm)/i,
|
||||
],
|
||||
},
|
||||
{
|
||||
mode: 'ask',
|
||||
baseConfidence: 0.75,
|
||||
patterns: [
|
||||
/\bwas\s+fehlt\b/i,
|
||||
/\b(luecken?|gaps?)\b.*\b(zeig|find|identifizier|analysier)/i,
|
||||
/\b(unvollstaendig|unfertig|offen)\b/i,
|
||||
/\bwelche\s+(dokumente?|informationen?|daten)\b.*\b(fehlen?|brauch|benoetig)/i,
|
||||
/\b(naechste[rn]?\s+schritt|next\s+step|todo)\b/i,
|
||||
/\bworan\s+(muss|soll)\b/i,
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
/** Dokumenttyp-Erkennung */
|
||||
const DOCUMENT_TYPE_PATTERNS: Array<{
|
||||
type: ScopeDocumentType
|
||||
patterns: RegExp[]
|
||||
}> = [
|
||||
{
|
||||
type: 'vvt',
|
||||
patterns: [
|
||||
/\bv{1,2}t\b/i,
|
||||
/\bverarbeitungsverzeichnis\b/i,
|
||||
/\bverarbeitungstaetigkeit/i,
|
||||
/\bprocessing\s+activit/i,
|
||||
/\bart\.?\s*30\b/i,
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'tom',
|
||||
patterns: [
|
||||
/\btom\b/i,
|
||||
/\btechnisch.*organisatorisch.*massnahm/i,
|
||||
/\bart\.?\s*32\b/i,
|
||||
/\bsicherheitsmassnahm/i,
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'dsfa',
|
||||
patterns: [
|
||||
/\bdsfa\b/i,
|
||||
/\bdatenschutz[\s-]?folgenabschaetzung\b/i,
|
||||
/\bdpia\b/i,
|
||||
/\bart\.?\s*35\b/i,
|
||||
/\bimpact\s+assessment\b/i,
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'dsi',
|
||||
patterns: [
|
||||
/\bdatenschutzerklaerung\b/i,
|
||||
/\bprivacy\s+policy\b/i,
|
||||
/\bdsi\b/i,
|
||||
/\bart\.?\s*13\b/i,
|
||||
/\bart\.?\s*14\b/i,
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'lf',
|
||||
patterns: [
|
||||
/\bloeschfrist/i,
|
||||
/\bloeschkonzept/i,
|
||||
/\bretention/i,
|
||||
/\baufbewahr/i,
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'av_vertrag',
|
||||
patterns: [
|
||||
/\bavv?\b/i,
|
||||
/\bauftragsverarbeit/i,
|
||||
/\bdata\s+processing\s+agreement/i,
|
||||
/\bart\.?\s*28\b/i,
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'betroffenenrechte',
|
||||
patterns: [
|
||||
/\bbetroffenenrecht/i,
|
||||
/\bdata\s+subject\s+right/i,
|
||||
/\bart\.?\s*15\b/i,
|
||||
/\bauskunft/i,
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'einwilligung',
|
||||
patterns: [
|
||||
/\beinwillig/i,
|
||||
/\bconsent/i,
|
||||
/\bcookie/i,
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
// ============================================================================
|
||||
// Classifier
|
||||
// ============================================================================
|
||||
|
||||
export class IntentClassifier {
|
||||
|
||||
/**
|
||||
* Klassifiziert die Nutzerabsicht anhand des Inputs.
|
||||
*
|
||||
* @param input - Die Nutzer-Nachricht
|
||||
* @returns IntentClassification mit Mode, Confidence, Patterns
|
||||
*/
|
||||
classify(input: string): IntentClassification {
|
||||
const normalized = this.normalize(input)
|
||||
let bestMatch: IntentClassification = {
|
||||
mode: 'explain',
|
||||
confidence: 0.3,
|
||||
matchedPatterns: [],
|
||||
}
|
||||
|
||||
for (const modePattern of MODE_PATTERNS) {
|
||||
const matched: string[] = []
|
||||
|
||||
for (const pattern of modePattern.patterns) {
|
||||
if (pattern.test(normalized)) {
|
||||
matched.push(pattern.source)
|
||||
}
|
||||
}
|
||||
|
||||
if (matched.length > 0) {
|
||||
// Mehr Matches = hoehere Confidence (bis zum Maximum)
|
||||
const matchBonus = Math.min(matched.length - 1, 2) * 0.05
|
||||
const confidence = Math.min(modePattern.baseConfidence + matchBonus, 0.99)
|
||||
|
||||
if (confidence > bestMatch.confidence) {
|
||||
bestMatch = {
|
||||
mode: modePattern.mode,
|
||||
confidence,
|
||||
matchedPatterns: matched,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dokumenttyp erkennen
|
||||
const detectedDocType = this.detectDocumentType(normalized)
|
||||
if (detectedDocType) {
|
||||
bestMatch.detectedDocumentType = detectedDocType
|
||||
// Dokumenttyp-Erkennung erhoeht Confidence leicht
|
||||
bestMatch.confidence = Math.min(bestMatch.confidence + 0.05, 0.99)
|
||||
}
|
||||
|
||||
// Fallback: Bei Confidence <0.6 immer 'explain'
|
||||
if (bestMatch.confidence < 0.6) {
|
||||
bestMatch.mode = 'explain'
|
||||
}
|
||||
|
||||
return bestMatch
|
||||
}
|
||||
|
||||
/**
|
||||
* Erkennt den Dokumenttyp aus dem Input.
|
||||
*/
|
||||
detectDocumentType(input: string): ScopeDocumentType | undefined {
|
||||
const normalized = this.normalize(input)
|
||||
|
||||
for (const docPattern of DOCUMENT_TYPE_PATTERNS) {
|
||||
for (const pattern of docPattern.patterns) {
|
||||
if (pattern.test(normalized)) {
|
||||
return docPattern.type
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalisiert den Input fuer Pattern-Matching.
|
||||
* Ersetzt Umlaute, entfernt Sonderzeichen.
|
||||
*/
|
||||
private normalize(input: string): string {
|
||||
return input
|
||||
.replace(/ä/g, 'ae')
|
||||
.replace(/ö/g, 'oe')
|
||||
.replace(/ü/g, 'ue')
|
||||
.replace(/ß/g, 'ss')
|
||||
.replace(/Ä/g, 'Ae')
|
||||
.replace(/Ö/g, 'Oe')
|
||||
.replace(/Ü/g, 'Ue')
|
||||
}
|
||||
}
|
||||
|
||||
/** Singleton-Instanz */
|
||||
export const intentClassifier = new IntentClassifier()
|
||||
49
admin-v2/lib/sdk/drafting-engine/prompts/ask-gap-analysis.ts
Normal file
49
admin-v2/lib/sdk/drafting-engine/prompts/ask-gap-analysis.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Gap Analysis Prompt - Lueckenanalyse und gezielte Fragen
|
||||
*/
|
||||
|
||||
import type { GapContext } from '../types'
|
||||
|
||||
export interface GapAnalysisInput {
|
||||
context: GapContext
|
||||
instructions?: string
|
||||
}
|
||||
|
||||
export function buildGapAnalysisPrompt(input: GapAnalysisInput): string {
|
||||
const { context, instructions } = input
|
||||
|
||||
return `## Aufgabe: Compliance-Lueckenanalyse
|
||||
|
||||
### Identifizierte Luecken:
|
||||
${context.gaps.length > 0
|
||||
? context.gaps.map(g => `- [${g.severity}] ${g.title}: ${g.description}`).join('\n')
|
||||
: '- Keine Luecken identifiziert'}
|
||||
|
||||
### Fehlende Pflichtdokumente:
|
||||
${context.missingDocuments.length > 0
|
||||
? context.missingDocuments.map(d => `- ${d.label} (Tiefe: ${d.depth}, Aufwand: ${d.estimatedEffort})`).join('\n')
|
||||
: '- Alle Pflichtdokumente vorhanden'}
|
||||
|
||||
### Unbeantwortete Fragen:
|
||||
${context.unansweredQuestions.length > 0
|
||||
? context.unansweredQuestions.map(q => `- [${q.blockId}] ${q.question}`).join('\n')
|
||||
: '- Alle Fragen beantwortet'}
|
||||
|
||||
${instructions ? `### Zusaetzliche Anweisungen: ${instructions}` : ''}
|
||||
|
||||
### Aufgabe:
|
||||
Analysiere den Stand und stelle EINE gezielte Frage, die die wichtigste Luecke adressiert.
|
||||
Priorisiere nach:
|
||||
1. Fehlende Pflichtdokumente
|
||||
2. Kritische Luecken (HIGH/CRITICAL severity)
|
||||
3. Unbeantwortete Pflichtfragen
|
||||
4. Mittlere Luecken
|
||||
|
||||
### Antwort-Format:
|
||||
Antworte in dieser Struktur:
|
||||
1. **Statusuebersicht**: Kurze Zusammenfassung des Compliance-Stands (2-3 Saetze)
|
||||
2. **Wichtigste Luecke**: Was fehlt am dringendsten?
|
||||
3. **Gezielte Frage**: Eine konkrete Frage an den Nutzer
|
||||
4. **Warum wichtig**: Warum muss diese Luecke geschlossen werden?
|
||||
5. **Empfohlener naechster Schritt**: Link/Verweis zum SDK-Modul`
|
||||
}
|
||||
91
admin-v2/lib/sdk/drafting-engine/prompts/draft-dsfa.ts
Normal file
91
admin-v2/lib/sdk/drafting-engine/prompts/draft-dsfa.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* DSFA Draft Prompt - Datenschutz-Folgenabschaetzung (Art. 35 DSGVO)
|
||||
*/
|
||||
|
||||
import type { DraftContext } from '../types'
|
||||
|
||||
export interface DSFADraftInput {
|
||||
context: DraftContext
|
||||
processingDescription?: string
|
||||
instructions?: string
|
||||
}
|
||||
|
||||
export function buildDSFADraftPrompt(input: DSFADraftInput): string {
|
||||
const { context, processingDescription, instructions } = input
|
||||
const level = context.decisions.level
|
||||
const depthItems = context.constraints.depthRequirements.detailItems
|
||||
const hardTriggers = context.decisions.hardTriggers
|
||||
|
||||
return `## Aufgabe: DSFA entwerfen (Art. 35 DSGVO)
|
||||
|
||||
### Unternehmensprofil
|
||||
- Name: ${context.companyProfile.name}
|
||||
- Branche: ${context.companyProfile.industry}
|
||||
- Mitarbeiter: ${context.companyProfile.employeeCount}
|
||||
|
||||
### Compliance-Level: ${level}
|
||||
Tiefe: ${context.constraints.depthRequirements.depth}
|
||||
|
||||
### Hard Triggers (Gruende fuer DSFA-Pflicht):
|
||||
${hardTriggers.length > 0
|
||||
? hardTriggers.map(t => `- ${t.id}: ${t.label} (${t.legalReference})`).join('\n')
|
||||
: '- Keine Hard Triggers (DSFA auf Wunsch)'}
|
||||
|
||||
### Erforderliche Inhalte:
|
||||
${depthItems.map((item, i) => `${i + 1}. ${item}`).join('\n')}
|
||||
|
||||
${processingDescription ? `### Beschreibung der Verarbeitung: ${processingDescription}` : ''}
|
||||
${instructions ? `### Zusaetzliche Anweisungen: ${instructions}` : ''}
|
||||
|
||||
### Antwort-Format
|
||||
Antworte als JSON:
|
||||
{
|
||||
"sections": [
|
||||
{
|
||||
"id": "beschreibung",
|
||||
"title": "Systematische Beschreibung der Verarbeitung",
|
||||
"content": "...",
|
||||
"schemaField": "processingDescription"
|
||||
},
|
||||
{
|
||||
"id": "notwendigkeit",
|
||||
"title": "Notwendigkeit und Verhaeltnismaessigkeit",
|
||||
"content": "...",
|
||||
"schemaField": "necessityAssessment"
|
||||
},
|
||||
{
|
||||
"id": "risikobewertung",
|
||||
"title": "Bewertung der Risiken fuer die Rechte und Freiheiten",
|
||||
"content": "...",
|
||||
"schemaField": "riskAssessment"
|
||||
},
|
||||
{
|
||||
"id": "massnahmen",
|
||||
"title": "Massnahmen zur Eindaemmung der Risiken",
|
||||
"content": "...",
|
||||
"schemaField": "mitigationMeasures"
|
||||
},
|
||||
{
|
||||
"id": "stellungnahme_dsb",
|
||||
"title": "Stellungnahme des Datenschutzbeauftragten",
|
||||
"content": "...",
|
||||
"schemaField": "dpoOpinion"
|
||||
},
|
||||
{
|
||||
"id": "standpunkt_betroffene",
|
||||
"title": "Standpunkt der betroffenen Personen",
|
||||
"content": "...",
|
||||
"schemaField": "dataSubjectView"
|
||||
},
|
||||
{
|
||||
"id": "ergebnis",
|
||||
"title": "Ergebnis und Empfehlung",
|
||||
"content": "...",
|
||||
"schemaField": "conclusion"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Halte die Tiefe exakt auf Level ${level}.
|
||||
Nutze WP248-Kriterien als Leitfaden fuer die Risikobewertung.`
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Loeschfristen Draft Prompt - Loeschkonzept
|
||||
*/
|
||||
|
||||
import type { DraftContext } from '../types'
|
||||
|
||||
export interface LoeschfristenDraftInput {
|
||||
context: DraftContext
|
||||
instructions?: string
|
||||
}
|
||||
|
||||
export function buildLoeschfristenDraftPrompt(input: LoeschfristenDraftInput): string {
|
||||
const { context, instructions } = input
|
||||
const level = context.decisions.level
|
||||
const depthItems = context.constraints.depthRequirements.detailItems
|
||||
|
||||
return `## Aufgabe: Loeschkonzept / Loeschfristen entwerfen
|
||||
|
||||
### Unternehmensprofil
|
||||
- Name: ${context.companyProfile.name}
|
||||
- Branche: ${context.companyProfile.industry}
|
||||
- Mitarbeiter: ${context.companyProfile.employeeCount}
|
||||
|
||||
### Compliance-Level: ${level}
|
||||
Tiefe: ${context.constraints.depthRequirements.depth}
|
||||
|
||||
### Erforderliche Inhalte:
|
||||
${depthItems.map((item, i) => `${i + 1}. ${item}`).join('\n')}
|
||||
|
||||
${context.existingDocumentData ? `### Bestehende Loeschfristen: ${JSON.stringify(context.existingDocumentData).slice(0, 500)}` : ''}
|
||||
${instructions ? `### Zusaetzliche Anweisungen: ${instructions}` : ''}
|
||||
|
||||
### Antwort-Format
|
||||
Antworte als JSON:
|
||||
{
|
||||
"sections": [
|
||||
{
|
||||
"id": "grundsaetze",
|
||||
"title": "Grundsaetze der Datenlöschung",
|
||||
"content": "...",
|
||||
"schemaField": "principles"
|
||||
},
|
||||
{
|
||||
"id": "kategorien",
|
||||
"title": "Datenkategorien und Loeschfristen",
|
||||
"content": "Tabellarische Uebersicht...",
|
||||
"schemaField": "retentionSchedule"
|
||||
},
|
||||
{
|
||||
"id": "gesetzliche_fristen",
|
||||
"title": "Gesetzliche Aufbewahrungsfristen",
|
||||
"content": "HGB, AO, weitere...",
|
||||
"schemaField": "legalRetention"
|
||||
},
|
||||
{
|
||||
"id": "loeschprozess",
|
||||
"title": "Technischer Loeschprozess",
|
||||
"content": "...",
|
||||
"schemaField": "deletionProcess"
|
||||
},
|
||||
{
|
||||
"id": "verantwortlichkeiten",
|
||||
"title": "Verantwortlichkeiten",
|
||||
"content": "...",
|
||||
"schemaField": "responsibilities"
|
||||
},
|
||||
{
|
||||
"id": "ausnahmen",
|
||||
"title": "Ausnahmen und Sonderfaelle",
|
||||
"content": "...",
|
||||
"schemaField": "exceptions"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Halte die Tiefe exakt auf Level ${level}.
|
||||
Beruecksichtige branchenspezifische Aufbewahrungsfristen fuer ${context.companyProfile.industry}.`
|
||||
}
|
||||
102
admin-v2/lib/sdk/drafting-engine/prompts/draft-privacy-policy.ts
Normal file
102
admin-v2/lib/sdk/drafting-engine/prompts/draft-privacy-policy.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* Privacy Policy Draft Prompt - Datenschutzerklaerung (Art. 13/14 DSGVO)
|
||||
*/
|
||||
|
||||
import type { DraftContext } from '../types'
|
||||
|
||||
export interface PrivacyPolicyDraftInput {
|
||||
context: DraftContext
|
||||
websiteUrl?: string
|
||||
instructions?: string
|
||||
}
|
||||
|
||||
export function buildPrivacyPolicyDraftPrompt(input: PrivacyPolicyDraftInput): string {
|
||||
const { context, websiteUrl, instructions } = input
|
||||
const level = context.decisions.level
|
||||
const depthItems = context.constraints.depthRequirements.detailItems
|
||||
|
||||
return `## Aufgabe: Datenschutzerklaerung entwerfen (Art. 13/14 DSGVO)
|
||||
|
||||
### Unternehmensprofil
|
||||
- Name: ${context.companyProfile.name}
|
||||
- Branche: ${context.companyProfile.industry}
|
||||
${context.companyProfile.dataProtectionOfficer ? `- DSB: ${context.companyProfile.dataProtectionOfficer.name} (${context.companyProfile.dataProtectionOfficer.email})` : ''}
|
||||
${websiteUrl ? `- Website: ${websiteUrl}` : ''}
|
||||
|
||||
### Compliance-Level: ${level}
|
||||
Tiefe: ${context.constraints.depthRequirements.depth}
|
||||
|
||||
### Erforderliche Inhalte:
|
||||
${depthItems.map((item, i) => `${i + 1}. ${item}`).join('\n')}
|
||||
|
||||
${instructions ? `### Zusaetzliche Anweisungen: ${instructions}` : ''}
|
||||
|
||||
### Antwort-Format
|
||||
Antworte als JSON:
|
||||
{
|
||||
"sections": [
|
||||
{
|
||||
"id": "verantwortlicher",
|
||||
"title": "Verantwortlicher",
|
||||
"content": "...",
|
||||
"schemaField": "controller"
|
||||
},
|
||||
{
|
||||
"id": "dsb",
|
||||
"title": "Datenschutzbeauftragter",
|
||||
"content": "...",
|
||||
"schemaField": "dpo"
|
||||
},
|
||||
{
|
||||
"id": "verarbeitungen",
|
||||
"title": "Verarbeitungstaetigkeiten und Zwecke",
|
||||
"content": "...",
|
||||
"schemaField": "processingPurposes"
|
||||
},
|
||||
{
|
||||
"id": "rechtsgrundlagen",
|
||||
"title": "Rechtsgrundlagen der Verarbeitung",
|
||||
"content": "...",
|
||||
"schemaField": "legalBases"
|
||||
},
|
||||
{
|
||||
"id": "empfaenger",
|
||||
"title": "Empfaenger und Datenweitergabe",
|
||||
"content": "...",
|
||||
"schemaField": "recipients"
|
||||
},
|
||||
{
|
||||
"id": "drittland",
|
||||
"title": "Uebermittlung in Drittlaender",
|
||||
"content": "...",
|
||||
"schemaField": "thirdCountryTransfers"
|
||||
},
|
||||
{
|
||||
"id": "speicherdauer",
|
||||
"title": "Speicherdauer",
|
||||
"content": "...",
|
||||
"schemaField": "retentionPeriods"
|
||||
},
|
||||
{
|
||||
"id": "betroffenenrechte",
|
||||
"title": "Ihre Rechte als betroffene Person",
|
||||
"content": "...",
|
||||
"schemaField": "dataSubjectRights"
|
||||
},
|
||||
{
|
||||
"id": "cookies",
|
||||
"title": "Cookies und Tracking",
|
||||
"content": "...",
|
||||
"schemaField": "cookies"
|
||||
},
|
||||
{
|
||||
"id": "aenderungen",
|
||||
"title": "Aenderungen dieser Datenschutzerklaerung",
|
||||
"content": "...",
|
||||
"schemaField": "changes"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Halte die Tiefe exakt auf Level ${level}.`
|
||||
}
|
||||
99
admin-v2/lib/sdk/drafting-engine/prompts/draft-tom.ts
Normal file
99
admin-v2/lib/sdk/drafting-engine/prompts/draft-tom.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* TOM Draft Prompt - Technische und Organisatorische Massnahmen (Art. 32 DSGVO)
|
||||
*/
|
||||
|
||||
import type { DraftContext } from '../types'
|
||||
|
||||
export interface TOMDraftInput {
|
||||
context: DraftContext
|
||||
focusArea?: string
|
||||
instructions?: string
|
||||
}
|
||||
|
||||
export function buildTOMDraftPrompt(input: TOMDraftInput): string {
|
||||
const { context, focusArea, instructions } = input
|
||||
const level = context.decisions.level
|
||||
const depthItems = context.constraints.depthRequirements.detailItems
|
||||
|
||||
return `## Aufgabe: TOM-Dokument entwerfen (Art. 32 DSGVO)
|
||||
|
||||
### Unternehmensprofil
|
||||
- Name: ${context.companyProfile.name}
|
||||
- Branche: ${context.companyProfile.industry}
|
||||
- Mitarbeiter: ${context.companyProfile.employeeCount}
|
||||
|
||||
### Compliance-Level: ${level}
|
||||
Tiefe: ${context.constraints.depthRequirements.depth}
|
||||
|
||||
### Erforderliche Inhalte fuer Level ${level}:
|
||||
${depthItems.map((item, i) => `${i + 1}. ${item}`).join('\n')}
|
||||
|
||||
### Constraints
|
||||
${context.constraints.boundaries.map(b => `- ${b}`).join('\n')}
|
||||
|
||||
${context.constraints.riskFlags.length > 0 ? `### Risiko-Flags
|
||||
${context.constraints.riskFlags.map(f => `- [${f.severity}] ${f.title}`).join('\n')}` : ''}
|
||||
|
||||
${focusArea ? `### Fokusbereich: ${focusArea}` : ''}
|
||||
${instructions ? `### Zusaetzliche Anweisungen: ${instructions}` : ''}
|
||||
|
||||
${context.existingDocumentData ? `### Bestehende TOM: ${JSON.stringify(context.existingDocumentData).slice(0, 500)}` : ''}
|
||||
|
||||
### Antwort-Format
|
||||
Antworte als JSON:
|
||||
{
|
||||
"sections": [
|
||||
{
|
||||
"id": "zutrittskontrolle",
|
||||
"title": "Zutrittskontrolle",
|
||||
"content": "Massnahmen die unbefugten Zutritt zu Datenverarbeitungsanlagen verhindern...",
|
||||
"schemaField": "accessControl"
|
||||
},
|
||||
{
|
||||
"id": "zugangskontrolle",
|
||||
"title": "Zugangskontrolle",
|
||||
"content": "Massnahmen gegen unbefugte Systemnutzung...",
|
||||
"schemaField": "systemAccessControl"
|
||||
},
|
||||
{
|
||||
"id": "zugriffskontrolle",
|
||||
"title": "Zugriffskontrolle",
|
||||
"content": "Massnahmen zur Sicherstellung berechtigter Datenzugriffe...",
|
||||
"schemaField": "dataAccessControl"
|
||||
},
|
||||
{
|
||||
"id": "weitergabekontrolle",
|
||||
"title": "Weitergabekontrolle / Uebertragungssicherheit",
|
||||
"content": "Massnahmen bei Datenuebertragung und -transport...",
|
||||
"schemaField": "transferControl"
|
||||
},
|
||||
{
|
||||
"id": "eingabekontrolle",
|
||||
"title": "Eingabekontrolle",
|
||||
"content": "Nachvollziehbarkeit von Dateneingaben...",
|
||||
"schemaField": "inputControl"
|
||||
},
|
||||
{
|
||||
"id": "auftragskontrolle",
|
||||
"title": "Auftragskontrolle",
|
||||
"content": "Massnahmen zur weisungsgemaessen Auftragsverarbeitung...",
|
||||
"schemaField": "orderControl"
|
||||
},
|
||||
{
|
||||
"id": "verfuegbarkeitskontrolle",
|
||||
"title": "Verfuegbarkeitskontrolle",
|
||||
"content": "Schutz gegen Datenverlust...",
|
||||
"schemaField": "availabilityControl"
|
||||
},
|
||||
{
|
||||
"id": "trennungsgebot",
|
||||
"title": "Trennungsgebot",
|
||||
"content": "Getrennte Verarbeitung fuer verschiedene Zwecke...",
|
||||
"schemaField": "separationControl"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Fuelle fehlende Informationen mit [PLATZHALTER: ...].
|
||||
Halte die Tiefe exakt auf Level ${level}.`
|
||||
}
|
||||
109
admin-v2/lib/sdk/drafting-engine/prompts/draft-vvt.ts
Normal file
109
admin-v2/lib/sdk/drafting-engine/prompts/draft-vvt.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* VVT Draft Prompt - Verarbeitungsverzeichnis (Art. 30 DSGVO)
|
||||
*/
|
||||
|
||||
import type { DraftContext } from '../types'
|
||||
|
||||
export interface VVTDraftInput {
|
||||
context: DraftContext
|
||||
activityName?: string
|
||||
activityPurpose?: string
|
||||
instructions?: string
|
||||
}
|
||||
|
||||
export function buildVVTDraftPrompt(input: VVTDraftInput): string {
|
||||
const { context, activityName, activityPurpose, instructions } = input
|
||||
const level = context.decisions.level
|
||||
const depthItems = context.constraints.depthRequirements.detailItems
|
||||
|
||||
return `## Aufgabe: VVT-Eintrag entwerfen (Art. 30 DSGVO)
|
||||
|
||||
### Unternehmensprofil
|
||||
- Name: ${context.companyProfile.name}
|
||||
- Branche: ${context.companyProfile.industry}
|
||||
- Mitarbeiter: ${context.companyProfile.employeeCount}
|
||||
- Geschaeftsmodell: ${context.companyProfile.businessModel}
|
||||
${context.companyProfile.dataProtectionOfficer ? `- DSB: ${context.companyProfile.dataProtectionOfficer.name} (${context.companyProfile.dataProtectionOfficer.email})` : '- DSB: Nicht benannt'}
|
||||
|
||||
### Compliance-Level: ${level}
|
||||
Tiefe: ${context.constraints.depthRequirements.depth}
|
||||
|
||||
### Erforderliche Inhalte fuer Level ${level}:
|
||||
${depthItems.map((item, i) => `${i + 1}. ${item}`).join('\n')}
|
||||
|
||||
### Constraints
|
||||
${context.constraints.boundaries.map(b => `- ${b}`).join('\n')}
|
||||
|
||||
${context.constraints.riskFlags.length > 0 ? `### Risiko-Flags
|
||||
${context.constraints.riskFlags.map(f => `- [${f.severity}] ${f.title}: ${f.recommendation}`).join('\n')}` : ''}
|
||||
|
||||
${activityName ? `### Gewuenschte Verarbeitungstaetigkeit: ${activityName}` : ''}
|
||||
${activityPurpose ? `### Zweck: ${activityPurpose}` : ''}
|
||||
${instructions ? `### Zusaetzliche Anweisungen: ${instructions}` : ''}
|
||||
|
||||
${context.existingDocumentData ? `### Bestehende VVT-Eintraege: ${JSON.stringify(context.existingDocumentData).slice(0, 500)}` : ''}
|
||||
|
||||
### Antwort-Format
|
||||
Antworte als JSON:
|
||||
{
|
||||
"sections": [
|
||||
{
|
||||
"id": "bezeichnung",
|
||||
"title": "Bezeichnung der Verarbeitungstaetigkeit",
|
||||
"content": "...",
|
||||
"schemaField": "name"
|
||||
},
|
||||
{
|
||||
"id": "verantwortlicher",
|
||||
"title": "Verantwortlicher",
|
||||
"content": "...",
|
||||
"schemaField": "controller"
|
||||
},
|
||||
{
|
||||
"id": "zweck",
|
||||
"title": "Zweck der Verarbeitung",
|
||||
"content": "...",
|
||||
"schemaField": "purpose"
|
||||
},
|
||||
{
|
||||
"id": "rechtsgrundlage",
|
||||
"title": "Rechtsgrundlage",
|
||||
"content": "...",
|
||||
"schemaField": "legalBasis"
|
||||
},
|
||||
{
|
||||
"id": "betroffene",
|
||||
"title": "Kategorien betroffener Personen",
|
||||
"content": "...",
|
||||
"schemaField": "dataSubjects"
|
||||
},
|
||||
{
|
||||
"id": "datenkategorien",
|
||||
"title": "Kategorien personenbezogener Daten",
|
||||
"content": "...",
|
||||
"schemaField": "dataCategories"
|
||||
},
|
||||
{
|
||||
"id": "empfaenger",
|
||||
"title": "Empfaenger",
|
||||
"content": "...",
|
||||
"schemaField": "recipients"
|
||||
},
|
||||
{
|
||||
"id": "speicherdauer",
|
||||
"title": "Speicherdauer / Loeschfristen",
|
||||
"content": "...",
|
||||
"schemaField": "retentionPeriod"
|
||||
},
|
||||
{
|
||||
"id": "tom_referenz",
|
||||
"title": "TOM-Referenz",
|
||||
"content": "...",
|
||||
"schemaField": "tomReference"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Fuelle fehlende Informationen mit [PLATZHALTER: Beschreibung was hier eingetragen werden muss].
|
||||
Halte die Tiefe exakt auf Level ${level} (${context.constraints.depthRequirements.depth}).`
|
||||
}
|
||||
11
admin-v2/lib/sdk/drafting-engine/prompts/index.ts
Normal file
11
admin-v2/lib/sdk/drafting-engine/prompts/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Drafting Engine Prompts - Re-Exports
|
||||
*/
|
||||
|
||||
export { buildVVTDraftPrompt, type VVTDraftInput } from './draft-vvt'
|
||||
export { buildTOMDraftPrompt, type TOMDraftInput } from './draft-tom'
|
||||
export { buildDSFADraftPrompt, type DSFADraftInput } from './draft-dsfa'
|
||||
export { buildPrivacyPolicyDraftPrompt, type PrivacyPolicyDraftInput } from './draft-privacy-policy'
|
||||
export { buildLoeschfristenDraftPrompt, type LoeschfristenDraftInput } from './draft-loeschfristen'
|
||||
export { buildCrossCheckPrompt, type CrossCheckInput } from './validate-cross-check'
|
||||
export { buildGapAnalysisPrompt, type GapAnalysisInput } from './ask-gap-analysis'
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Cross-Document Validation Prompt
|
||||
*/
|
||||
|
||||
import type { ValidationContext } from '../types'
|
||||
|
||||
export interface CrossCheckInput {
|
||||
context: ValidationContext
|
||||
focusDocuments?: string[]
|
||||
instructions?: string
|
||||
}
|
||||
|
||||
export function buildCrossCheckPrompt(input: CrossCheckInput): string {
|
||||
const { context, focusDocuments, instructions } = input
|
||||
|
||||
return `## Aufgabe: Cross-Dokument-Konsistenzpruefung
|
||||
|
||||
### Scope-Level: ${context.scopeLevel}
|
||||
|
||||
### Vorhandene Dokumente:
|
||||
${context.documents.map(d => `- ${d.type}: ${d.contentSummary}`).join('\n')}
|
||||
|
||||
### Cross-Referenzen:
|
||||
- VVT-Kategorien: ${context.crossReferences.vvtCategories.join(', ') || 'Keine'}
|
||||
- DSFA-Risiken: ${context.crossReferences.dsfaRisks.join(', ') || 'Keine'}
|
||||
- TOM-Controls: ${context.crossReferences.tomControls.join(', ') || 'Keine'}
|
||||
- Loeschfristen-Kategorien: ${context.crossReferences.retentionCategories.join(', ') || 'Keine'}
|
||||
|
||||
### Tiefenpruefung pro Dokument:
|
||||
${context.documents.map(d => {
|
||||
const req = context.depthRequirements[d.type]
|
||||
return req ? `- ${d.type}: Erforderlich=${req.required}, Tiefe=${req.depth}` : `- ${d.type}: Keine Requirements`
|
||||
}).join('\n')}
|
||||
|
||||
${focusDocuments ? `### Fokus auf: ${focusDocuments.join(', ')}` : ''}
|
||||
${instructions ? `### Zusaetzliche Anweisungen: ${instructions}` : ''}
|
||||
|
||||
### Pruefkriterien:
|
||||
1. Jede VVT-Taetigkeit muss einen TOM-Verweis haben
|
||||
2. Jede VVT-Kategorie muss eine Loeschfrist haben
|
||||
3. Bei DSFA-pflichtigen Verarbeitungen muss eine DSFA existieren
|
||||
4. TOM-Massnahmen muessen zum Risikoprofil passen
|
||||
5. Loeschfristen duerfen gesetzliche Minima nicht unterschreiten
|
||||
6. Dokument-Tiefe muss Level ${context.scopeLevel} entsprechen
|
||||
|
||||
### Antwort-Format
|
||||
Antworte als JSON:
|
||||
{
|
||||
"passed": true/false,
|
||||
"errors": [
|
||||
{
|
||||
"id": "ERR-001",
|
||||
"severity": "error",
|
||||
"category": "scope_violation|inconsistency|missing_content|depth_mismatch|cross_reference",
|
||||
"title": "...",
|
||||
"description": "...",
|
||||
"documentType": "vvt|tom|dsfa|...",
|
||||
"crossReferenceType": "...",
|
||||
"legalReference": "Art. ... DSGVO",
|
||||
"suggestion": "..."
|
||||
}
|
||||
],
|
||||
"warnings": [...],
|
||||
"suggestions": [...]
|
||||
}`
|
||||
}
|
||||
337
admin-v2/lib/sdk/drafting-engine/state-projector.ts
Normal file
337
admin-v2/lib/sdk/drafting-engine/state-projector.ts
Normal file
@@ -0,0 +1,337 @@
|
||||
/**
|
||||
* State Projector - Token-budgetierte Projektion des SDK-State
|
||||
*
|
||||
* Extrahiert aus dem vollen SDKState (der ~50k Tokens betragen kann) nur die
|
||||
* relevanten Slices fuer den jeweiligen Agent-Modus.
|
||||
*
|
||||
* Token-Budgets:
|
||||
* - Draft: ~1500 Tokens
|
||||
* - Ask: ~600 Tokens
|
||||
* - Validate: ~2000 Tokens
|
||||
*/
|
||||
|
||||
import type { SDKState, CompanyProfile } from '../types'
|
||||
import type {
|
||||
ComplianceScopeState,
|
||||
ScopeDecision,
|
||||
ScopeDocumentType,
|
||||
ScopeGap,
|
||||
RequiredDocument,
|
||||
RiskFlag,
|
||||
DOCUMENT_SCOPE_MATRIX,
|
||||
DocumentDepthRequirement,
|
||||
} from '../compliance-scope-types'
|
||||
import { DOCUMENT_SCOPE_MATRIX as DOC_MATRIX, DOCUMENT_TYPE_LABELS } from '../compliance-scope-types'
|
||||
import type {
|
||||
DraftContext,
|
||||
GapContext,
|
||||
ValidationContext,
|
||||
} from './types'
|
||||
|
||||
// ============================================================================
|
||||
// State Projector
|
||||
// ============================================================================
|
||||
|
||||
export class StateProjector {
|
||||
|
||||
/**
|
||||
* Projiziert den SDKState fuer Draft-Operationen.
|
||||
* Fokus: Scope-Decision, Company-Profile, Dokument-spezifische Constraints.
|
||||
*
|
||||
* ~1500 Tokens
|
||||
*/
|
||||
projectForDraft(
|
||||
state: SDKState,
|
||||
documentType: ScopeDocumentType
|
||||
): DraftContext {
|
||||
const decision = state.complianceScope?.decision ?? null
|
||||
const level = decision?.determinedLevel ?? 'L1'
|
||||
const depthReq = DOC_MATRIX[documentType]?.[level] ?? {
|
||||
required: false,
|
||||
depth: 'Basis',
|
||||
detailItems: [],
|
||||
estimatedEffort: 'N/A',
|
||||
}
|
||||
|
||||
return {
|
||||
decisions: {
|
||||
level,
|
||||
scores: decision?.scores ?? {
|
||||
risk_score: 0,
|
||||
complexity_score: 0,
|
||||
assurance_need: 0,
|
||||
composite_score: 0,
|
||||
},
|
||||
hardTriggers: (decision?.triggeredHardTriggers ?? []).map(t => ({
|
||||
id: t.rule.id,
|
||||
label: t.rule.label,
|
||||
legalReference: t.rule.legalReference,
|
||||
})),
|
||||
requiredDocuments: (decision?.requiredDocuments ?? [])
|
||||
.filter(d => d.required)
|
||||
.map(d => ({
|
||||
documentType: d.documentType,
|
||||
depth: d.depth,
|
||||
detailItems: d.detailItems,
|
||||
})),
|
||||
},
|
||||
companyProfile: this.projectCompanyProfile(state.companyProfile),
|
||||
constraints: {
|
||||
depthRequirements: depthReq,
|
||||
riskFlags: (decision?.riskFlags ?? []).map(f => ({
|
||||
severity: f.severity,
|
||||
title: f.title,
|
||||
recommendation: f.recommendation,
|
||||
})),
|
||||
boundaries: this.deriveBoundaries(decision, documentType),
|
||||
},
|
||||
existingDocumentData: this.extractExistingDocumentData(state, documentType),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Projiziert den SDKState fuer Ask-Operationen.
|
||||
* Fokus: Luecken, unbeantwortete Fragen, fehlende Dokumente.
|
||||
*
|
||||
* ~600 Tokens
|
||||
*/
|
||||
projectForAsk(state: SDKState): GapContext {
|
||||
const decision = state.complianceScope?.decision ?? null
|
||||
|
||||
// Fehlende Pflichtdokumente ermitteln
|
||||
const requiredDocs = (decision?.requiredDocuments ?? []).filter(d => d.required)
|
||||
const existingDocTypes = this.getExistingDocumentTypes(state)
|
||||
const missingDocuments = requiredDocs
|
||||
.filter(d => !existingDocTypes.includes(d.documentType))
|
||||
.map(d => ({
|
||||
documentType: d.documentType,
|
||||
label: DOCUMENT_TYPE_LABELS[d.documentType] ?? d.documentType,
|
||||
depth: d.depth,
|
||||
estimatedEffort: d.estimatedEffort,
|
||||
}))
|
||||
|
||||
// Gaps aus der Scope-Decision
|
||||
const gaps = (decision?.gaps ?? []).map(g => ({
|
||||
id: g.id,
|
||||
severity: g.severity,
|
||||
title: g.title,
|
||||
description: g.description,
|
||||
relatedDocuments: g.relatedDocuments,
|
||||
}))
|
||||
|
||||
// Unbeantwortete Fragen (aus dem Scope-Profiling)
|
||||
const answers = state.complianceScope?.answers ?? []
|
||||
const answeredIds = new Set(answers.map(a => a.questionId))
|
||||
|
||||
return {
|
||||
unansweredQuestions: [], // Populated dynamically from question catalog
|
||||
gaps,
|
||||
missingDocuments,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Projiziert den SDKState fuer Validate-Operationen.
|
||||
* Fokus: Cross-Dokument-Konsistenz, Scope-Compliance.
|
||||
*
|
||||
* ~2000 Tokens
|
||||
*/
|
||||
projectForValidate(
|
||||
state: SDKState,
|
||||
documentTypes: ScopeDocumentType[]
|
||||
): ValidationContext {
|
||||
const decision = state.complianceScope?.decision ?? null
|
||||
const level = decision?.determinedLevel ?? 'L1'
|
||||
|
||||
// Dokument-Zusammenfassungen sammeln
|
||||
const documents = documentTypes.map(type => ({
|
||||
type,
|
||||
contentSummary: this.summarizeDocument(state, type),
|
||||
structuredData: this.extractExistingDocumentData(state, type),
|
||||
}))
|
||||
|
||||
// Cross-Referenzen extrahieren
|
||||
const crossReferences = {
|
||||
vvtCategories: (state.vvt ?? []).map(v =>
|
||||
typeof v === 'object' && v !== null && 'name' in v ? String((v as Record<string, unknown>).name) : ''
|
||||
).filter(Boolean),
|
||||
dsfaRisks: state.dsfa
|
||||
? ['DSFA vorhanden']
|
||||
: [],
|
||||
tomControls: (state.toms ?? []).map(t =>
|
||||
typeof t === 'object' && t !== null && 'name' in t ? String((t as Record<string, unknown>).name) : ''
|
||||
).filter(Boolean),
|
||||
retentionCategories: (state.retentionPolicies ?? []).map(p =>
|
||||
typeof p === 'object' && p !== null && 'name' in p ? String((p as Record<string, unknown>).name) : ''
|
||||
).filter(Boolean),
|
||||
}
|
||||
|
||||
// Depth-Requirements fuer alle angefragten Typen
|
||||
const depthRequirements: Record<string, DocumentDepthRequirement> = {}
|
||||
for (const type of documentTypes) {
|
||||
depthRequirements[type] = DOC_MATRIX[type]?.[level] ?? {
|
||||
required: false,
|
||||
depth: 'Basis',
|
||||
detailItems: [],
|
||||
estimatedEffort: 'N/A',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
documents,
|
||||
crossReferences,
|
||||
scopeLevel: level,
|
||||
depthRequirements: depthRequirements as Record<ScopeDocumentType, DocumentDepthRequirement>,
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Private Helpers
|
||||
// ==========================================================================
|
||||
|
||||
private projectCompanyProfile(
|
||||
profile: CompanyProfile | null
|
||||
): DraftContext['companyProfile'] {
|
||||
if (!profile) {
|
||||
return {
|
||||
name: 'Unbekannt',
|
||||
industry: 'Unbekannt',
|
||||
employeeCount: 0,
|
||||
businessModel: 'Unbekannt',
|
||||
isPublicSector: false,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: profile.companyName ?? profile.name ?? 'Unbekannt',
|
||||
industry: profile.industry ?? 'Unbekannt',
|
||||
employeeCount: typeof profile.employeeCount === 'number'
|
||||
? profile.employeeCount
|
||||
: parseInt(String(profile.employeeCount ?? '0'), 10) || 0,
|
||||
businessModel: profile.businessModel ?? 'Unbekannt',
|
||||
isPublicSector: profile.isPublicSector ?? false,
|
||||
...(profile.dataProtectionOfficer ? {
|
||||
dataProtectionOfficer: {
|
||||
name: profile.dataProtectionOfficer.name ?? '',
|
||||
email: profile.dataProtectionOfficer.email ?? '',
|
||||
},
|
||||
} : {}),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Leitet Grenzen (Boundaries) ab, die der Agent nicht ueberschreiten darf.
|
||||
*/
|
||||
private deriveBoundaries(
|
||||
decision: ScopeDecision | null,
|
||||
documentType: ScopeDocumentType
|
||||
): string[] {
|
||||
const boundaries: string[] = []
|
||||
const level = decision?.determinedLevel ?? 'L1'
|
||||
|
||||
// Grundregel: Scope-Engine ist autoritativ
|
||||
boundaries.push(
|
||||
`Maximale Dokumenttiefe: ${level} (${DOC_MATRIX[documentType]?.[level]?.depth ?? 'Basis'})`
|
||||
)
|
||||
|
||||
// DSFA-Boundary
|
||||
if (documentType === 'dsfa') {
|
||||
const dsfaRequired = decision?.triggeredHardTriggers?.some(
|
||||
t => t.rule.dsfaRequired
|
||||
) ?? false
|
||||
if (!dsfaRequired && level !== 'L4') {
|
||||
boundaries.push('DSFA ist laut Scope-Engine NICHT erforderlich. Nur auf expliziten Wunsch erstellen.')
|
||||
}
|
||||
}
|
||||
|
||||
// Dokument nicht in requiredDocuments?
|
||||
const isRequired = decision?.requiredDocuments?.some(
|
||||
d => d.documentType === documentType && d.required
|
||||
) ?? false
|
||||
if (!isRequired) {
|
||||
boundaries.push(
|
||||
`Dokument "${DOCUMENT_TYPE_LABELS[documentType] ?? documentType}" ist auf Level ${level} nicht als Pflicht eingestuft.`
|
||||
)
|
||||
}
|
||||
|
||||
return boundaries
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrahiert bereits vorhandene Dokumentdaten aus dem SDK-State.
|
||||
*/
|
||||
private extractExistingDocumentData(
|
||||
state: SDKState,
|
||||
documentType: ScopeDocumentType
|
||||
): Record<string, unknown> | undefined {
|
||||
switch (documentType) {
|
||||
case 'vvt':
|
||||
return state.vvt?.length ? { entries: state.vvt.slice(0, 5), totalCount: state.vvt.length } : undefined
|
||||
case 'tom':
|
||||
return state.toms?.length ? { entries: state.toms.slice(0, 5), totalCount: state.toms.length } : undefined
|
||||
case 'lf':
|
||||
return state.retentionPolicies?.length
|
||||
? { entries: state.retentionPolicies.slice(0, 5), totalCount: state.retentionPolicies.length }
|
||||
: undefined
|
||||
case 'dsfa':
|
||||
return state.dsfa ? { assessment: state.dsfa } : undefined
|
||||
case 'dsi':
|
||||
return state.documents?.length
|
||||
? { entries: state.documents.slice(0, 3), totalCount: state.documents.length }
|
||||
: undefined
|
||||
case 'einwilligung':
|
||||
return state.consents?.length
|
||||
? { entries: state.consents.slice(0, 5), totalCount: state.consents.length }
|
||||
: undefined
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ermittelt welche Dokumenttypen bereits im State vorhanden sind.
|
||||
*/
|
||||
private getExistingDocumentTypes(state: SDKState): ScopeDocumentType[] {
|
||||
const types: ScopeDocumentType[] = []
|
||||
if (state.vvt?.length) types.push('vvt')
|
||||
if (state.toms?.length) types.push('tom')
|
||||
if (state.retentionPolicies?.length) types.push('lf')
|
||||
if (state.dsfa) types.push('dsfa')
|
||||
if (state.documents?.length) types.push('dsi')
|
||||
if (state.consents?.length) types.push('einwilligung')
|
||||
if (state.cookieBanner) types.push('einwilligung')
|
||||
return types
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt eine kurze Zusammenfassung eines Dokuments fuer Validierung.
|
||||
*/
|
||||
private summarizeDocument(
|
||||
state: SDKState,
|
||||
documentType: ScopeDocumentType
|
||||
): string {
|
||||
switch (documentType) {
|
||||
case 'vvt':
|
||||
return state.vvt?.length
|
||||
? `${state.vvt.length} Verarbeitungstaetigkeiten erfasst`
|
||||
: 'Keine VVT-Eintraege vorhanden'
|
||||
case 'tom':
|
||||
return state.toms?.length
|
||||
? `${state.toms.length} TOM-Massnahmen definiert`
|
||||
: 'Keine TOM-Massnahmen vorhanden'
|
||||
case 'lf':
|
||||
return state.retentionPolicies?.length
|
||||
? `${state.retentionPolicies.length} Loeschfristen definiert`
|
||||
: 'Keine Loeschfristen vorhanden'
|
||||
case 'dsfa':
|
||||
return state.dsfa
|
||||
? 'DSFA vorhanden'
|
||||
: 'Keine DSFA vorhanden'
|
||||
default:
|
||||
return `Dokument ${DOCUMENT_TYPE_LABELS[documentType] ?? documentType}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Singleton-Instanz */
|
||||
export const stateProjector = new StateProjector()
|
||||
279
admin-v2/lib/sdk/drafting-engine/types.ts
Normal file
279
admin-v2/lib/sdk/drafting-engine/types.ts
Normal file
@@ -0,0 +1,279 @@
|
||||
/**
|
||||
* Drafting Engine - Type Definitions
|
||||
*
|
||||
* Typen fuer die 4 Agent-Rollen: Explain, Ask, Draft, Validate
|
||||
* Die Drafting Engine erweitert den Compliance Advisor um aktive Dokumententwurfs-
|
||||
* und Validierungsfaehigkeiten, stets unter Beachtung der deterministischen Scope-Engine.
|
||||
*/
|
||||
|
||||
import type {
|
||||
ComplianceDepthLevel,
|
||||
ComplianceScores,
|
||||
ScopeDecision,
|
||||
ScopeDocumentType,
|
||||
ScopeGap,
|
||||
RequiredDocument,
|
||||
RiskFlag,
|
||||
DocumentDepthRequirement,
|
||||
ScopeProfilingQuestion,
|
||||
} from '../compliance-scope-types'
|
||||
import type { CompanyProfile } from '../types'
|
||||
|
||||
// ============================================================================
|
||||
// Agent Mode
|
||||
// ============================================================================
|
||||
|
||||
/** Die 4 Agent-Rollen */
|
||||
export type AgentMode = 'explain' | 'ask' | 'draft' | 'validate'
|
||||
|
||||
/** Confidence-Score fuer Intent-Erkennung */
|
||||
export interface IntentClassification {
|
||||
mode: AgentMode
|
||||
confidence: number
|
||||
matchedPatterns: string[]
|
||||
/** Falls Draft oder Validate: erkannter Dokumenttyp */
|
||||
detectedDocumentType?: ScopeDocumentType
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Draft Context (fuer Draft-Mode)
|
||||
// ============================================================================
|
||||
|
||||
/** Projizierter State fuer Draft-Operationen (~1500 Tokens) */
|
||||
export interface DraftContext {
|
||||
/** Scope-Entscheidung (Level, Scores, Hard Triggers) */
|
||||
decisions: {
|
||||
level: ComplianceDepthLevel
|
||||
scores: ComplianceScores
|
||||
hardTriggers: Array<{ id: string; label: string; legalReference: string }>
|
||||
requiredDocuments: Array<{
|
||||
documentType: ScopeDocumentType
|
||||
depth: string
|
||||
detailItems: string[]
|
||||
}>
|
||||
}
|
||||
/** Firmenprofil-Auszug */
|
||||
companyProfile: {
|
||||
name: string
|
||||
industry: string
|
||||
employeeCount: number
|
||||
businessModel: string
|
||||
isPublicSector: boolean
|
||||
dataProtectionOfficer?: { name: string; email: string }
|
||||
}
|
||||
/** Constraints aus der Scope-Engine */
|
||||
constraints: {
|
||||
depthRequirements: DocumentDepthRequirement
|
||||
riskFlags: Array<{ severity: string; title: string; recommendation: string }>
|
||||
boundaries: string[]
|
||||
}
|
||||
/** Optional: bestehende Dokumentdaten aus dem SDK-State */
|
||||
existingDocumentData?: Record<string, unknown>
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Gap Context (fuer Ask-Mode)
|
||||
// ============================================================================
|
||||
|
||||
/** Projizierter State fuer Ask-Operationen (~600 Tokens) */
|
||||
export interface GapContext {
|
||||
/** Noch unbeantwortete Fragen aus dem Scope-Profiling */
|
||||
unansweredQuestions: Array<{
|
||||
id: string
|
||||
question: string
|
||||
type: string
|
||||
blockId: string
|
||||
}>
|
||||
/** Identifizierte Luecken */
|
||||
gaps: Array<{
|
||||
id: string
|
||||
severity: string
|
||||
title: string
|
||||
description: string
|
||||
relatedDocuments: ScopeDocumentType[]
|
||||
}>
|
||||
/** Fehlende Pflichtdokumente */
|
||||
missingDocuments: Array<{
|
||||
documentType: ScopeDocumentType
|
||||
label: string
|
||||
depth: string
|
||||
estimatedEffort: string
|
||||
}>
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Validation Context (fuer Validate-Mode)
|
||||
// ============================================================================
|
||||
|
||||
/** Projizierter State fuer Validate-Operationen (~2000 Tokens) */
|
||||
export interface ValidationContext {
|
||||
/** Zu validierende Dokumente */
|
||||
documents: Array<{
|
||||
type: ScopeDocumentType
|
||||
/** Zusammenfassung/Auszug des Inhalts */
|
||||
contentSummary: string
|
||||
/** Strukturierte Daten falls vorhanden */
|
||||
structuredData?: Record<string, unknown>
|
||||
}>
|
||||
/** Cross-Referenzen zwischen Dokumenten */
|
||||
crossReferences: {
|
||||
/** VVT Kategorien (Verarbeitungstaetigkeiten) */
|
||||
vvtCategories: string[]
|
||||
/** DSFA Risiken */
|
||||
dsfaRisks: string[]
|
||||
/** TOM Controls */
|
||||
tomControls: string[]
|
||||
/** Loeschfristen-Kategorien */
|
||||
retentionCategories: string[]
|
||||
}
|
||||
/** Scope-Level fuer Tiefenpruefung */
|
||||
scopeLevel: ComplianceDepthLevel
|
||||
/** Relevante Depth-Requirements */
|
||||
depthRequirements: Record<ScopeDocumentType, DocumentDepthRequirement>
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Validation Result
|
||||
// ============================================================================
|
||||
|
||||
export type ValidationSeverity = 'error' | 'warning' | 'suggestion'
|
||||
|
||||
export interface ValidationFinding {
|
||||
id: string
|
||||
severity: ValidationSeverity
|
||||
category: 'scope_violation' | 'inconsistency' | 'missing_content' | 'depth_mismatch' | 'cross_reference'
|
||||
title: string
|
||||
description: string
|
||||
/** Betroffenes Dokument */
|
||||
documentType: ScopeDocumentType
|
||||
/** Optional: Referenz zu anderem Dokument */
|
||||
crossReferenceType?: ScopeDocumentType
|
||||
/** Rechtsgrundlage falls relevant */
|
||||
legalReference?: string
|
||||
/** Vorschlag zur Behebung */
|
||||
suggestion?: string
|
||||
/** Kann automatisch uebernommen werden */
|
||||
autoFixable?: boolean
|
||||
}
|
||||
|
||||
export interface ValidationResult {
|
||||
passed: boolean
|
||||
timestamp: string
|
||||
scopeLevel: ComplianceDepthLevel
|
||||
errors: ValidationFinding[]
|
||||
warnings: ValidationFinding[]
|
||||
suggestions: ValidationFinding[]
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Draft Session
|
||||
// ============================================================================
|
||||
|
||||
export interface DraftRevision {
|
||||
id: string
|
||||
content: string
|
||||
sections: DraftSection[]
|
||||
createdAt: string
|
||||
instruction?: string
|
||||
}
|
||||
|
||||
export interface DraftSection {
|
||||
id: string
|
||||
title: string
|
||||
content: string
|
||||
/** Mapping zum Dokumentschema (z.B. VVT-Feld) */
|
||||
schemaField?: string
|
||||
}
|
||||
|
||||
export interface DraftSession {
|
||||
id: string
|
||||
mode: AgentMode
|
||||
documentType: ScopeDocumentType
|
||||
/** Aktueller Draft-Inhalt */
|
||||
currentDraft: DraftRevision | null
|
||||
/** Alle bisherigen Revisionen */
|
||||
revisions: DraftRevision[]
|
||||
/** Validierungszustand */
|
||||
validationState: ValidationResult | null
|
||||
/** Constraint-Check Ergebnis */
|
||||
constraintCheck: ConstraintCheckResult | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Constraint Check (Hard Gate)
|
||||
// ============================================================================
|
||||
|
||||
export interface ConstraintCheckResult {
|
||||
/** Darf der Draft erstellt werden? */
|
||||
allowed: boolean
|
||||
/** Verletzungen die den Draft blockieren */
|
||||
violations: string[]
|
||||
/** Anpassungen die vorgenommen werden sollten */
|
||||
adjustments: string[]
|
||||
/** Gepruefte Regeln */
|
||||
checkedRules: string[]
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Chat / API Types
|
||||
// ============================================================================
|
||||
|
||||
export interface DraftingChatMessage {
|
||||
role: 'user' | 'assistant' | 'system'
|
||||
content: string
|
||||
/** Metadata fuer Agent-Nachrichten */
|
||||
metadata?: {
|
||||
mode: AgentMode
|
||||
documentType?: ScopeDocumentType
|
||||
hasDraft?: boolean
|
||||
hasValidation?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface DraftingChatRequest {
|
||||
message: string
|
||||
history: DraftingChatMessage[]
|
||||
sdkStateProjection: DraftContext | GapContext | ValidationContext
|
||||
mode?: AgentMode
|
||||
documentType?: ScopeDocumentType
|
||||
}
|
||||
|
||||
export interface DraftRequest {
|
||||
documentType: ScopeDocumentType
|
||||
draftContext: DraftContext
|
||||
instructions?: string
|
||||
existingDraft?: DraftRevision
|
||||
}
|
||||
|
||||
export interface DraftResponse {
|
||||
draft: DraftRevision
|
||||
constraintCheck: ConstraintCheckResult
|
||||
tokensUsed: number
|
||||
}
|
||||
|
||||
export interface ValidateRequest {
|
||||
documentType: ScopeDocumentType
|
||||
draftContent: string
|
||||
validationContext: ValidationContext
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Feature Flag
|
||||
// ============================================================================
|
||||
|
||||
export interface DraftingEngineConfig {
|
||||
/** Feature-Flag: Drafting Engine aktiviert */
|
||||
enableDraftingEngine: boolean
|
||||
/** Verfuegbare Modi (fuer schrittweises Rollout) */
|
||||
enabledModes: AgentMode[]
|
||||
/** Max Token-Budget fuer State-Projection */
|
||||
maxProjectionTokens: number
|
||||
}
|
||||
|
||||
export const DEFAULT_DRAFTING_ENGINE_CONFIG: DraftingEngineConfig = {
|
||||
enableDraftingEngine: false,
|
||||
enabledModes: ['explain'],
|
||||
maxProjectionTokens: 4096,
|
||||
}
|
||||
343
admin-v2/lib/sdk/drafting-engine/use-drafting-engine.ts
Normal file
343
admin-v2/lib/sdk/drafting-engine/use-drafting-engine.ts
Normal file
@@ -0,0 +1,343 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* useDraftingEngine - React Hook fuer die Drafting Engine
|
||||
*
|
||||
* Managed: currentMode, activeDocumentType, draftSessions, validationState
|
||||
* Handled: State-Projection, API-Calls, Streaming
|
||||
* Provides: sendMessage(), requestDraft(), validateDraft(), acceptDraft()
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useRef } from 'react'
|
||||
import { useSDK } from '../context'
|
||||
import { stateProjector } from './state-projector'
|
||||
import { intentClassifier } from './intent-classifier'
|
||||
import { constraintEnforcer } from './constraint-enforcer'
|
||||
import type {
|
||||
AgentMode,
|
||||
DraftSession,
|
||||
DraftRevision,
|
||||
DraftingChatMessage,
|
||||
ValidationResult,
|
||||
ConstraintCheckResult,
|
||||
DraftContext,
|
||||
GapContext,
|
||||
ValidationContext,
|
||||
} from './types'
|
||||
import type { ScopeDocumentType } from '../compliance-scope-types'
|
||||
|
||||
export interface DraftingEngineState {
|
||||
currentMode: AgentMode
|
||||
activeDocumentType: ScopeDocumentType | null
|
||||
messages: DraftingChatMessage[]
|
||||
isTyping: boolean
|
||||
currentDraft: DraftRevision | null
|
||||
validationResult: ValidationResult | null
|
||||
constraintCheck: ConstraintCheckResult | null
|
||||
error: string | null
|
||||
}
|
||||
|
||||
export interface DraftingEngineActions {
|
||||
setMode: (mode: AgentMode) => void
|
||||
setDocumentType: (type: ScopeDocumentType) => void
|
||||
sendMessage: (content: string) => Promise<void>
|
||||
requestDraft: (instructions?: string) => Promise<void>
|
||||
validateDraft: () => Promise<void>
|
||||
acceptDraft: () => void
|
||||
stopGeneration: () => void
|
||||
clearMessages: () => void
|
||||
}
|
||||
|
||||
export function useDraftingEngine(): DraftingEngineState & DraftingEngineActions {
|
||||
const { state, dispatch } = useSDK()
|
||||
const abortControllerRef = useRef<AbortController | null>(null)
|
||||
|
||||
const [currentMode, setCurrentMode] = useState<AgentMode>('explain')
|
||||
const [activeDocumentType, setActiveDocumentType] = useState<ScopeDocumentType | null>(null)
|
||||
const [messages, setMessages] = useState<DraftingChatMessage[]>([])
|
||||
const [isTyping, setIsTyping] = useState(false)
|
||||
const [currentDraft, setCurrentDraft] = useState<DraftRevision | null>(null)
|
||||
const [validationResult, setValidationResult] = useState<ValidationResult | null>(null)
|
||||
const [constraintCheck, setConstraintCheck] = useState<ConstraintCheckResult | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
// Get state projection based on mode
|
||||
const getProjection = useCallback(() => {
|
||||
switch (currentMode) {
|
||||
case 'draft':
|
||||
return activeDocumentType
|
||||
? stateProjector.projectForDraft(state, activeDocumentType)
|
||||
: null
|
||||
case 'ask':
|
||||
return stateProjector.projectForAsk(state)
|
||||
case 'validate':
|
||||
return activeDocumentType
|
||||
? stateProjector.projectForValidate(state, [activeDocumentType])
|
||||
: stateProjector.projectForValidate(state, ['vvt', 'tom', 'lf'])
|
||||
default:
|
||||
return activeDocumentType
|
||||
? stateProjector.projectForDraft(state, activeDocumentType)
|
||||
: null
|
||||
}
|
||||
}, [state, currentMode, activeDocumentType])
|
||||
|
||||
const setMode = useCallback((mode: AgentMode) => {
|
||||
setCurrentMode(mode)
|
||||
}, [])
|
||||
|
||||
const setDocumentType = useCallback((type: ScopeDocumentType) => {
|
||||
setActiveDocumentType(type)
|
||||
}, [])
|
||||
|
||||
const sendMessage = useCallback(async (content: string) => {
|
||||
if (!content.trim() || isTyping) return
|
||||
setError(null)
|
||||
|
||||
// Auto-detect mode if needed
|
||||
const classification = intentClassifier.classify(content)
|
||||
if (classification.confidence > 0.7 && classification.mode !== currentMode) {
|
||||
setCurrentMode(classification.mode)
|
||||
}
|
||||
if (classification.detectedDocumentType && !activeDocumentType) {
|
||||
setActiveDocumentType(classification.detectedDocumentType)
|
||||
}
|
||||
|
||||
const userMessage: DraftingChatMessage = {
|
||||
role: 'user',
|
||||
content: content.trim(),
|
||||
}
|
||||
setMessages(prev => [...prev, userMessage])
|
||||
setIsTyping(true)
|
||||
|
||||
abortControllerRef.current = new AbortController()
|
||||
|
||||
try {
|
||||
const projection = getProjection()
|
||||
const response = await fetch('/api/sdk/drafting-engine/chat', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
message: content.trim(),
|
||||
history: messages.map(m => ({ role: m.role, content: m.content })),
|
||||
sdkStateProjection: projection,
|
||||
mode: currentMode,
|
||||
documentType: activeDocumentType,
|
||||
}),
|
||||
signal: abortControllerRef.current.signal,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ error: 'Unbekannter Fehler' }))
|
||||
throw new Error(errorData.error || `Server-Fehler (${response.status})`)
|
||||
}
|
||||
|
||||
const agentMessageId = `msg-${Date.now()}-agent`
|
||||
setMessages(prev => [...prev, {
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
metadata: { mode: currentMode, documentType: activeDocumentType ?? undefined },
|
||||
}])
|
||||
|
||||
// Stream response
|
||||
const reader = response.body!.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
let accumulated = ''
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
accumulated += decoder.decode(value, { stream: true })
|
||||
const text = accumulated
|
||||
setMessages(prev =>
|
||||
prev.map((m, i) => i === prev.length - 1 ? { ...m, content: text } : m)
|
||||
)
|
||||
}
|
||||
|
||||
setIsTyping(false)
|
||||
} catch (err) {
|
||||
if ((err as Error).name === 'AbortError') {
|
||||
setIsTyping(false)
|
||||
return
|
||||
}
|
||||
setError((err as Error).message)
|
||||
setMessages(prev => [...prev, {
|
||||
role: 'assistant',
|
||||
content: `Fehler: ${(err as Error).message}`,
|
||||
}])
|
||||
setIsTyping(false)
|
||||
}
|
||||
}, [isTyping, messages, currentMode, activeDocumentType, getProjection])
|
||||
|
||||
const requestDraft = useCallback(async (instructions?: string) => {
|
||||
if (!activeDocumentType) {
|
||||
setError('Bitte waehlen Sie zuerst einen Dokumenttyp.')
|
||||
return
|
||||
}
|
||||
setError(null)
|
||||
setIsTyping(true)
|
||||
|
||||
try {
|
||||
const draftContext = stateProjector.projectForDraft(state, activeDocumentType)
|
||||
|
||||
const response = await fetch('/api/sdk/drafting-engine/draft', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
documentType: activeDocumentType,
|
||||
draftContext,
|
||||
instructions,
|
||||
existingDraft: currentDraft,
|
||||
}),
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(result.error || 'Draft-Generierung fehlgeschlagen')
|
||||
}
|
||||
|
||||
setCurrentDraft(result.draft)
|
||||
setConstraintCheck(result.constraintCheck)
|
||||
|
||||
setMessages(prev => [...prev, {
|
||||
role: 'assistant',
|
||||
content: `Draft fuer ${activeDocumentType} erstellt (${result.draft.sections.length} Sections). Oeffnen Sie den Editor zur Bearbeitung.`,
|
||||
metadata: { mode: 'draft', documentType: activeDocumentType, hasDraft: true },
|
||||
}])
|
||||
|
||||
setIsTyping(false)
|
||||
} catch (err) {
|
||||
setError((err as Error).message)
|
||||
setIsTyping(false)
|
||||
}
|
||||
}, [activeDocumentType, state, currentDraft])
|
||||
|
||||
const validateDraft = useCallback(async () => {
|
||||
setError(null)
|
||||
setIsTyping(true)
|
||||
|
||||
try {
|
||||
const docTypes: ScopeDocumentType[] = activeDocumentType
|
||||
? [activeDocumentType]
|
||||
: ['vvt', 'tom', 'lf']
|
||||
const validationContext = stateProjector.projectForValidate(state, docTypes)
|
||||
|
||||
const response = await fetch('/api/sdk/drafting-engine/validate', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
documentType: activeDocumentType || 'vvt',
|
||||
draftContent: currentDraft?.content || '',
|
||||
validationContext,
|
||||
}),
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(result.error || 'Validierung fehlgeschlagen')
|
||||
}
|
||||
|
||||
setValidationResult(result)
|
||||
|
||||
const summary = result.passed
|
||||
? `Validierung bestanden. ${result.warnings.length} Warnungen, ${result.suggestions.length} Vorschlaege.`
|
||||
: `Validierung fehlgeschlagen. ${result.errors.length} Fehler, ${result.warnings.length} Warnungen.`
|
||||
|
||||
setMessages(prev => [...prev, {
|
||||
role: 'assistant',
|
||||
content: summary,
|
||||
metadata: { mode: 'validate', hasValidation: true },
|
||||
}])
|
||||
|
||||
setIsTyping(false)
|
||||
} catch (err) {
|
||||
setError((err as Error).message)
|
||||
setIsTyping(false)
|
||||
}
|
||||
}, [activeDocumentType, state, currentDraft])
|
||||
|
||||
const acceptDraft = useCallback(() => {
|
||||
if (!currentDraft || !activeDocumentType) return
|
||||
|
||||
// Dispatch the draft data into SDK state
|
||||
switch (activeDocumentType) {
|
||||
case 'vvt':
|
||||
dispatch({
|
||||
type: 'ADD_PROCESSING_ACTIVITY',
|
||||
payload: {
|
||||
id: `draft-vvt-${Date.now()}`,
|
||||
name: currentDraft.sections.find(s => s.schemaField === 'name')?.content || 'Neuer VVT-Eintrag',
|
||||
...Object.fromEntries(
|
||||
currentDraft.sections
|
||||
.filter(s => s.schemaField)
|
||||
.map(s => [s.schemaField!, s.content])
|
||||
),
|
||||
},
|
||||
})
|
||||
break
|
||||
case 'tom':
|
||||
dispatch({
|
||||
type: 'ADD_TOM',
|
||||
payload: {
|
||||
id: `draft-tom-${Date.now()}`,
|
||||
name: 'TOM-Entwurf',
|
||||
...Object.fromEntries(
|
||||
currentDraft.sections
|
||||
.filter(s => s.schemaField)
|
||||
.map(s => [s.schemaField!, s.content])
|
||||
),
|
||||
},
|
||||
})
|
||||
break
|
||||
default:
|
||||
dispatch({
|
||||
type: 'ADD_DOCUMENT',
|
||||
payload: {
|
||||
id: `draft-${activeDocumentType}-${Date.now()}`,
|
||||
type: activeDocumentType,
|
||||
content: currentDraft.content,
|
||||
sections: currentDraft.sections,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
setMessages(prev => [...prev, {
|
||||
role: 'assistant',
|
||||
content: `Draft wurde in den SDK-State uebernommen.`,
|
||||
}])
|
||||
setCurrentDraft(null)
|
||||
}, [currentDraft, activeDocumentType, dispatch])
|
||||
|
||||
const stopGeneration = useCallback(() => {
|
||||
abortControllerRef.current?.abort()
|
||||
setIsTyping(false)
|
||||
}, [])
|
||||
|
||||
const clearMessages = useCallback(() => {
|
||||
setMessages([])
|
||||
setCurrentDraft(null)
|
||||
setValidationResult(null)
|
||||
setConstraintCheck(null)
|
||||
setError(null)
|
||||
}, [])
|
||||
|
||||
return {
|
||||
currentMode,
|
||||
activeDocumentType,
|
||||
messages,
|
||||
isTyping,
|
||||
currentDraft,
|
||||
validationResult,
|
||||
constraintCheck,
|
||||
error,
|
||||
setMode,
|
||||
setDocumentType,
|
||||
sendMessage,
|
||||
requestDraft,
|
||||
validateDraft,
|
||||
acceptDraft,
|
||||
stopGeneration,
|
||||
clearMessages,
|
||||
}
|
||||
}
|
||||
845
admin-v2/lib/sdk/incidents/api.ts
Normal file
845
admin-v2/lib/sdk/incidents/api.ts
Normal file
@@ -0,0 +1,845 @@
|
||||
/**
|
||||
* Incident/Breach Management API Client
|
||||
*
|
||||
* API client for DSGVO Art. 33/34 Incident & Data Breach Management
|
||||
* Connects via Next.js proxy to the ai-compliance-sdk backend
|
||||
*/
|
||||
|
||||
import {
|
||||
Incident,
|
||||
IncidentListResponse,
|
||||
IncidentFilters,
|
||||
IncidentCreateRequest,
|
||||
IncidentUpdateRequest,
|
||||
IncidentStatistics,
|
||||
IncidentMeasure,
|
||||
TimelineEntry,
|
||||
RiskAssessmentRequest,
|
||||
RiskAssessment,
|
||||
AuthorityNotification,
|
||||
DataSubjectNotification,
|
||||
IncidentSeverity,
|
||||
IncidentStatus,
|
||||
IncidentCategory,
|
||||
calculateRiskLevel,
|
||||
isNotificationRequired,
|
||||
get72hDeadline
|
||||
} from './types'
|
||||
|
||||
// =============================================================================
|
||||
// CONFIGURATION
|
||||
// =============================================================================
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
447
admin-v2/lib/sdk/incidents/types.ts
Normal file
447
admin-v2/lib/sdk/incidents/types.ts
Normal file
@@ -0,0 +1,447 @@
|
||||
/**
|
||||
* Incident/Breach Management Types (Datenpannen-Management)
|
||||
*
|
||||
* TypeScript definitions for DSGVO Art. 33/34 Incident & Data Breach Management
|
||||
* 72-Stunden-Meldefrist an die Aufsichtsbehoerde
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// ENUMS & CONSTANTS
|
||||
// =============================================================================
|
||||
|
||||
export type IncidentSeverity = 'low' | 'medium' | 'high' | 'critical'
|
||||
|
||||
export type IncidentStatus =
|
||||
| 'detected' // Erkannt
|
||||
| 'assessment' // Bewertung laeuft
|
||||
| 'containment' // Eindaemmung
|
||||
| 'notification_required' // Meldepflichtig - Meldung steht aus
|
||||
| 'notification_sent' // Gemeldet an Aufsichtsbehoerde
|
||||
| 'remediation' // Behebung laeuft
|
||||
| 'closed' // Abgeschlossen
|
||||
|
||||
export type IncidentCategory =
|
||||
| 'data_breach' // Datenpanne / Datenschutzverletzung
|
||||
| 'unauthorized_access' // Unbefugter Zugriff
|
||||
| 'data_loss' // Datenverlust
|
||||
| 'system_compromise' // Systemkompromittierung
|
||||
| 'phishing' // Phishing-Angriff
|
||||
| 'ransomware' // Ransomware
|
||||
| 'insider_threat' // Insider-Bedrohung
|
||||
| 'physical_breach' // Physischer Sicherheitsvorfall
|
||||
| 'other' // Sonstiges
|
||||
|
||||
// =============================================================================
|
||||
// SEVERITY METADATA
|
||||
// =============================================================================
|
||||
|
||||
export interface IncidentSeverityInfo {
|
||||
label: string
|
||||
description: string
|
||||
color: string
|
||||
bgColor: string
|
||||
}
|
||||
|
||||
export const INCIDENT_SEVERITY_INFO: Record<IncidentSeverity, IncidentSeverityInfo> = {
|
||||
low: {
|
||||
label: 'Niedrig',
|
||||
description: 'Geringes Risiko fuer betroffene Personen, keine Meldepflicht erwartet',
|
||||
color: 'text-green-700',
|
||||
bgColor: 'bg-green-100'
|
||||
},
|
||||
medium: {
|
||||
label: 'Mittel',
|
||||
description: 'Moderates Risiko, Meldepflicht an Aufsichtsbehoerde moeglich',
|
||||
color: 'text-yellow-700',
|
||||
bgColor: 'bg-yellow-100'
|
||||
},
|
||||
high: {
|
||||
label: 'Hoch',
|
||||
description: 'Hohes Risiko, Meldepflicht an Aufsichtsbehoerde wahrscheinlich',
|
||||
color: 'text-orange-700',
|
||||
bgColor: 'bg-orange-100'
|
||||
},
|
||||
critical: {
|
||||
label: 'Kritisch',
|
||||
description: 'Sehr hohes Risiko, Meldepflicht an Aufsichtsbehoerde und Betroffene',
|
||||
color: 'text-red-700',
|
||||
bgColor: 'bg-red-100'
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// STATUS METADATA
|
||||
// =============================================================================
|
||||
|
||||
export interface IncidentStatusInfo {
|
||||
label: string
|
||||
description: string
|
||||
color: string
|
||||
bgColor: string
|
||||
}
|
||||
|
||||
export const INCIDENT_STATUS_INFO: Record<IncidentStatus, IncidentStatusInfo> = {
|
||||
detected: {
|
||||
label: 'Erkannt',
|
||||
description: 'Vorfall wurde erkannt und dokumentiert',
|
||||
color: 'text-blue-700',
|
||||
bgColor: 'bg-blue-100'
|
||||
},
|
||||
assessment: {
|
||||
label: 'Bewertung',
|
||||
description: 'Risikobewertung und Einschaetzung der Meldepflicht',
|
||||
color: 'text-yellow-700',
|
||||
bgColor: 'bg-yellow-100'
|
||||
},
|
||||
containment: {
|
||||
label: 'Eindaemmung',
|
||||
description: 'Sofortmassnahmen zur Eindaemmung werden durchgefuehrt',
|
||||
color: 'text-orange-700',
|
||||
bgColor: 'bg-orange-100'
|
||||
},
|
||||
notification_required: {
|
||||
label: 'Meldepflichtig',
|
||||
description: 'Meldung an Aufsichtsbehoerde erforderlich (Art. 33 DSGVO)',
|
||||
color: 'text-red-700',
|
||||
bgColor: 'bg-red-100'
|
||||
},
|
||||
notification_sent: {
|
||||
label: 'Gemeldet',
|
||||
description: 'Meldung an die Aufsichtsbehoerde wurde eingereicht',
|
||||
color: 'text-purple-700',
|
||||
bgColor: 'bg-purple-100'
|
||||
},
|
||||
remediation: {
|
||||
label: 'Behebung',
|
||||
description: 'Langfristige Behebungs- und Praeventionsmassnahmen',
|
||||
color: 'text-indigo-700',
|
||||
bgColor: 'bg-indigo-100'
|
||||
},
|
||||
closed: {
|
||||
label: 'Abgeschlossen',
|
||||
description: 'Vorfall vollstaendig bearbeitet und dokumentiert',
|
||||
color: 'text-green-700',
|
||||
bgColor: 'bg-green-100'
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CATEGORY METADATA
|
||||
// =============================================================================
|
||||
|
||||
export interface IncidentCategoryInfo {
|
||||
label: string
|
||||
description: string
|
||||
icon: string
|
||||
color: string
|
||||
bgColor: string
|
||||
}
|
||||
|
||||
export const INCIDENT_CATEGORY_INFO: Record<IncidentCategory, IncidentCategoryInfo> = {
|
||||
data_breach: {
|
||||
label: 'Datenpanne',
|
||||
description: 'Allgemeine Datenschutzverletzung mit Offenlegung personenbezogener Daten',
|
||||
icon: '\u{1F4C4}',
|
||||
color: 'text-red-700',
|
||||
bgColor: 'bg-red-100'
|
||||
},
|
||||
unauthorized_access: {
|
||||
label: 'Unbefugter Zugriff',
|
||||
description: 'Unberechtigter Zugriff auf Systeme oder Daten',
|
||||
icon: '\u{1F6AB}',
|
||||
color: 'text-red-700',
|
||||
bgColor: 'bg-red-100'
|
||||
},
|
||||
data_loss: {
|
||||
label: 'Datenverlust',
|
||||
description: 'Verlust von Daten durch technischen Fehler oder Versehen',
|
||||
icon: '\u{1F4BE}',
|
||||
color: 'text-orange-700',
|
||||
bgColor: 'bg-orange-100'
|
||||
},
|
||||
system_compromise: {
|
||||
label: 'Systemkompromittierung',
|
||||
description: 'System wurde durch Angreifer kompromittiert',
|
||||
icon: '\u{1F4BB}',
|
||||
color: 'text-red-700',
|
||||
bgColor: 'bg-red-100'
|
||||
},
|
||||
phishing: {
|
||||
label: 'Phishing-Angriff',
|
||||
description: 'Taeuschendes Abfangen von Zugangsdaten oder Daten',
|
||||
icon: '\u{1F3A3}',
|
||||
color: 'text-orange-700',
|
||||
bgColor: 'bg-orange-100'
|
||||
},
|
||||
ransomware: {
|
||||
label: 'Ransomware',
|
||||
description: 'Verschluesselung von Daten durch Schadsoftware',
|
||||
icon: '\u{1F512}',
|
||||
color: 'text-red-700',
|
||||
bgColor: 'bg-red-100'
|
||||
},
|
||||
insider_threat: {
|
||||
label: 'Insider-Bedrohung',
|
||||
description: 'Vorsaetzlicher oder fahrlaessiger Verstoss durch Mitarbeiter',
|
||||
icon: '\u{1F464}',
|
||||
color: 'text-purple-700',
|
||||
bgColor: 'bg-purple-100'
|
||||
},
|
||||
physical_breach: {
|
||||
label: 'Physischer Sicherheitsvorfall',
|
||||
description: 'Einbruch, Diebstahl von Geraeten oder physische Zugriffe',
|
||||
icon: '\u{1F3E2}',
|
||||
color: 'text-gray-700',
|
||||
bgColor: 'bg-gray-100'
|
||||
},
|
||||
other: {
|
||||
label: 'Sonstiges',
|
||||
description: 'Sonstiger Datenschutzvorfall',
|
||||
icon: '\u{2753}',
|
||||
color: 'text-gray-700',
|
||||
bgColor: 'bg-gray-100'
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MAIN INTERFACES
|
||||
// =============================================================================
|
||||
|
||||
export interface RiskAssessment {
|
||||
id: string
|
||||
assessedBy: string
|
||||
assessedAt: string
|
||||
likelihoodScore: number // 1-5 (1 = sehr unwahrscheinlich, 5 = sehr wahrscheinlich)
|
||||
impactScore: number // 1-5 (1 = gering, 5 = katastrophal)
|
||||
overallRisk: IncidentSeverity // Berechnetes Gesamtrisiko
|
||||
notificationRequired: boolean // Art. 33 Bewertung
|
||||
reasoning: string // Begruendung der Bewertung
|
||||
}
|
||||
|
||||
export interface AuthorityNotification {
|
||||
id: string
|
||||
authority: string // z.B. "LfD Niedersachsen"
|
||||
deadline72h: string // 72 Stunden nach Erkennung (Art. 33)
|
||||
submittedAt?: string
|
||||
status: 'pending' | 'submitted' | 'acknowledged'
|
||||
formData: Record<string, unknown>
|
||||
pdfUrl?: string
|
||||
}
|
||||
|
||||
export interface DataSubjectNotification {
|
||||
id: string
|
||||
notificationRequired: boolean // Art. 34 Bewertung
|
||||
templateText: string
|
||||
sentAt?: string
|
||||
recipientCount: number
|
||||
method: 'email' | 'letter' | 'portal' | 'public'
|
||||
}
|
||||
|
||||
export interface IncidentMeasure {
|
||||
id: string
|
||||
incidentId: string
|
||||
title: string
|
||||
description: string
|
||||
type: 'immediate' | 'corrective' | 'preventive'
|
||||
status: 'planned' | 'in_progress' | 'completed'
|
||||
responsible: string
|
||||
dueDate: string
|
||||
completedAt?: string
|
||||
}
|
||||
|
||||
export interface TimelineEntry {
|
||||
id: string
|
||||
incidentId: string
|
||||
timestamp: string
|
||||
action: string
|
||||
description: string
|
||||
performedBy: string
|
||||
}
|
||||
|
||||
export interface Incident {
|
||||
id: string
|
||||
referenceNumber: string // z.B. "INC-2025-000001"
|
||||
title: string
|
||||
description: string
|
||||
category: IncidentCategory
|
||||
severity: IncidentSeverity
|
||||
status: IncidentStatus
|
||||
|
||||
// Erkennung
|
||||
detectedAt: string
|
||||
detectedBy: string
|
||||
|
||||
// Betroffene Systeme & Daten
|
||||
affectedSystems: string[]
|
||||
affectedDataCategories: string[]
|
||||
estimatedAffectedPersons: number
|
||||
|
||||
// Risikobewertung
|
||||
riskAssessment: RiskAssessment | null
|
||||
|
||||
// Meldungen
|
||||
authorityNotification: AuthorityNotification | null
|
||||
dataSubjectNotification: DataSubjectNotification | null
|
||||
|
||||
// Massnahmen & Verlauf
|
||||
measures: IncidentMeasure[]
|
||||
timeline: TimelineEntry[]
|
||||
|
||||
// Zuweisung
|
||||
assignedTo?: string
|
||||
|
||||
// Abschluss
|
||||
closedAt?: string
|
||||
lessonsLearned?: string
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// STATISTICS
|
||||
// =============================================================================
|
||||
|
||||
export interface IncidentStatistics {
|
||||
totalIncidents: number
|
||||
openIncidents: number
|
||||
notificationsPending: number
|
||||
averageResponseTimeHours: number
|
||||
bySeverity: Record<IncidentSeverity, number>
|
||||
byCategory: Record<IncidentCategory, number>
|
||||
byStatus: Record<IncidentStatus, number>
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// API TYPES
|
||||
// =============================================================================
|
||||
|
||||
export interface IncidentFilters {
|
||||
status?: IncidentStatus | IncidentStatus[]
|
||||
severity?: IncidentSeverity | IncidentSeverity[]
|
||||
category?: IncidentCategory | IncidentCategory[]
|
||||
assignedTo?: string
|
||||
overdue?: boolean
|
||||
search?: string
|
||||
dateFrom?: string
|
||||
dateTo?: string
|
||||
}
|
||||
|
||||
export interface IncidentListResponse {
|
||||
incidents: Incident[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
export interface IncidentCreateRequest {
|
||||
title: string
|
||||
description: string
|
||||
category: IncidentCategory
|
||||
severity: IncidentSeverity
|
||||
detectedAt: string
|
||||
detectedBy: string
|
||||
affectedSystems: string[]
|
||||
affectedDataCategories: string[]
|
||||
estimatedAffectedPersons: number
|
||||
assignedTo?: string
|
||||
}
|
||||
|
||||
export interface IncidentUpdateRequest {
|
||||
title?: string
|
||||
description?: string
|
||||
category?: IncidentCategory
|
||||
severity?: IncidentSeverity
|
||||
status?: IncidentStatus
|
||||
affectedSystems?: string[]
|
||||
affectedDataCategories?: string[]
|
||||
estimatedAffectedPersons?: number
|
||||
assignedTo?: string
|
||||
}
|
||||
|
||||
export interface RiskAssessmentRequest {
|
||||
likelihoodScore: number // 1-5
|
||||
impactScore: number // 1-5
|
||||
reasoning: string
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Berechnet die verbleibenden Stunden bis zur 72h-Meldefrist (Art. 33 DSGVO)
|
||||
*/
|
||||
export function getHoursUntil72hDeadline(detectedAt: string): number {
|
||||
const detected = new Date(detectedAt)
|
||||
const deadline = new Date(detected.getTime() + 72 * 60 * 60 * 1000)
|
||||
const now = new Date()
|
||||
const diff = deadline.getTime() - now.getTime()
|
||||
return Math.round(diff / (1000 * 60 * 60) * 10) / 10
|
||||
}
|
||||
|
||||
/**
|
||||
* Prueft ob die 72-Stunden-Meldefrist abgelaufen ist
|
||||
*/
|
||||
export function is72hDeadlineExpired(detectedAt: string): boolean {
|
||||
const detected = new Date(detectedAt)
|
||||
const deadline = new Date(detected.getTime() + 72 * 60 * 60 * 1000)
|
||||
return new Date() > deadline
|
||||
}
|
||||
|
||||
/**
|
||||
* Berechnet die Risikostufe basierend auf Eintrittswahrscheinlichkeit und Auswirkung
|
||||
* Risiko-Matrix:
|
||||
* likelihood x impact >= 20 -> critical
|
||||
* likelihood x impact >= 12 -> high
|
||||
* likelihood x impact >= 6 -> medium
|
||||
* sonst -> low
|
||||
*/
|
||||
export function calculateRiskLevel(likelihood: number, impact: number): IncidentSeverity {
|
||||
const riskScore = likelihood * impact
|
||||
if (riskScore >= 20) return 'critical'
|
||||
if (riskScore >= 12) return 'high'
|
||||
if (riskScore >= 6) return 'medium'
|
||||
return 'low'
|
||||
}
|
||||
|
||||
/**
|
||||
* Prueft ob eine Meldung an die Aufsichtsbehoerde erforderlich ist
|
||||
* Bei hohem oder kritischem Risiko ist eine Meldung gemaess Art. 33 DSGVO erforderlich
|
||||
*/
|
||||
export function isNotificationRequired(riskAssessment: RiskAssessment): boolean {
|
||||
return riskAssessment.overallRisk === 'high' || riskAssessment.overallRisk === 'critical'
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert eine Referenznummer fuer einen Vorfall
|
||||
*/
|
||||
export function generateIncidentReferenceNumber(year: number, sequence: number): string {
|
||||
return `INC-${year}-${String(sequence).padStart(6, '0')}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die 72h-Deadline als Date zurueck
|
||||
*/
|
||||
export function get72hDeadline(detectedAt: string): Date {
|
||||
const detected = new Date(detectedAt)
|
||||
return new Date(detected.getTime() + 72 * 60 * 60 * 1000)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die Severity-Info zurueck
|
||||
*/
|
||||
export function getSeverityInfo(severity: IncidentSeverity): IncidentSeverityInfo {
|
||||
return INCIDENT_SEVERITY_INFO[severity]
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die Status-Info zurueck
|
||||
*/
|
||||
export function getStatusInfo(status: IncidentStatus): IncidentStatusInfo {
|
||||
return INCIDENT_STATUS_INFO[status]
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die Kategorie-Info zurueck
|
||||
*/
|
||||
export function getCategoryInfo(category: IncidentCategory): IncidentCategoryInfo {
|
||||
return INCIDENT_CATEGORY_INFO[category]
|
||||
}
|
||||
@@ -693,6 +693,45 @@ export const SDK_STEPS: SDKStep[] = [
|
||||
prerequisiteSteps: ['consent-management'],
|
||||
isOptional: false,
|
||||
},
|
||||
{
|
||||
id: 'incidents',
|
||||
phase: 2,
|
||||
package: 'betrieb',
|
||||
order: 6,
|
||||
name: 'Incident Management',
|
||||
nameShort: 'Incidents',
|
||||
description: 'Datenpannen erfassen, bewerten und melden (Art. 33/34 DSGVO)',
|
||||
url: '/sdk/incidents',
|
||||
checkpointId: 'CP-INC',
|
||||
prerequisiteSteps: ['notfallplan'],
|
||||
isOptional: false,
|
||||
},
|
||||
{
|
||||
id: 'whistleblower',
|
||||
phase: 2,
|
||||
package: 'betrieb',
|
||||
order: 7,
|
||||
name: 'Hinweisgebersystem',
|
||||
nameShort: 'Whistleblower',
|
||||
description: 'Anonymes Meldesystem gemaess HinSchG',
|
||||
url: '/sdk/whistleblower',
|
||||
checkpointId: 'CP-WB',
|
||||
prerequisiteSteps: ['incidents'],
|
||||
isOptional: false,
|
||||
},
|
||||
{
|
||||
id: 'academy',
|
||||
phase: 2,
|
||||
package: 'betrieb',
|
||||
order: 8,
|
||||
name: 'Compliance Academy',
|
||||
nameShort: 'Academy',
|
||||
description: 'Mitarbeiter-Schulungen & Zertifikate',
|
||||
url: '/sdk/academy',
|
||||
checkpointId: 'CP-ACAD',
|
||||
prerequisiteSteps: ['whistleblower'],
|
||||
isOptional: false,
|
||||
},
|
||||
]
|
||||
|
||||
// =============================================================================
|
||||
|
||||
755
admin-v2/lib/sdk/whistleblower/api.ts
Normal file
755
admin-v2/lib/sdk/whistleblower/api.ts
Normal file
@@ -0,0 +1,755 @@
|
||||
/**
|
||||
* Whistleblower System API Client
|
||||
*
|
||||
* API client for Hinweisgeberschutzgesetz (HinSchG) compliant
|
||||
* Whistleblower/Hinweisgebersystem management
|
||||
* Connects to the ai-compliance-sdk backend
|
||||
*/
|
||||
|
||||
import {
|
||||
WhistleblowerReport,
|
||||
WhistleblowerStatistics,
|
||||
ReportListResponse,
|
||||
ReportFilters,
|
||||
PublicReportSubmission,
|
||||
ReportUpdateRequest,
|
||||
MessageSendRequest,
|
||||
AnonymousMessage,
|
||||
WhistleblowerMeasure,
|
||||
FileAttachment,
|
||||
ReportCategory,
|
||||
ReportStatus,
|
||||
ReportPriority,
|
||||
generateAccessKey
|
||||
} from './types'
|
||||
|
||||
// =============================================================================
|
||||
// CONFIGURATION
|
||||
// =============================================================================
|
||||
|
||||
const WB_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)
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ADMIN CRUD - Reports
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Alle Meldungen abrufen (Admin)
|
||||
*/
|
||||
export async function fetchReports(filters?: ReportFilters): Promise<ReportListResponse> {
|
||||
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.category) {
|
||||
const categories = Array.isArray(filters.category) ? filters.category : [filters.category]
|
||||
categories.forEach(c => params.append('category', c))
|
||||
}
|
||||
if (filters.priority) params.set('priority', filters.priority)
|
||||
if (filters.assignedTo) params.set('assignedTo', filters.assignedTo)
|
||||
if (filters.isAnonymous !== undefined) params.set('isAnonymous', String(filters.isAnonymous))
|
||||
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 = `${WB_API_BASE}/api/v1/admin/whistleblower/reports${queryString ? `?${queryString}` : ''}`
|
||||
|
||||
return fetchWithTimeout<ReportListResponse>(url)
|
||||
}
|
||||
|
||||
/**
|
||||
* Einzelne Meldung abrufen (Admin)
|
||||
*/
|
||||
export async function fetchReport(id: string): Promise<WhistleblowerReport> {
|
||||
return fetchWithTimeout<WhistleblowerReport>(
|
||||
`${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}`
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Meldung aktualisieren (Status, Prioritaet, Kategorie, Zuweisung)
|
||||
*/
|
||||
export async function updateReport(id: string, update: ReportUpdateRequest): Promise<WhistleblowerReport> {
|
||||
return fetchWithTimeout<WhistleblowerReport>(
|
||||
`${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(update)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Meldung loeschen (soft delete)
|
||||
*/
|
||||
export async function deleteReport(id: string): Promise<void> {
|
||||
await fetchWithTimeout<void>(
|
||||
`${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}`,
|
||||
{
|
||||
method: 'DELETE'
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PUBLIC ENDPOINTS - Kein Auth erforderlich
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Neue Meldung einreichen (oeffentlich, keine Auth)
|
||||
*/
|
||||
export async function submitPublicReport(
|
||||
data: PublicReportSubmission
|
||||
): Promise<{ report: WhistleblowerReport; accessKey: string }> {
|
||||
const response = await fetch(
|
||||
`${WB_API_BASE}/api/v1/public/whistleblower/submit`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||
}
|
||||
|
||||
return response.json()
|
||||
}
|
||||
|
||||
/**
|
||||
* Meldung ueber Zugangscode abrufen (oeffentlich, keine Auth)
|
||||
*/
|
||||
export async function fetchReportByAccessKey(
|
||||
accessKey: string
|
||||
): Promise<WhistleblowerReport> {
|
||||
const response = await fetch(
|
||||
`${WB_API_BASE}/api/v1/public/whistleblower/report/${accessKey}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||
}
|
||||
|
||||
return response.json()
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// WORKFLOW ACTIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Eingangsbestaetigung versenden (HinSchG ss 17 Abs. 1)
|
||||
*/
|
||||
export async function acknowledgeReport(id: string): Promise<WhistleblowerReport> {
|
||||
return fetchWithTimeout<WhistleblowerReport>(
|
||||
`${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}/acknowledge`,
|
||||
{
|
||||
method: 'POST'
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Untersuchung starten
|
||||
*/
|
||||
export async function startInvestigation(id: string): Promise<WhistleblowerReport> {
|
||||
return fetchWithTimeout<WhistleblowerReport>(
|
||||
`${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}/investigate`,
|
||||
{
|
||||
method: 'POST'
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Massnahme zu einer Meldung hinzufuegen
|
||||
*/
|
||||
export async function addMeasure(
|
||||
id: string,
|
||||
measure: Omit<WhistleblowerMeasure, 'id' | 'reportId' | 'completedAt'>
|
||||
): Promise<WhistleblowerMeasure> {
|
||||
return fetchWithTimeout<WhistleblowerMeasure>(
|
||||
`${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}/measures`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify(measure)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Meldung abschliessen mit Begruendung
|
||||
*/
|
||||
export async function closeReport(
|
||||
id: string,
|
||||
resolution: { reason: string; notes: string }
|
||||
): Promise<WhistleblowerReport> {
|
||||
return fetchWithTimeout<WhistleblowerReport>(
|
||||
`${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}/close`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify(resolution)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ANONYMOUS MESSAGING
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Nachricht im anonymen Kanal senden
|
||||
*/
|
||||
export async function sendMessage(
|
||||
reportId: string,
|
||||
message: string,
|
||||
role: 'reporter' | 'ombudsperson'
|
||||
): Promise<AnonymousMessage> {
|
||||
return fetchWithTimeout<AnonymousMessage>(
|
||||
`${WB_API_BASE}/api/v1/admin/whistleblower/reports/${reportId}/messages`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ senderRole: role, message })
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Nachrichten fuer eine Meldung abrufen
|
||||
*/
|
||||
export async function fetchMessages(reportId: string): Promise<AnonymousMessage[]> {
|
||||
return fetchWithTimeout<AnonymousMessage[]>(
|
||||
`${WB_API_BASE}/api/v1/admin/whistleblower/reports/${reportId}/messages`
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ATTACHMENTS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Anhang zu einer Meldung hochladen
|
||||
*/
|
||||
export async function uploadAttachment(
|
||||
reportId: string,
|
||||
file: File
|
||||
): Promise<FileAttachment> {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
const controller = new AbortController()
|
||||
const timeoutId = setTimeout(() => controller.abort(), 60000) // 60s for uploads
|
||||
|
||||
try {
|
||||
const headers: HeadersInit = {
|
||||
'X-Tenant-ID': getTenantId()
|
||||
}
|
||||
if (typeof window !== 'undefined') {
|
||||
const token = localStorage.getItem('authToken')
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`${WB_API_BASE}/api/v1/admin/whistleblower/reports/${reportId}/attachments`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: formData,
|
||||
signal: controller.signal
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Upload fehlgeschlagen: ${response.statusText}`)
|
||||
}
|
||||
|
||||
return response.json()
|
||||
} finally {
|
||||
clearTimeout(timeoutId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Anhang loeschen
|
||||
*/
|
||||
export async function deleteAttachment(id: string): Promise<void> {
|
||||
await fetchWithTimeout<void>(
|
||||
`${WB_API_BASE}/api/v1/admin/whistleblower/attachments/${id}`,
|
||||
{
|
||||
method: 'DELETE'
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// STATISTICS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Statistiken fuer das Whistleblower-Dashboard abrufen
|
||||
*/
|
||||
export async function fetchWhistleblowerStatistics(): Promise<WhistleblowerStatistics> {
|
||||
return fetchWithTimeout<WhistleblowerStatistics>(
|
||||
`${WB_API_BASE}/api/v1/admin/whistleblower/statistics`
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SDK PROXY FUNCTION (via Next.js proxy)
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Fetch Whistleblower-Daten via SDK Proxy mit Fallback auf Mock-Daten
|
||||
*/
|
||||
export async function fetchSDKWhistleblowerList(): Promise<{
|
||||
reports: WhistleblowerReport[]
|
||||
statistics: WhistleblowerStatistics
|
||||
}> {
|
||||
try {
|
||||
const [reportsResponse, statsResponse] = await Promise.all([
|
||||
fetchReports(),
|
||||
fetchWhistleblowerStatistics()
|
||||
])
|
||||
return {
|
||||
reports: reportsResponse.reports,
|
||||
statistics: statsResponse
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load Whistleblower data from API, using mock data:', error)
|
||||
// Fallback to mock data
|
||||
const reports = createMockReports()
|
||||
const statistics = createMockStatistics()
|
||||
return { reports, statistics }
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MOCK DATA (Demo/Entwicklung)
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Erstellt Demo-Meldungen fuer Entwicklung und Praesentationen
|
||||
*/
|
||||
export function createMockReports(): WhistleblowerReport[] {
|
||||
const now = new Date()
|
||||
|
||||
// Helper: Berechne Fristen
|
||||
function calcDeadlines(receivedAt: Date): { ack: string; fb: string } {
|
||||
const ack = new Date(receivedAt)
|
||||
ack.setDate(ack.getDate() + 7)
|
||||
const fb = new Date(receivedAt)
|
||||
fb.setMonth(fb.getMonth() + 3)
|
||||
return { ack: ack.toISOString(), fb: fb.toISOString() }
|
||||
}
|
||||
|
||||
const received1 = new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000)
|
||||
const deadlines1 = calcDeadlines(received1)
|
||||
|
||||
const received2 = new Date(now.getTime() - 14 * 24 * 60 * 60 * 1000)
|
||||
const deadlines2 = calcDeadlines(received2)
|
||||
|
||||
const received3 = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000)
|
||||
const deadlines3 = calcDeadlines(received3)
|
||||
|
||||
const received4 = new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000)
|
||||
const deadlines4 = calcDeadlines(received4)
|
||||
|
||||
return [
|
||||
// Report 1: Neu
|
||||
{
|
||||
id: 'wb-001',
|
||||
referenceNumber: 'WB-2026-000001',
|
||||
accessKey: generateAccessKey(),
|
||||
category: 'corruption',
|
||||
status: 'new',
|
||||
priority: 'high',
|
||||
title: 'Unregelmaessigkeiten bei Auftragsvergabe',
|
||||
description: 'Bei der Vergabe des IT-Rahmenvertrags im November wurden offenbar Angebote eines bestimmten Anbieters bevorzugt. Der zustaendige Abteilungsleiter hat private Verbindungen zum Geschaeftsfuehrer des Anbieters.',
|
||||
isAnonymous: true,
|
||||
receivedAt: received1.toISOString(),
|
||||
deadlineAcknowledgment: deadlines1.ack,
|
||||
deadlineFeedback: deadlines1.fb,
|
||||
measures: [],
|
||||
messages: [],
|
||||
attachments: [],
|
||||
auditTrail: [
|
||||
{
|
||||
id: 'audit-001',
|
||||
action: 'report_created',
|
||||
description: 'Meldung ueber Online-Meldeformular eingegangen',
|
||||
performedBy: 'system',
|
||||
performedAt: received1.toISOString()
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// Report 2: In Pruefung (under_review)
|
||||
{
|
||||
id: 'wb-002',
|
||||
referenceNumber: 'WB-2026-000002',
|
||||
accessKey: generateAccessKey(),
|
||||
category: 'data_protection',
|
||||
status: 'under_review',
|
||||
priority: 'normal',
|
||||
title: 'Unerlaubte Weitergabe von Kundendaten',
|
||||
description: 'Ein Mitarbeiter der Vertriebsabteilung gibt regelmaessig Kundenlisten an externe Dienstleister weiter, ohne dass eine Auftragsverarbeitungsvereinbarung vorliegt.',
|
||||
isAnonymous: false,
|
||||
reporterName: 'Maria Schmidt',
|
||||
reporterEmail: 'maria.schmidt@example.de',
|
||||
assignedTo: 'DSB Mueller',
|
||||
receivedAt: received2.toISOString(),
|
||||
acknowledgedAt: new Date(received2.getTime() + 3 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
deadlineAcknowledgment: deadlines2.ack,
|
||||
deadlineFeedback: deadlines2.fb,
|
||||
measures: [],
|
||||
messages: [
|
||||
{
|
||||
id: 'msg-001',
|
||||
reportId: 'wb-002',
|
||||
senderRole: 'ombudsperson',
|
||||
message: 'Vielen Dank fuer Ihre Meldung. Koennen Sie uns mitteilen, welche Dienstleister konkret betroffen sind?',
|
||||
createdAt: new Date(received2.getTime() + 5 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
isRead: true
|
||||
},
|
||||
{
|
||||
id: 'msg-002',
|
||||
reportId: 'wb-002',
|
||||
senderRole: 'reporter',
|
||||
message: 'Es handelt sich um die Firma DataServ GmbH und MarketPro AG. Die Listen werden per unverschluesselter E-Mail versendet.',
|
||||
createdAt: new Date(received2.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
isRead: true
|
||||
}
|
||||
],
|
||||
attachments: [
|
||||
{
|
||||
id: 'att-001',
|
||||
fileName: 'email_screenshot_vertrieb.png',
|
||||
fileSize: 245000,
|
||||
mimeType: 'image/png',
|
||||
uploadedAt: received2.toISOString(),
|
||||
uploadedBy: 'reporter'
|
||||
}
|
||||
],
|
||||
auditTrail: [
|
||||
{
|
||||
id: 'audit-002',
|
||||
action: 'report_created',
|
||||
description: 'Meldung per E-Mail eingegangen',
|
||||
performedBy: 'system',
|
||||
performedAt: received2.toISOString()
|
||||
},
|
||||
{
|
||||
id: 'audit-003',
|
||||
action: 'acknowledged',
|
||||
description: 'Eingangsbestaetigung an Hinweisgeber versendet',
|
||||
performedBy: 'DSB Mueller',
|
||||
performedAt: new Date(received2.getTime() + 3 * 24 * 60 * 60 * 1000).toISOString()
|
||||
},
|
||||
{
|
||||
id: 'audit-004',
|
||||
action: 'status_changed',
|
||||
description: 'Status geaendert: Bestaetigt -> In Pruefung',
|
||||
performedBy: 'DSB Mueller',
|
||||
performedAt: new Date(received2.getTime() + 5 * 24 * 60 * 60 * 1000).toISOString()
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// Report 3: Untersuchung (investigation)
|
||||
{
|
||||
id: 'wb-003',
|
||||
referenceNumber: 'WB-2026-000003',
|
||||
accessKey: generateAccessKey(),
|
||||
category: 'product_safety',
|
||||
status: 'investigation',
|
||||
priority: 'critical',
|
||||
title: 'Fehlende Sicherheitspruefungen bei Produktfreigabe',
|
||||
description: 'In der Fertigung werden seit Wochen Produkte ohne die vorgeschriebenen Sicherheitspruefungen freigegeben. Pruefprotokolle werden nachtraeglich erstellt, ohne dass tatsaechliche Pruefungen stattfinden.',
|
||||
isAnonymous: true,
|
||||
assignedTo: 'Qualitaetsbeauftragter Weber',
|
||||
receivedAt: received3.toISOString(),
|
||||
acknowledgedAt: new Date(received3.getTime() + 2 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
deadlineAcknowledgment: deadlines3.ack,
|
||||
deadlineFeedback: deadlines3.fb,
|
||||
measures: [
|
||||
{
|
||||
id: 'msr-001',
|
||||
reportId: 'wb-003',
|
||||
title: 'Sofortiger Produktionsstopp fuer betroffene Charge',
|
||||
description: 'Produktion der betroffenen Produktlinie stoppen bis Pruefverfahren sichergestellt ist',
|
||||
status: 'completed',
|
||||
responsible: 'Fertigungsleitung',
|
||||
dueDate: new Date(received3.getTime() + 5 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
completedAt: new Date(received3.getTime() + 3 * 24 * 60 * 60 * 1000).toISOString()
|
||||
},
|
||||
{
|
||||
id: 'msr-002',
|
||||
reportId: 'wb-003',
|
||||
title: 'Externe Pruefung der Pruefprotokolle',
|
||||
description: 'Unabhaengige Pruefstelle mit der Revision aller Pruefprotokolle der letzten 6 Monate beauftragen',
|
||||
status: 'in_progress',
|
||||
responsible: 'Qualitaetsmanagement',
|
||||
dueDate: new Date(now.getTime() + 14 * 24 * 60 * 60 * 1000).toISOString()
|
||||
}
|
||||
],
|
||||
messages: [],
|
||||
attachments: [
|
||||
{
|
||||
id: 'att-002',
|
||||
fileName: 'pruefprotokoll_vergleich.pdf',
|
||||
fileSize: 890000,
|
||||
mimeType: 'application/pdf',
|
||||
uploadedAt: new Date(received3.getTime() + 5 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
uploadedBy: 'ombudsperson'
|
||||
}
|
||||
],
|
||||
auditTrail: [
|
||||
{
|
||||
id: 'audit-005',
|
||||
action: 'report_created',
|
||||
description: 'Meldung ueber Online-Meldeformular eingegangen',
|
||||
performedBy: 'system',
|
||||
performedAt: received3.toISOString()
|
||||
},
|
||||
{
|
||||
id: 'audit-006',
|
||||
action: 'acknowledged',
|
||||
description: 'Eingangsbestaetigung versendet',
|
||||
performedBy: 'Qualitaetsbeauftragter Weber',
|
||||
performedAt: new Date(received3.getTime() + 2 * 24 * 60 * 60 * 1000).toISOString()
|
||||
},
|
||||
{
|
||||
id: 'audit-007',
|
||||
action: 'investigation_started',
|
||||
description: 'Formelle Untersuchung eingeleitet',
|
||||
performedBy: 'Qualitaetsbeauftragter Weber',
|
||||
performedAt: new Date(received3.getTime() + 5 * 24 * 60 * 60 * 1000).toISOString()
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// Report 4: Abgeschlossen (closed)
|
||||
{
|
||||
id: 'wb-004',
|
||||
referenceNumber: 'WB-2026-000004',
|
||||
accessKey: generateAccessKey(),
|
||||
category: 'fraud',
|
||||
status: 'closed',
|
||||
priority: 'high',
|
||||
title: 'Gefaelschte Reisekostenabrechnungen',
|
||||
description: 'Ein leitender Mitarbeiter reicht seit ueber einem Jahr gefaelschte Reisekostenabrechnungen ein. Hotelrechnungen werden manipuliert, Taxiquittungen erfunden.',
|
||||
isAnonymous: false,
|
||||
reporterName: 'Thomas Klein',
|
||||
reporterEmail: 'thomas.klein@example.de',
|
||||
reporterPhone: '+49 170 9876543',
|
||||
assignedTo: 'Compliance-Abteilung',
|
||||
receivedAt: received4.toISOString(),
|
||||
acknowledgedAt: new Date(received4.getTime() + 3 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
deadlineAcknowledgment: deadlines4.ack,
|
||||
deadlineFeedback: deadlines4.fb,
|
||||
closedAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
measures: [
|
||||
{
|
||||
id: 'msr-003',
|
||||
reportId: 'wb-004',
|
||||
title: 'Interne Revision der Reisekosten',
|
||||
description: 'Pruefung aller Reisekostenabrechnungen des betroffenen Mitarbeiters der letzten 24 Monate',
|
||||
status: 'completed',
|
||||
responsible: 'Interne Revision',
|
||||
dueDate: new Date(received4.getTime() + 30 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
completedAt: new Date(received4.getTime() + 25 * 24 * 60 * 60 * 1000).toISOString()
|
||||
},
|
||||
{
|
||||
id: 'msr-004',
|
||||
reportId: 'wb-004',
|
||||
title: 'Arbeitsrechtliche Konsequenzen',
|
||||
description: 'Einleitung arbeitsrechtlicher Schritte nach Bestaetigung des Betrugs',
|
||||
status: 'completed',
|
||||
responsible: 'Personalabteilung',
|
||||
dueDate: new Date(received4.getTime() + 60 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
completedAt: new Date(received4.getTime() + 55 * 24 * 60 * 60 * 1000).toISOString()
|
||||
}
|
||||
],
|
||||
messages: [],
|
||||
attachments: [
|
||||
{
|
||||
id: 'att-003',
|
||||
fileName: 'vergleich_originalrechnung_einreichung.pdf',
|
||||
fileSize: 567000,
|
||||
mimeType: 'application/pdf',
|
||||
uploadedAt: received4.toISOString(),
|
||||
uploadedBy: 'reporter'
|
||||
}
|
||||
],
|
||||
auditTrail: [
|
||||
{
|
||||
id: 'audit-008',
|
||||
action: 'report_created',
|
||||
description: 'Meldung per Brief eingegangen',
|
||||
performedBy: 'system',
|
||||
performedAt: received4.toISOString()
|
||||
},
|
||||
{
|
||||
id: 'audit-009',
|
||||
action: 'acknowledged',
|
||||
description: 'Eingangsbestaetigung versendet',
|
||||
performedBy: 'Compliance-Abteilung',
|
||||
performedAt: new Date(received4.getTime() + 3 * 24 * 60 * 60 * 1000).toISOString()
|
||||
},
|
||||
{
|
||||
id: 'audit-010',
|
||||
action: 'closed',
|
||||
description: 'Fall abgeschlossen - Betrug bestaetigt, arbeitsrechtliche Massnahmen eingeleitet',
|
||||
performedBy: 'Compliance-Abteilung',
|
||||
performedAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString()
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Berechnet Statistiken aus den Mock-Daten
|
||||
*/
|
||||
export function createMockStatistics(): WhistleblowerStatistics {
|
||||
const reports = createMockReports()
|
||||
const now = new Date()
|
||||
|
||||
const byStatus: Record<ReportStatus, number> = {
|
||||
new: 0,
|
||||
acknowledged: 0,
|
||||
under_review: 0,
|
||||
investigation: 0,
|
||||
measures_taken: 0,
|
||||
closed: 0,
|
||||
rejected: 0
|
||||
}
|
||||
|
||||
const byCategory: Record<ReportCategory, number> = {
|
||||
corruption: 0,
|
||||
fraud: 0,
|
||||
data_protection: 0,
|
||||
discrimination: 0,
|
||||
environment: 0,
|
||||
competition: 0,
|
||||
product_safety: 0,
|
||||
tax_evasion: 0,
|
||||
other: 0
|
||||
}
|
||||
|
||||
reports.forEach(r => {
|
||||
byStatus[r.status]++
|
||||
byCategory[r.category]++
|
||||
})
|
||||
|
||||
const closedStatuses: ReportStatus[] = ['closed', 'rejected']
|
||||
|
||||
// Pruefe ueberfaellige Eingangsbestaetigungen
|
||||
const overdueAcknowledgment = reports.filter(r => {
|
||||
if (r.status !== 'new') return false
|
||||
return now > new Date(r.deadlineAcknowledgment)
|
||||
}).length
|
||||
|
||||
// Pruefe ueberfaellige Rueckmeldungen
|
||||
const overdueFeedback = reports.filter(r => {
|
||||
if (closedStatuses.includes(r.status)) return false
|
||||
return now > new Date(r.deadlineFeedback)
|
||||
}).length
|
||||
|
||||
return {
|
||||
totalReports: reports.length,
|
||||
newReports: byStatus.new,
|
||||
underReview: byStatus.under_review + byStatus.investigation,
|
||||
closed: byStatus.closed + byStatus.rejected,
|
||||
overdueAcknowledgment,
|
||||
overdueFeedback,
|
||||
byCategory,
|
||||
byStatus
|
||||
}
|
||||
}
|
||||
381
admin-v2/lib/sdk/whistleblower/types.ts
Normal file
381
admin-v2/lib/sdk/whistleblower/types.ts
Normal file
@@ -0,0 +1,381 @@
|
||||
/**
|
||||
* Whistleblower System (Hinweisgebersystem) Types
|
||||
*
|
||||
* TypeScript definitions for Hinweisgeberschutzgesetz (HinSchG)
|
||||
* compliant Whistleblower/Hinweisgebersystem module
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// ENUMS & CONSTANTS
|
||||
// =============================================================================
|
||||
|
||||
export type ReportCategory =
|
||||
| 'corruption' // Korruption
|
||||
| 'fraud' // Betrug
|
||||
| 'data_protection' // Datenschutz
|
||||
| 'discrimination' // Diskriminierung
|
||||
| 'environment' // Umwelt
|
||||
| 'competition' // Wettbewerb
|
||||
| 'product_safety' // Produktsicherheit
|
||||
| 'tax_evasion' // Steuerhinterziehung
|
||||
| 'other' // Sonstiges
|
||||
|
||||
export type ReportStatus =
|
||||
| 'new' // Neu eingegangen
|
||||
| 'acknowledged' // Eingangsbestaetigung versendet
|
||||
| 'under_review' // In Pruefung
|
||||
| 'investigation' // Untersuchung laeuft
|
||||
| 'measures_taken' // Massnahmen ergriffen
|
||||
| 'closed' // Abgeschlossen
|
||||
| 'rejected' // Abgelehnt
|
||||
|
||||
export type ReportPriority = 'low' | 'normal' | 'high' | 'critical'
|
||||
|
||||
// =============================================================================
|
||||
// REPORT CATEGORY METADATA
|
||||
// =============================================================================
|
||||
|
||||
export interface ReportCategoryInfo {
|
||||
category: ReportCategory
|
||||
label: string
|
||||
description: string
|
||||
icon: string
|
||||
color: string
|
||||
bgColor: string
|
||||
}
|
||||
|
||||
export const REPORT_CATEGORY_INFO: Record<ReportCategory, ReportCategoryInfo> = {
|
||||
corruption: {
|
||||
category: 'corruption',
|
||||
label: 'Korruption',
|
||||
description: 'Bestechung, Bestechlichkeit, Vorteilsnahme oder Vorteilsgewaehrung',
|
||||
icon: '\u{1F4B0}',
|
||||
color: 'text-red-700',
|
||||
bgColor: 'bg-red-100'
|
||||
},
|
||||
fraud: {
|
||||
category: 'fraud',
|
||||
label: 'Betrug',
|
||||
description: 'Betrug, Untreue, Urkundenfaelschung oder sonstige Vermoegensstraftaten',
|
||||
icon: '\u{1F3AD}',
|
||||
color: 'text-orange-700',
|
||||
bgColor: 'bg-orange-100'
|
||||
},
|
||||
data_protection: {
|
||||
category: 'data_protection',
|
||||
label: 'Datenschutz',
|
||||
description: 'Verstoesse gegen Datenschutzvorschriften (DSGVO, BDSG)',
|
||||
icon: '\u{1F512}',
|
||||
color: 'text-blue-700',
|
||||
bgColor: 'bg-blue-100'
|
||||
},
|
||||
discrimination: {
|
||||
category: 'discrimination',
|
||||
label: 'Diskriminierung',
|
||||
description: 'Diskriminierung, Mobbing, sexuelle Belaestigung oder Benachteiligung',
|
||||
icon: '\u{26A0}\u{FE0F}',
|
||||
color: 'text-purple-700',
|
||||
bgColor: 'bg-purple-100'
|
||||
},
|
||||
environment: {
|
||||
category: 'environment',
|
||||
label: 'Umwelt',
|
||||
description: 'Umweltverschmutzung, illegale Entsorgung oder Verstoesse gegen Umweltauflagen',
|
||||
icon: '\u{1F33F}',
|
||||
color: 'text-green-700',
|
||||
bgColor: 'bg-green-100'
|
||||
},
|
||||
competition: {
|
||||
category: 'competition',
|
||||
label: 'Wettbewerb',
|
||||
description: 'Kartellrechtsverstoesse, unlauterer Wettbewerb, Marktmanipulation',
|
||||
icon: '\u{2696}\u{FE0F}',
|
||||
color: 'text-indigo-700',
|
||||
bgColor: 'bg-indigo-100'
|
||||
},
|
||||
product_safety: {
|
||||
category: 'product_safety',
|
||||
label: 'Produktsicherheit',
|
||||
description: 'Verstoesse gegen Produktsicherheitsvorschriften, mangelhafte Produkte, fehlende Warnhinweise',
|
||||
icon: '\u{1F6E1}\u{FE0F}',
|
||||
color: 'text-yellow-700',
|
||||
bgColor: 'bg-yellow-100'
|
||||
},
|
||||
tax_evasion: {
|
||||
category: 'tax_evasion',
|
||||
label: 'Steuerhinterziehung',
|
||||
description: 'Steuerhinterziehung, Steuerumgehung oder sonstige Steuerverstoesse',
|
||||
icon: '\u{1F4C4}',
|
||||
color: 'text-teal-700',
|
||||
bgColor: 'bg-teal-100'
|
||||
},
|
||||
other: {
|
||||
category: 'other',
|
||||
label: 'Sonstiges',
|
||||
description: 'Sonstige Verstoesse gegen geltendes Recht oder interne Richtlinien',
|
||||
icon: '\u{1F4CB}',
|
||||
color: 'text-gray-700',
|
||||
bgColor: 'bg-gray-100'
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// REPORT STATUS METADATA
|
||||
// =============================================================================
|
||||
|
||||
export const REPORT_STATUS_INFO: Record<ReportStatus, { label: string; description: string; color: string; bgColor: string }> = {
|
||||
new: {
|
||||
label: 'Neu',
|
||||
description: 'Meldung ist eingegangen, Eingangsbestaetigung steht aus',
|
||||
color: 'text-blue-700',
|
||||
bgColor: 'bg-blue-100'
|
||||
},
|
||||
acknowledged: {
|
||||
label: 'Bestaetigt',
|
||||
description: 'Eingangsbestaetigung wurde an den Hinweisgeber versendet',
|
||||
color: 'text-cyan-700',
|
||||
bgColor: 'bg-cyan-100'
|
||||
},
|
||||
under_review: {
|
||||
label: 'In Pruefung',
|
||||
description: 'Meldung wird inhaltlich geprueft und bewertet',
|
||||
color: 'text-yellow-700',
|
||||
bgColor: 'bg-yellow-100'
|
||||
},
|
||||
investigation: {
|
||||
label: 'Untersuchung',
|
||||
description: 'Formelle Untersuchung des gemeldeten Sachverhalts laeuft',
|
||||
color: 'text-purple-700',
|
||||
bgColor: 'bg-purple-100'
|
||||
},
|
||||
measures_taken: {
|
||||
label: 'Massnahmen ergriffen',
|
||||
description: 'Folgemaßnahmen wurden eingeleitet oder abgeschlossen',
|
||||
color: 'text-orange-700',
|
||||
bgColor: 'bg-orange-100'
|
||||
},
|
||||
closed: {
|
||||
label: 'Abgeschlossen',
|
||||
description: 'Fall wurde abgeschlossen und dokumentiert',
|
||||
color: 'text-green-700',
|
||||
bgColor: 'bg-green-100'
|
||||
},
|
||||
rejected: {
|
||||
label: 'Abgelehnt',
|
||||
description: 'Meldung wurde als unbegrundet oder nicht zustaendig abgelehnt',
|
||||
color: 'text-red-700',
|
||||
bgColor: 'bg-red-100'
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MAIN INTERFACES
|
||||
// =============================================================================
|
||||
|
||||
export interface FileAttachment {
|
||||
id: string
|
||||
fileName: string
|
||||
fileSize: number
|
||||
mimeType: string
|
||||
uploadedAt: string
|
||||
uploadedBy: string
|
||||
}
|
||||
|
||||
export interface AuditEntry {
|
||||
id: string
|
||||
action: string
|
||||
description: string
|
||||
performedBy: string
|
||||
performedAt: string
|
||||
}
|
||||
|
||||
export interface AnonymousMessage {
|
||||
id: string
|
||||
reportId: string
|
||||
senderRole: 'reporter' | 'ombudsperson'
|
||||
message: string
|
||||
createdAt: string
|
||||
isRead: boolean
|
||||
}
|
||||
|
||||
export interface WhistleblowerMeasure {
|
||||
id: string
|
||||
reportId: string
|
||||
title: string
|
||||
description: string
|
||||
status: 'planned' | 'in_progress' | 'completed'
|
||||
responsible: string
|
||||
dueDate: string
|
||||
completedAt?: string
|
||||
}
|
||||
|
||||
export interface WhistleblowerReport {
|
||||
id: string
|
||||
referenceNumber: string // z.B. "WB-2026-000042"
|
||||
accessKey: string // Anonymer Zugangscode fuer den Hinweisgeber
|
||||
category: ReportCategory
|
||||
status: ReportStatus
|
||||
priority: ReportPriority
|
||||
title: string
|
||||
description: string
|
||||
|
||||
// Hinweisgeber-Info (optional bei anonymen Meldungen)
|
||||
isAnonymous: boolean
|
||||
reporterName?: string
|
||||
reporterEmail?: string
|
||||
reporterPhone?: string
|
||||
|
||||
// Zuweisung
|
||||
assignedTo?: string
|
||||
|
||||
// Zeitstempel
|
||||
receivedAt: string
|
||||
acknowledgedAt?: string
|
||||
|
||||
// Fristen gemaess HinSchG
|
||||
deadlineAcknowledgment: string // 7 Tage nach Eingang (ss 17 Abs. 1 S. 2)
|
||||
deadlineFeedback: string // 3 Monate nach Eingang (ss 17 Abs. 2)
|
||||
closedAt?: string
|
||||
|
||||
// Verknuepfte Daten
|
||||
measures: WhistleblowerMeasure[]
|
||||
messages: AnonymousMessage[]
|
||||
attachments: FileAttachment[]
|
||||
auditTrail: AuditEntry[]
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// STATISTICS
|
||||
// =============================================================================
|
||||
|
||||
export interface WhistleblowerStatistics {
|
||||
totalReports: number
|
||||
newReports: number
|
||||
underReview: number
|
||||
closed: number
|
||||
overdueAcknowledgment: number
|
||||
overdueFeedback: number
|
||||
byCategory: Record<ReportCategory, number>
|
||||
byStatus: Record<ReportStatus, number>
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DEADLINE TRACKING (HinSchG)
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Gibt die verbleibenden Tage bis zur Eingangsbestaetigung zurueck (7-Tage-Frist)
|
||||
* Negative Werte bedeuten ueberfaellig
|
||||
*/
|
||||
export function getDaysUntilAcknowledgment(report: WhistleblowerReport): number {
|
||||
if (report.acknowledgedAt || report.status !== 'new') {
|
||||
return 0
|
||||
}
|
||||
const deadline = new Date(report.deadlineAcknowledgment)
|
||||
const now = new Date()
|
||||
const diff = deadline.getTime() - now.getTime()
|
||||
return Math.ceil(diff / (1000 * 60 * 60 * 24))
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die verbleibenden Tage bis zur Rueckmeldungsfrist zurueck (3-Monate-Frist)
|
||||
* Negative Werte bedeuten ueberfaellig
|
||||
*/
|
||||
export function getDaysUntilFeedback(report: WhistleblowerReport): number {
|
||||
if (report.status === 'closed' || report.status === 'rejected') {
|
||||
return 0
|
||||
}
|
||||
const deadline = new Date(report.deadlineFeedback)
|
||||
const now = new Date()
|
||||
const diff = deadline.getTime() - now.getTime()
|
||||
return Math.ceil(diff / (1000 * 60 * 60 * 24))
|
||||
}
|
||||
|
||||
/**
|
||||
* Prueft ob die Eingangsbestaetigungsfrist ueberschritten ist (7 Tage, HinSchG ss 17 Abs. 1)
|
||||
*/
|
||||
export function isAcknowledgmentOverdue(report: WhistleblowerReport): boolean {
|
||||
if (report.acknowledgedAt || report.status !== 'new') {
|
||||
return false
|
||||
}
|
||||
return new Date() > new Date(report.deadlineAcknowledgment)
|
||||
}
|
||||
|
||||
/**
|
||||
* Prueft ob die Rueckmeldungsfrist ueberschritten ist (3 Monate, HinSchG ss 17 Abs. 2)
|
||||
*/
|
||||
export function isFeedbackOverdue(report: WhistleblowerReport): boolean {
|
||||
if (report.status === 'closed' || report.status === 'rejected') {
|
||||
return false
|
||||
}
|
||||
return new Date() > new Date(report.deadlineFeedback)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert einen anonymen Zugangscode im Format XXXX-XXXX-XXXX
|
||||
*/
|
||||
export function generateAccessKey(): string {
|
||||
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789' // Kein I, O, 0, 1 fuer Lesbarkeit
|
||||
let result = ''
|
||||
for (let i = 0; i < 12; i++) {
|
||||
if (i > 0 && i % 4 === 0) result += '-'
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length))
|
||||
}
|
||||
return result // Format: XXXX-XXXX-XXXX
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// API TYPES
|
||||
// =============================================================================
|
||||
|
||||
export interface ReportFilters {
|
||||
status?: ReportStatus | ReportStatus[]
|
||||
category?: ReportCategory | ReportCategory[]
|
||||
priority?: ReportPriority
|
||||
assignedTo?: string
|
||||
isAnonymous?: boolean
|
||||
search?: string
|
||||
dateFrom?: string
|
||||
dateTo?: string
|
||||
}
|
||||
|
||||
export interface ReportListResponse {
|
||||
reports: WhistleblowerReport[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
export interface PublicReportSubmission {
|
||||
category: ReportCategory
|
||||
title: string
|
||||
description: string
|
||||
isAnonymous: boolean
|
||||
reporterName?: string
|
||||
reporterEmail?: string
|
||||
reporterPhone?: string
|
||||
}
|
||||
|
||||
export interface ReportUpdateRequest {
|
||||
status?: ReportStatus
|
||||
priority?: ReportPriority
|
||||
category?: ReportCategory
|
||||
assignedTo?: string
|
||||
}
|
||||
|
||||
export interface MessageSendRequest {
|
||||
senderRole: 'reporter' | 'ombudsperson'
|
||||
message: string
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
export function getCategoryInfo(category: ReportCategory): ReportCategoryInfo {
|
||||
return REPORT_CATEGORY_INFO[category]
|
||||
}
|
||||
|
||||
export function getStatusInfo(status: ReportStatus) {
|
||||
return REPORT_STATUS_INFO[status]
|
||||
}
|
||||
Reference in New Issue
Block a user