diff --git a/admin-compliance/app/sdk/audit-llm/_components/ComplianceTab.tsx b/admin-compliance/app/sdk/audit-llm/_components/ComplianceTab.tsx new file mode 100644 index 0000000..1ff0046 --- /dev/null +++ b/admin-compliance/app/sdk/audit-llm/_components/ComplianceTab.tsx @@ -0,0 +1,128 @@ +'use client' + +import { StatCard } from './StatCard' +import { formatNumber, type ComplianceReport } from './types' + +export function ComplianceTab({ complianceReport }: { complianceReport: ComplianceReport }) { + return ( +
+ {/* Summary Cards */} +
+ + 0} + /> + 0.05} + /> + +
+ + {complianceReport.policy_violations > 0 && ( +
+
+ + + + {complianceReport.policy_violations} Policy-Verletzungen im Zeitraum +
+
+ )} + +
+ {/* PII Categories */} +
+

PII-Kategorien

+ {Object.entries(complianceReport.top_pii_categories || {}).length === 0 ? ( +

Keine PII erkannt

+ ) : ( +
+ {Object.entries(complianceReport.top_pii_categories).sort((a, b) => b[1] - a[1]).map(([cat, count]) => ( +
+ {cat} + {count} +
+ ))} +
+ )} +
+ + {/* Namespace Breakdown */} +
+

Namespace-Analyse

