Files
breakpilot-compliance/admin-compliance/app/sdk/vendor-compliance/processing-activities/page.tsx
Benjamin Admin 215b95adfa
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 32s
CI / test-python-backend-compliance (push) Successful in 31s
CI / test-python-document-crawler (push) Successful in 21s
CI / test-python-dsms-gateway (push) Successful in 19s
refactor: Admin-Layout komplett entfernt — SDK als einziges Layout
Kaputtes (admin) Layout geloescht (Role-Selection, 404-Sidebar, localhost-Dashboard).
SDK-Flow nach /sdk/sdk-flow verschoben. Route-Gruppe (sdk) aufgeloest.
Root-Seite redirected auf /sdk. ~25 ungenutzte Dateien/Verzeichnisse entfernt.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 11:43:00 +01:00

426 lines
18 KiB
TypeScript

'use client'
import { useState, useMemo } from 'react'
import Link from 'next/link'
import {
useVendorCompliance,
ProcessingActivity,
ProcessingActivityStatus,
ProtectionLevel,
DATA_SUBJECT_CATEGORY_META,
PERSONAL_DATA_CATEGORY_META,
getStatusColor,
formatDate,
} from '@/lib/sdk/vendor-compliance'
type SortField = 'vvtId' | 'name' | 'status' | 'protectionLevel' | 'updatedAt'
type SortOrder = 'asc' | 'desc'
export default function ProcessingActivitiesPage() {
const { processingActivities, deleteProcessingActivity, duplicateProcessingActivity, isLoading } = useVendorCompliance()
const [searchTerm, setSearchTerm] = useState('')
const [statusFilter, setStatusFilter] = useState<ProcessingActivityStatus | 'ALL'>('ALL')
const [protectionFilter, setProtectionFilter] = useState<ProtectionLevel | 'ALL'>('ALL')
const [sortField, setSortField] = useState<SortField>('vvtId')
const [sortOrder, setSortOrder] = useState<SortOrder>('asc')
const filteredActivities = useMemo(() => {
let result = [...processingActivities]
// Search filter
if (searchTerm) {
const term = searchTerm.toLowerCase()
result = result.filter(
(a) =>
a.vvtId.toLowerCase().includes(term) ||
a.name.de.toLowerCase().includes(term) ||
a.name.en.toLowerCase().includes(term)
)
}
// Status filter
if (statusFilter !== 'ALL') {
result = result.filter((a) => a.status === statusFilter)
}
// Protection level filter
if (protectionFilter !== 'ALL') {
result = result.filter((a) => a.protectionLevel === protectionFilter)
}
// Sort
result.sort((a, b) => {
let comparison = 0
switch (sortField) {
case 'vvtId':
comparison = a.vvtId.localeCompare(b.vvtId)
break
case 'name':
comparison = a.name.de.localeCompare(b.name.de)
break
case 'status':
comparison = a.status.localeCompare(b.status)
break
case 'protectionLevel':
const levels = { LOW: 1, MEDIUM: 2, HIGH: 3 }
comparison = levels[a.protectionLevel] - levels[b.protectionLevel]
break
case 'updatedAt':
comparison = new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime()
break
}
return sortOrder === 'asc' ? comparison : -comparison
})
return result
}, [processingActivities, searchTerm, statusFilter, protectionFilter, sortField, sortOrder])
const handleSort = (field: SortField) => {
if (sortField === field) {
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')
} else {
setSortField(field)
setSortOrder('asc')
}
}
const handleDelete = async (id: string) => {
if (confirm('Möchten Sie diese Verarbeitungstätigkeit wirklich löschen?')) {
await deleteProcessingActivity(id)
}
}
const handleDuplicate = async (id: string) => {
await duplicateProcessingActivity(id)
}
if (isLoading) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
)
}
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
Verarbeitungsverzeichnis (VVT)
</h1>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
Art. 30 DSGVO - Verzeichnis von Verarbeitungstätigkeiten
</p>
</div>
<Link
href="/sdk/vendor-compliance/processing-activities/new"
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
<svg className="w-5 h-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
Neue Verarbeitung
</Link>
</div>
{/* Filters */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
{/* Search */}
<div className="md:col-span-2">
<label htmlFor="search" className="sr-only">Suchen</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg className="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
<input
type="text"
id="search"
className="block w-full pl-10 pr-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md leading-5 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-500 focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
placeholder="VVT-ID oder Name suchen..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
</div>
{/* Status Filter */}
<div>
<label htmlFor="status" className="sr-only">Status</label>
<select
id="status"
className="block w-full pl-3 pr-10 py-2 text-base border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value as ProcessingActivityStatus | 'ALL')}
>
<option value="ALL">Alle Status</option>
<option value="DRAFT">Entwurf</option>
<option value="REVIEW">In Prüfung</option>
<option value="APPROVED">Freigegeben</option>
<option value="ARCHIVED">Archiviert</option>
</select>
</div>
{/* Protection Level Filter */}
<div>
<label htmlFor="protection" className="sr-only">Schutzbedarf</label>
<select
id="protection"
className="block w-full pl-3 pr-10 py-2 text-base border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
value={protectionFilter}
onChange={(e) => setProtectionFilter(e.target.value as ProtectionLevel | 'ALL')}
>
<option value="ALL">Alle Schutzbedarfe</option>
<option value="LOW">Niedrig</option>
<option value="MEDIUM">Mittel</option>
<option value="HIGH">Hoch</option>
</select>
</div>
</div>
</div>
{/* Table */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-700">
<tr>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-600"
onClick={() => handleSort('vvtId')}
>
<div className="flex items-center gap-1">
VVT-ID
<SortIcon field="vvtId" currentField={sortField} order={sortOrder} />
</div>
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-600"
onClick={() => handleSort('name')}
>
<div className="flex items-center gap-1">
Name
<SortIcon field="name" currentField={sortField} order={sortOrder} />
</div>
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Betroffene
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Datenkategorien
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-600"
onClick={() => handleSort('protectionLevel')}
>
<div className="flex items-center gap-1">
Schutzbedarf
<SortIcon field="protectionLevel" currentField={sortField} order={sortOrder} />
</div>
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-600"
onClick={() => handleSort('status')}
>
<div className="flex items-center gap-1">
Status
<SortIcon field="status" currentField={sortField} order={sortOrder} />
</div>
</th>
<th scope="col" className="relative px-6 py-3">
<span className="sr-only">Aktionen</span>
</th>
</tr>
</thead>
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
{filteredActivities.map((activity) => (
<tr key={activity.id} className="hover:bg-gray-50 dark:hover:bg-gray-700">
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">
{activity.vvtId}
</td>
<td className="px-6 py-4">
<div className="text-sm font-medium text-gray-900 dark:text-white">
{activity.name.de}
</div>
{activity.name.en && activity.name.en !== activity.name.de && (
<div className="text-sm text-gray-500 dark:text-gray-400">
{activity.name.en}
</div>
)}
</td>
<td className="px-6 py-4">
<div className="flex flex-wrap gap-1">
{activity.dataSubjectCategories.slice(0, 2).map((cat) => (
<span
key={cat}
className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200"
>
{DATA_SUBJECT_CATEGORY_META[cat]?.de || cat}
</span>
))}
{activity.dataSubjectCategories.length > 2 && (
<span className="text-xs text-gray-500 dark:text-gray-400">
+{activity.dataSubjectCategories.length - 2}
</span>
)}
</div>
</td>
<td className="px-6 py-4">
<div className="flex flex-wrap gap-1">
{activity.personalDataCategories.slice(0, 2).map((cat) => (
<span
key={cat}
className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${
PERSONAL_DATA_CATEGORY_META[cat]?.isSpecial
? 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'
: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200'
}`}
>
{PERSONAL_DATA_CATEGORY_META[cat]?.label.de || cat}
</span>
))}
{activity.personalDataCategories.length > 2 && (
<span className="text-xs text-gray-500 dark:text-gray-400">
+{activity.personalDataCategories.length - 2}
</span>
)}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<ProtectionLevelBadge level={activity.protectionLevel} />
</td>
<td className="px-6 py-4 whitespace-nowrap">
<StatusBadge status={activity.status} />
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div className="flex items-center justify-end gap-2">
<Link
href={`/sdk/vendor-compliance/processing-activities/${activity.id}`}
className="text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300"
>
Bearbeiten
</Link>
<button
onClick={() => handleDuplicate(activity.id)}
className="text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-300"
>
Duplizieren
</button>
<button
onClick={() => handleDelete(activity.id)}
className="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300"
>
Löschen
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
{filteredActivities.length === 0 && (
<div className="text-center py-12">
<svg
className="mx-auto h-12 w-12 text-gray-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
<h3 className="mt-2 text-sm font-medium text-gray-900 dark:text-white">
Keine Verarbeitungstätigkeiten
</h3>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
Erstellen Sie eine neue Verarbeitungstätigkeit, um zu beginnen.
</p>
<div className="mt-6">
<Link
href="/sdk/vendor-compliance/processing-activities/new"
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700"
>
<svg className="w-5 h-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
Neue Verarbeitung
</Link>
</div>
</div>
)}
</div>
{/* Summary */}
<div className="text-sm text-gray-500 dark:text-gray-400">
{filteredActivities.length} von {processingActivities.length} Verarbeitungstätigkeiten
</div>
</div>
)
}
function SortIcon({ field, currentField, order }: { field: SortField; currentField: SortField; order: SortOrder }) {
if (field !== currentField) {
return (
<svg className="w-4 h-4 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4" />
</svg>
)
}
return order === 'asc' ? (
<svg className="w-4 h-4 text-blue-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 15l7-7 7 7" />
</svg>
) : (
<svg className="w-4 h-4 text-blue-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
)
}
function StatusBadge({ status }: { status: ProcessingActivityStatus }) {
const statusConfig = {
DRAFT: { label: 'Entwurf', color: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200' },
REVIEW: { label: 'In Prüfung', color: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200' },
APPROVED: { label: 'Freigegeben', color: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' },
ARCHIVED: { label: 'Archiviert', color: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200' },
}
const config = statusConfig[status]
return (
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${config.color}`}>
{config.label}
</span>
)
}
function ProtectionLevelBadge({ level }: { level: ProtectionLevel }) {
const config = {
LOW: { label: 'Niedrig', color: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' },
MEDIUM: { label: 'Mittel', color: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200' },
HIGH: { label: 'Hoch', color: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200' },
}
return (
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${config[level].color}`}>
{config[level].label}
</span>
)
}