+ {Object.entries(complianceReport.namespace_breakdown || {}).length === 0 ? ( +

Keine Namespace-Daten

+ ) : ( +
+ + + + + + + + + + {Object.entries(complianceReport.namespace_breakdown).map(([ns, data]) => ( + + + + + + ))} + +
NamespaceRequestsPII
{ns}{formatNumber(data.requests)} + {data.pii_incidents > 0 ? ( + {data.pii_incidents} + ) : ( + 0 + )} +
+
+ )} +
+
+ + {/* User Breakdown */} + {Object.entries(complianceReport.user_breakdown || {}).length > 0 && ( +
+

Top-Nutzer

+
+ + + + + + + + + + {Object.entries(complianceReport.user_breakdown) + .sort((a, b) => b[1].requests - a[1].requests) + .slice(0, 10) + .map(([userId, data]) => ( + + + + + + ))} + +
User-IDRequestsPII-Vorfaelle
{userId}{formatNumber(data.requests)} + {data.pii_incidents > 0 ? ( + {data.pii_incidents} + ) : ( + 0 + )} +
+
+
+ )} +
+ ) +} diff --git a/admin-compliance/app/sdk/audit-llm/_components/LLMLogTab.tsx b/admin-compliance/app/sdk/audit-llm/_components/LLMLogTab.tsx new file mode 100644 index 0000000..c9f82a9 --- /dev/null +++ b/admin-compliance/app/sdk/audit-llm/_components/LLMLogTab.tsx @@ -0,0 +1,90 @@ +'use client' + +import { formatDate, formatNumber, formatDuration, type LLMLogEntry } from './types' + +interface Props { + logEntries: LLMLogEntry[] + logFilter: { model: string; pii: string } + onFilterChange: (filter: { model: string; pii: string }) => void +} + +export function LLMLogTab({ logEntries, logFilter, onFilterChange }: Props) { + return ( +
+ {/* Filters */} +
+ onFilterChange({ ...logFilter, model: e.target.value })} + className="border border-gray-300 rounded-lg px-3 py-2 text-sm w-48" + /> + +
+ + {/* Table */} +
+ + + + + + + + + + + + + + {logEntries.length === 0 ? ( + + + + ) : logEntries.map(entry => ( + + + + + + + + + + ))} + +
ZeitpunktUserModelTokensPIIDauerStatus
+ Keine Log-Eintraege im gewaehlten Zeitraum +
{formatDate(entry.created_at)}{entry.user_id?.slice(0, 8)}... + + {entry.model} + + {formatNumber(entry.total_tokens)} + {entry.pii_detected ? ( + + {entry.redacted ? 'Redacted' : 'Erkannt'} + + ) : ( + - + )} + {formatDuration(entry.duration_ms)} + + {entry.status} + +
+
+
{logEntries.length} Eintraege
+
+ ) +} diff --git a/admin-compliance/app/sdk/audit-llm/_components/StatCard.tsx b/admin-compliance/app/sdk/audit-llm/_components/StatCard.tsx new file mode 100644 index 0000000..be68647 --- /dev/null +++ b/admin-compliance/app/sdk/audit-llm/_components/StatCard.tsx @@ -0,0 +1,10 @@ +'use client' + +export function StatCard({ label, value, highlight }: { label: string; value: string; highlight?: boolean }) { + return ( +
+
{label}
+
{value}
+
+ ) +} diff --git a/admin-compliance/app/sdk/audit-llm/_components/UsageTab.tsx b/admin-compliance/app/sdk/audit-llm/_components/UsageTab.tsx new file mode 100644 index 0000000..22cadf9 --- /dev/null +++ b/admin-compliance/app/sdk/audit-llm/_components/UsageTab.tsx @@ -0,0 +1,84 @@ +'use client' + +import { StatCard } from './StatCard' +import { formatNumber, formatDuration, type UsageStats } from './types' + +export function UsageTab({ usageStats }: { usageStats: UsageStats }) { + return ( +
+ {/* Stats Cards */} +
+ + + + 0.1} + /> +
+ + {/* Token Breakdown */} +
+
+

Model-Nutzung

+ {Object.entries(usageStats.models_used || {}).length === 0 ? ( +

Keine Daten

+ ) : ( +
+ {Object.entries(usageStats.models_used).sort((a, b) => b[1] - a[1]).map(([model, count]) => ( +
+ {model} +
+
+
+
+ {formatNumber(count)} +
+
+ ))} +
+ )} +
+ +
+

Provider-Verteilung

+ {Object.entries(usageStats.providers_used || {}).length === 0 ? ( +

Keine Daten

+ ) : ( +
+ {Object.entries(usageStats.providers_used).sort((a, b) => b[1] - a[1]).map(([provider, count]) => ( +
+ {provider} + {formatNumber(count)} +
+ ))} +
+ )} +
+
+ + {/* Token Details */} +
+

Token-Aufschluesselung

+
+
+
{formatNumber(usageStats.total_prompt_tokens)}
+
Prompt Tokens
+
+
+
{formatNumber(usageStats.total_completion_tokens)}
+
Completion Tokens
+
+
+
{formatNumber(usageStats.total_tokens)}
+
Gesamt
+
+
+
+
+ ) +} diff --git a/admin-compliance/app/sdk/audit-llm/_components/types.ts b/admin-compliance/app/sdk/audit-llm/_components/types.ts new file mode 100644 index 0000000..ada7593 --- /dev/null +++ b/admin-compliance/app/sdk/audit-llm/_components/types.ts @@ -0,0 +1,75 @@ +export interface LLMLogEntry { + id: string + user_id: string + namespace: string + model: string + provider: string + prompt_tokens: number + completion_tokens: number + total_tokens: number + pii_detected: boolean + pii_categories: string[] + redacted: boolean + duration_ms: number + status: string + created_at: string +} + +export interface UsageStats { + total_requests: number + total_tokens: number + total_prompt_tokens: number + total_completion_tokens: number + models_used: Record + providers_used: Record + avg_duration_ms: number + pii_detection_rate: number + period_start: string + period_end: string +} + +export interface ComplianceReport { + total_requests: number + pii_incidents: number + pii_rate: number + redaction_rate: number + policy_violations: number + top_pii_categories: Record + namespace_breakdown: Record + user_breakdown: Record + period_start: string + period_end: string +} + +export type TabId = 'llm-log' | 'usage' | 'compliance' + +export const API_BASE = '/api/sdk/v1/audit-llm' + +export function formatDate(iso: string): string { + return new Date(iso).toLocaleString('de-DE', { + day: '2-digit', month: '2-digit', year: 'numeric', + hour: '2-digit', minute: '2-digit', + }) +} + +export function formatNumber(n: number): string { + return n.toLocaleString('de-DE') +} + +export function formatDuration(ms: number): string { + if (ms < 1000) return `${ms}ms` + return `${(ms / 1000).toFixed(1)}s` +} + +export function getDateRange(period: string): { from: string; to: string } { + const now = new Date() + const to = now.toISOString().slice(0, 10) + const from = new Date(now) + switch (period) { + case '7d': from.setDate(from.getDate() - 7); break + case '30d': from.setDate(from.getDate() - 30); break + case '90d': from.setDate(from.getDate() - 90); break + default: from.setDate(from.getDate() - 7) + } + return { from: from.toISOString().slice(0, 10), to } +} diff --git a/admin-compliance/app/sdk/audit-llm/_hooks/useAuditData.ts b/admin-compliance/app/sdk/audit-llm/_hooks/useAuditData.ts new file mode 100644 index 0000000..702c09b --- /dev/null +++ b/admin-compliance/app/sdk/audit-llm/_hooks/useAuditData.ts @@ -0,0 +1,94 @@ +'use client' + +import { useState, useCallback } from 'react' +import { API_BASE, getDateRange, type LLMLogEntry, type UsageStats, type ComplianceReport } from '../_components/types' + +export function useAuditData(period: string, logFilter: { model: string; pii: string }) { + const [logEntries, setLogEntries] = useState([]) + const [usageStats, setUsageStats] = useState(null) + const [complianceReport, setComplianceReport] = useState(null) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + + const loadLLMLog = useCallback(async () => { + setLoading(true) + setError(null) + try { + const { from, to } = getDateRange(period) + const params = new URLSearchParams({ from, to, limit: '100' }) + if (logFilter.model) params.set('model', logFilter.model) + if (logFilter.pii === 'true') params.set('pii_detected', 'true') + if (logFilter.pii === 'false') params.set('pii_detected', 'false') + + const res = await fetch(`${API_BASE}/llm?${params}`) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + const data = await res.json() + setLogEntries(Array.isArray(data) ? data : data.entries || data.logs || []) + } catch (e) { + setError(e instanceof Error ? e.message : 'Fehler beim Laden') + } finally { + setLoading(false) + } + }, [period, logFilter]) + + const loadUsage = useCallback(async () => { + setLoading(true) + setError(null) + try { + const { from, to } = getDateRange(period) + const res = await fetch(`${API_BASE}/usage?from=${from}&to=${to}`) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + const data = await res.json() + setUsageStats(data) + } catch (e) { + setError(e instanceof Error ? e.message : 'Fehler beim Laden') + } finally { + setLoading(false) + } + }, [period]) + + const loadCompliance = useCallback(async () => { + setLoading(true) + setError(null) + try { + const { from, to } = getDateRange(period) + const res = await fetch(`${API_BASE}/compliance-report?from=${from}&to=${to}`) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + const data = await res.json() + setComplianceReport(data) + } catch (e) { + setError(e instanceof Error ? e.message : 'Fehler beim Laden') + } finally { + setLoading(false) + } + }, [period]) + + const handleExport = useCallback(async (type: 'llm' | 'general' | 'compliance', format: 'json' | 'csv') => { + try { + const { from, to } = getDateRange(period) + const res = await fetch(`${API_BASE}/export/${type}?from=${from}&to=${to}&format=${format}`) + if (!res.ok) throw new Error(`Export fehlgeschlagen: ${res.status}`) + const blob = await res.blob() + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = `audit-${type}-${from}-${to}.${format}` + a.click() + URL.revokeObjectURL(url) + } catch (e) { + setError(e instanceof Error ? e.message : 'Export fehlgeschlagen') + } + }, [period]) + + return { + logEntries, + usageStats, + complianceReport, + loading, + error, + loadLLMLog, + loadUsage, + loadCompliance, + handleExport, + } +} diff --git a/admin-compliance/app/sdk/audit-llm/page.tsx b/admin-compliance/app/sdk/audit-llm/page.tsx index ebef77e..78e6c84 100644 --- a/admin-compliance/app/sdk/audit-llm/page.tsx +++ b/admin-compliance/app/sdk/audit-llm/page.tsx @@ -1,167 +1,36 @@ 'use client' -import React, { useState, useEffect, useCallback } from 'react' +import { useState, useEffect } from 'react' import { useSDK } from '@/lib/sdk' +import { useAuditData } from './_hooks/useAuditData' +import { LLMLogTab } from './_components/LLMLogTab' +import { UsageTab } from './_components/UsageTab' +import { ComplianceTab } from './_components/ComplianceTab' +import type { TabId } from './_components/types' -// ============================================================================= -// TYPES -// ============================================================================= - -interface LLMLogEntry { - id: string - user_id: string - namespace: string - model: string - provider: string - prompt_tokens: number - completion_tokens: number - total_tokens: number - pii_detected: boolean - pii_categories: string[] - redacted: boolean - duration_ms: number - status: string - created_at: string -} - -interface UsageStats { - total_requests: number - total_tokens: number - total_prompt_tokens: number - total_completion_tokens: number - models_used: Record - providers_used: Record - avg_duration_ms: number - pii_detection_rate: number - period_start: string - period_end: string -} - -interface ComplianceReport { - total_requests: number - pii_incidents: number - pii_rate: number - redaction_rate: number - policy_violations: number - top_pii_categories: Record - namespace_breakdown: Record - user_breakdown: Record - period_start: string - period_end: string -} - -type TabId = 'llm-log' | 'usage' | 'compliance' - -// ============================================================================= -// HELPERS -// ============================================================================= - -const API_BASE = '/api/sdk/v1/audit-llm' - -function formatDate(iso: string): string { - return new Date(iso).toLocaleString('de-DE', { - day: '2-digit', month: '2-digit', year: 'numeric', - hour: '2-digit', minute: '2-digit', - }) -} - -function formatNumber(n: number): string { - return n.toLocaleString('de-DE') -} - -function formatDuration(ms: number): string { - if (ms < 1000) return `${ms}ms` - return `${(ms / 1000).toFixed(1)}s` -} - -function getDateRange(period: string): { from: string; to: string } { - const now = new Date() - const to = now.toISOString().slice(0, 10) - const from = new Date(now) - switch (period) { - case '7d': from.setDate(from.getDate() - 7); break - case '30d': from.setDate(from.getDate() - 30); break - case '90d': from.setDate(from.getDate() - 90); break - default: from.setDate(from.getDate() - 7) - } - return { from: from.toISOString().slice(0, 10), to } -} - -// ============================================================================= -// MAIN PAGE -// ============================================================================= +const tabs: { id: TabId; label: string }[] = [ + { id: 'llm-log', label: 'LLM-Log' }, + { id: 'usage', label: 'Nutzung' }, + { id: 'compliance', label: 'Compliance' }, +] export default function AuditLLMPage() { const { state } = useSDK() const [activeTab, setActiveTab] = useState('llm-log') const [period, setPeriod] = useState('7d') - const [loading, setLoading] = useState(false) - const [error, setError] = useState(null) - - // LLM Log state - const [logEntries, setLogEntries] = useState([]) const [logFilter, setLogFilter] = useState({ model: '', pii: '' }) - // Usage state - const [usageStats, setUsageStats] = useState(null) - - // Compliance state - const [complianceReport, setComplianceReport] = useState(null) - - // ─── Load Data ─────────────────────────────────────────────────────── - - const loadLLMLog = useCallback(async () => { - setLoading(true) - setError(null) - try { - const { from, to } = getDateRange(period) - const params = new URLSearchParams({ from, to, limit: '100' }) - if (logFilter.model) params.set('model', logFilter.model) - if (logFilter.pii === 'true') params.set('pii_detected', 'true') - if (logFilter.pii === 'false') params.set('pii_detected', 'false') - - const res = await fetch(`${API_BASE}/llm?${params}`) - if (!res.ok) throw new Error(`HTTP ${res.status}`) - const data = await res.json() - setLogEntries(Array.isArray(data) ? data : data.entries || data.logs || []) - } catch (e) { - setError(e instanceof Error ? e.message : 'Fehler beim Laden') - } finally { - setLoading(false) - } - }, [period, logFilter]) - - const loadUsage = useCallback(async () => { - setLoading(true) - setError(null) - try { - const { from, to } = getDateRange(period) - const res = await fetch(`${API_BASE}/usage?from=${from}&to=${to}`) - if (!res.ok) throw new Error(`HTTP ${res.status}`) - const data = await res.json() - setUsageStats(data) - } catch (e) { - setError(e instanceof Error ? e.message : 'Fehler beim Laden') - } finally { - setLoading(false) - } - }, [period]) - - const loadCompliance = useCallback(async () => { - setLoading(true) - setError(null) - try { - const { from, to } = getDateRange(period) - const res = await fetch(`${API_BASE}/compliance-report?from=${from}&to=${to}`) - if (!res.ok) throw new Error(`HTTP ${res.status}`) - const data = await res.json() - setComplianceReport(data) - } catch (e) { - setError(e instanceof Error ? e.message : 'Fehler beim Laden') - } finally { - setLoading(false) - } - }, [period]) + const { + logEntries, + usageStats, + complianceReport, + loading, + error, + loadLLMLog, + loadUsage, + loadCompliance, + handleExport, + } = useAuditData(period, logFilter) useEffect(() => { if (activeTab === 'llm-log') loadLLMLog() @@ -169,42 +38,13 @@ export default function AuditLLMPage() { else if (activeTab === 'compliance') loadCompliance() }, [activeTab, loadLLMLog, loadUsage, loadCompliance]) - // ─── Export ────────────────────────────────────────────────────────── - - const handleExport = async (type: 'llm' | 'general' | 'compliance', format: 'json' | 'csv') => { - try { - const { from, to } = getDateRange(period) - const res = await fetch(`${API_BASE}/export/${type}?from=${from}&to=${to}&format=${format}`) - if (!res.ok) throw new Error(`Export fehlgeschlagen: ${res.status}`) - const blob = await res.blob() - const url = URL.createObjectURL(blob) - const a = document.createElement('a') - a.href = url - a.download = `audit-${type}-${from}-${to}.${format}` - a.click() - URL.revokeObjectURL(url) - } catch (e) { - setError(e instanceof Error ? e.message : 'Export fehlgeschlagen') - } - } - - // ─── Tabs ──────────────────────────────────────────────────────────── - - const tabs: { id: TabId; label: string }[] = [ - { id: 'llm-log', label: 'LLM-Log' }, - { id: 'usage', label: 'Nutzung' }, - { id: 'compliance', label: 'Compliance' }, - ] - return (
- {/* Header */}

LLM Audit Dashboard

Monitoring und Compliance-Analyse der LLM-Operationen

- {/* Period + Tabs */}
{tabs.map(tab => ( @@ -254,289 +94,22 @@ export default function AuditLLMPage() {
)} - {/* ── LLM-Log Tab ── */} {!loading && activeTab === 'llm-log' && ( -
- {/* Filters */} -
- setLogFilter(f => ({ ...f, model: e.target.value }))} - className="border border-gray-300 rounded-lg px-3 py-2 text-sm w-48" - /> - -
- - {/* Table */} -
- - - - - - - - - - - - - - {logEntries.length === 0 ? ( - - - - ) : logEntries.map(entry => ( - - - - - - - - - - ))} - -
ZeitpunktUserModelTokensPIIDauerStatus
- Keine Log-Eintraege im gewaehlten Zeitraum -
{formatDate(entry.created_at)}{entry.user_id?.slice(0, 8)}... - - {entry.model} - - {formatNumber(entry.total_tokens)} - {entry.pii_detected ? ( - - {entry.redacted ? 'Redacted' : 'Erkannt'} - - ) : ( - - - )} - {formatDuration(entry.duration_ms)} - - {entry.status} - -
-
-
{logEntries.length} Eintraege
-
+ )} - {/* ── Nutzung Tab ── */} {!loading && activeTab === 'usage' && usageStats && ( -
- {/* Stats Cards */} -
- - - - 0.1} - /> -
- - {/* Token Breakdown */} -
-
-

Model-Nutzung

- {Object.entries(usageStats.models_used || {}).length === 0 ? ( -

Keine Daten

- ) : ( -
- {Object.entries(usageStats.models_used).sort((a, b) => b[1] - a[1]).map(([model, count]) => ( -
- {model} -
-
-
-
- {formatNumber(count)} -
-
- ))} -
- )} -
- -
-

Provider-Verteilung

- {Object.entries(usageStats.providers_used || {}).length === 0 ? ( -

Keine Daten

- ) : ( -
- {Object.entries(usageStats.providers_used).sort((a, b) => b[1] - a[1]).map(([provider, count]) => ( -
- {provider} - {formatNumber(count)} -
- ))} -
- )} -
-
- - {/* Token Details */} -
-

Token-Aufschluesselung

-
-
-
{formatNumber(usageStats.total_prompt_tokens)}
-
Prompt Tokens
-
-
-
{formatNumber(usageStats.total_completion_tokens)}
-
Completion Tokens
-
-
-
{formatNumber(usageStats.total_tokens)}
-
Gesamt
-
-
-
-
+ )} - {/* ── Compliance Tab ── */} {!loading && activeTab === 'compliance' && complianceReport && ( -
- {/* Summary Cards */} -
- - 0} - /> - 0.05} - /> - -
- - {complianceReport.policy_violations > 0 && ( -
-
- - - - {complianceReport.policy_violations} Policy-Verletzungen im Zeitraum -
-
- )} - -
- {/* PII Categories */} -
-

PII-Kategorien

- {Object.entries(complianceReport.top_pii_categories || {}).length === 0 ? ( -

Keine PII erkannt

- ) : ( -
- {Object.entries(complianceReport.top_pii_categories).sort((a, b) => b[1] - a[1]).map(([cat, count]) => ( -
- {cat} - {count} -
- ))} -
- )} -
- - {/* Namespace Breakdown */} -
-

Namespace-Analyse

- {Object.entries(complianceReport.namespace_breakdown || {}).length === 0 ? ( -

Keine Namespace-Daten

- ) : ( -
- - - - - - - - - - {Object.entries(complianceReport.namespace_breakdown).map(([ns, data]) => ( - - - - - - ))} - -
NamespaceRequestsPII
{ns}{formatNumber(data.requests)} - {data.pii_incidents > 0 ? ( - {data.pii_incidents} - ) : ( - 0 - )} -
-
- )} -
-
- - {/* User Breakdown */} - {Object.entries(complianceReport.user_breakdown || {}).length > 0 && ( -
-

Top-Nutzer

-
- - - - - - - - - - {Object.entries(complianceReport.user_breakdown) - .sort((a, b) => b[1].requests - a[1].requests) - .slice(0, 10) - .map(([userId, data]) => ( - - - - - - ))} - -
User-IDRequestsPII-Vorfaelle
{userId}{formatNumber(data.requests)} - {data.pii_incidents > 0 ? ( - {data.pii_incidents} - ) : ( - 0 - )} -
-
-
- )} -
+ )} - {/* Empty state for usage/compliance when no data */} {!loading && activeTab === 'usage' && !usageStats && !error && (
Keine Nutzungsdaten verfuegbar
)} @@ -546,16 +119,3 @@ export default function AuditLLMPage() {
) } - -// ============================================================================= -// STAT CARD -// ============================================================================= - -function StatCard({ label, value, highlight }: { label: string; value: string; highlight?: boolean }) { - return ( -
-
{label}
-
{value}
-
- ) -} diff --git a/admin-compliance/app/sdk/dsfa/_components/DSFACard.tsx b/admin-compliance/app/sdk/dsfa/_components/DSFACard.tsx new file mode 100644 index 0000000..a09a365 --- /dev/null +++ b/admin-compliance/app/sdk/dsfa/_components/DSFACard.tsx @@ -0,0 +1,133 @@ +'use client' + +export interface DSFA { + id: string + title: string + description: string + status: 'draft' | 'in-review' | 'approved' | 'needs-update' + createdAt: string + updatedAt: string + approvedBy: string | null + riskLevel: 'low' | 'medium' | 'high' | 'critical' + processingActivity: string + dataCategories: string[] + recipients: string[] + measures: string[] +} + +export function DSFACard({ + dsfa, + onStatusChange, + onDelete, +}: { + dsfa: DSFA + onStatusChange: (id: string, status: string) => void + onDelete: (id: string) => void +}) { + const statusColors = { + draft: 'bg-gray-100 text-gray-600 border-gray-200', + 'in-review': 'bg-yellow-100 text-yellow-700 border-yellow-200', + approved: 'bg-green-100 text-green-700 border-green-200', + 'needs-update': 'bg-orange-100 text-orange-700 border-orange-200', + } + + const statusLabels = { + draft: 'Entwurf', + 'in-review': 'In Pruefung', + approved: 'Genehmigt', + 'needs-update': 'Aktualisierung erforderlich', + } + + const riskColors = { + low: 'bg-green-100 text-green-700', + medium: 'bg-yellow-100 text-yellow-700', + high: 'bg-orange-100 text-orange-700', + critical: 'bg-red-100 text-red-700', + } + + const createdDate = dsfa.createdAt + ? new Date(dsfa.createdAt).toLocaleDateString('de-DE') + : '—' + + return ( +
+
+
+
+ + {statusLabels[dsfa.status]} + + + Risiko: {dsfa.riskLevel === 'low' ? 'Niedrig' : + dsfa.riskLevel === 'medium' ? 'Mittel' : + dsfa.riskLevel === 'high' ? 'Hoch' : 'Kritisch'} + +
+

{dsfa.title}

+

{dsfa.description}

+
+
+ +
+

Verarbeitungstaetigkeit: {dsfa.processingActivity}

+
+ +
+ {dsfa.dataCategories.map(cat => ( + + {cat} + + ))} +
+ + {dsfa.measures.length > 0 && ( +
+ Massnahmen: +
+ {dsfa.measures.map(m => ( + + {m} + + ))} +
+
+ )} + +
+
+ Erstellt: {createdDate} + {dsfa.approvedBy && ( + Genehmigt von: {dsfa.approvedBy} + )} +
+
+ {dsfa.status === 'draft' && ( + + )} + {dsfa.status === 'in-review' && ( + + )} + +
+
+
+ ) +} diff --git a/admin-compliance/app/sdk/dsfa/_components/GeneratorWizard.tsx b/admin-compliance/app/sdk/dsfa/_components/GeneratorWizard.tsx new file mode 100644 index 0000000..1132340 --- /dev/null +++ b/admin-compliance/app/sdk/dsfa/_components/GeneratorWizard.tsx @@ -0,0 +1,191 @@ +'use client' + +import React, { useState } from 'react' +import type { DSFA } from './DSFACard' + +export function GeneratorWizard({ onClose, onSubmit }: { onClose: () => void; onSubmit: (data: Partial) => Promise }) { + const [step, setStep] = useState(1) + const [saving, setSaving] = useState(false) + const [title, setTitle] = useState('') + const [description, setDescription] = useState('') + const [processingActivity, setProcessingActivity] = useState('') + const [selectedCategories, setSelectedCategories] = useState([]) + const [riskLevel, setRiskLevel] = useState<'low' | 'medium' | 'high' | 'critical'>('low') + const [selectedMeasures, setSelectedMeasures] = useState([]) + + const riskMap: Record = { + Niedrig: 'low', Mittel: 'medium', Hoch: 'high', Kritisch: 'critical', + } + + const handleSubmit = async () => { + setSaving(true) + try { + await onSubmit({ + title, + description, + processingActivity, + dataCategories: selectedCategories, + riskLevel, + measures: selectedMeasures, + status: 'draft', + }) + onClose() + } finally { + setSaving(false) + } + } + + return ( +
+
+

Neue DSFA erstellen

+ +
+ + {/* Progress Steps */} +
+ {[1, 2, 3, 4].map(s => ( + +
+ {s < step ? ( + + + + ) : s} +
+ {s < 4 &&
} + + ))} +
+ + {/* Step Content */} +
+ {step === 1 && ( +
+
+ + setTitle(e.target.value)} + placeholder="z.B. DSFA - Mitarbeiter-Monitoring" + className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500" + /> +
+
+ +