Files
breakpilot-compliance/admin-compliance/app/sdk/vvt/page.tsx
Sharang Parnerkar 74927c6f66 refactor(admin): split vvt page.tsx into colocated components
Split the 1371-line VVT page into _components/ extractions
(FormPrimitives, api, TabVerzeichnis, TabEditor, TabExport)
to bring page.tsx under the 300 LOC soft target.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 22:47:52 +02:00

237 lines
8.1 KiB
TypeScript

'use client'
/**
* VVT — Verarbeitungsverzeichnis (Art. 30 DSGVO)
*
* 3 Tabs:
* 1. Verzeichnis (Uebersicht + "Aus Scope generieren")
* 2. Verarbeitung bearbeiten (Detail-Editor)
* 3. Export & Compliance
*/
import { useState, useEffect } from 'react'
import { useSDK } from '@/lib/sdk'
import StepHeader, { STEP_EXPLANATIONS } from '@/components/sdk/StepHeader/StepHeader'
import {
ART9_CATEGORIES,
createEmptyActivity,
createDefaultOrgHeader,
generateVVTId,
} from '@/lib/sdk/vvt-types'
import type { VVTActivity, VVTOrganizationHeader } from '@/lib/sdk/vvt-types'
import {
apiListActivities,
apiGetOrganization,
apiCreateActivity,
apiUpdateActivity,
apiDeleteActivity,
apiUpsertOrganization,
} from './_components/api'
import { TabVerzeichnis } from './_components/TabVerzeichnis'
import { TabEditor } from './_components/TabEditor'
import { TabExport } from './_components/TabExport'
type Tab = 'verzeichnis' | 'editor' | 'export'
export default function VVTPage() {
const { state } = useSDK()
const [tab, setTab] = useState<Tab>('verzeichnis')
const [activities, setActivities] = useState<VVTActivity[]>([])
const [orgHeader, setOrgHeader] = useState<VVTOrganizationHeader>(createDefaultOrgHeader())
const [editingId, setEditingId] = useState<string | null>(null)
const [filter, setFilter] = useState('all')
const [searchQuery, setSearchQuery] = useState('')
const [sortBy, setSortBy] = useState<'name' | 'date' | 'status'>('name')
const [isLoading, setIsLoading] = useState(true)
const [apiError, setApiError] = useState<string | null>(null)
// Load activities + org header from API
useEffect(() => {
async function loadFromApi() {
setIsLoading(true)
setApiError(null)
try {
const [acts, org] = await Promise.all([
apiListActivities(),
apiGetOrganization(),
])
setActivities(acts)
if (org) setOrgHeader(org)
} catch (err) {
setApiError('Fehler beim Laden der VVT-Daten. Bitte Verbindung prüfen.')
console.error('VVT API load error:', err)
} finally {
setIsLoading(false)
}
}
loadFromApi()
}, [])
// Computed stats
const activeCount = activities.filter(a => a.status === 'APPROVED').length
const draftCount = activities.filter(a => a.status === 'DRAFT').length
const thirdCountryCount = activities.filter(a => a.thirdCountryTransfers.length > 0).length
const art9Count = activities.filter(a => a.personalDataCategories.some(c => ART9_CATEGORIES.includes(c))).length
// Filtered & sorted activities
const filteredActivities = activities
.filter(a => {
const matchesFilter = filter === 'all' || a.status === filter || (filter === 'thirdcountry' && a.thirdCountryTransfers.length > 0) || (filter === 'art9' && a.personalDataCategories.some(c => ART9_CATEGORIES.includes(c)))
const matchesSearch = searchQuery === '' ||
a.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
a.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
a.vvtId.toLowerCase().includes(searchQuery.toLowerCase())
return matchesFilter && matchesSearch
})
.sort((a, b) => {
if (sortBy === 'name') return a.name.localeCompare(b.name)
if (sortBy === 'date') return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
return a.status.localeCompare(b.status)
})
const editingActivity = editingId ? activities.find(a => a.id === editingId) : null
const stepInfo = STEP_EXPLANATIONS['vvt']
// Tab buttons
const tabs: { id: Tab; label: string; count?: number }[] = [
{ id: 'verzeichnis', label: 'Verzeichnis', count: activities.length },
{ id: 'editor', label: 'Verarbeitung bearbeiten' },
{ id: 'export', label: 'Export & Compliance' },
]
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-purple-600"></div>
</div>
)
}
return (
<div className="space-y-6">
<StepHeader
stepId="vvt"
title={stepInfo.title}
description={stepInfo.description}
explanation={stepInfo.explanation}
tips={stepInfo.tips}
/>
{apiError && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4 text-sm text-red-700">
{apiError}
</div>
)}
{/* Tab Navigation */}
<div className="flex items-center gap-1 bg-gray-100 p-1 rounded-lg">
{tabs.map(t => (
<button
key={t.id}
onClick={() => setTab(t.id)}
className={`flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-md transition-colors ${
tab === t.id ? 'bg-white text-purple-700 shadow-sm' : 'text-gray-600 hover:text-gray-900'
}`}
>
{t.label}
{t.count !== undefined && (
<span className={`px-1.5 py-0.5 text-xs rounded-full ${tab === t.id ? 'bg-purple-100 text-purple-700' : 'bg-gray-200 text-gray-500'}`}>
{t.count}
</span>
)}
</button>
))}
</div>
{/* Tab Content */}
{tab === 'verzeichnis' && (
<TabVerzeichnis
activities={filteredActivities}
allActivities={activities}
activeCount={activeCount}
draftCount={draftCount}
thirdCountryCount={thirdCountryCount}
art9Count={art9Count}
filter={filter}
setFilter={setFilter}
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
sortBy={sortBy}
setSortBy={setSortBy}
scopeAnswers={state.complianceScope?.answers}
onEdit={(id) => { setEditingId(id); setTab('editor') }}
onNew={async () => {
const vvtId = generateVVTId(activities.map(a => a.vvtId))
const newAct = createEmptyActivity(vvtId)
try {
const created = await apiCreateActivity(newAct)
setActivities(prev => [...prev, created])
setEditingId(created.id)
setTab('editor')
} catch (err) {
setApiError('Fehler beim Anlegen der Verarbeitung.')
console.error(err)
}
}}
onDelete={async (id) => {
try {
await apiDeleteActivity(id)
setActivities(prev => prev.filter(a => a.id !== id))
} catch (err) {
setApiError('Fehler beim Löschen der Verarbeitung.')
console.error(err)
}
}}
onAdoptGenerated={async (newActivities) => {
const created: VVTActivity[] = []
for (const act of newActivities) {
try {
const saved = await apiCreateActivity(act)
created.push(saved)
} catch (err) {
console.error('Failed to create activity from scope:', err)
}
}
if (created.length > 0) setActivities(prev => [...prev, ...created])
}}
/>
)}
{tab === 'editor' && (
<TabEditor
activity={editingActivity}
activities={activities}
onSave={async (updated) => {
try {
const saved = await apiUpdateActivity(updated.id, updated)
setActivities(prev => prev.map(a => a.id === saved.id ? saved : a))
} catch (err) {
setApiError('Fehler beim Speichern der Verarbeitung.')
console.error(err)
}
}}
onBack={() => setTab('verzeichnis')}
onSelectActivity={(id) => setEditingId(id)}
/>
)}
{tab === 'export' && (
<TabExport
activities={activities}
orgHeader={orgHeader}
onUpdateOrgHeader={async (org) => {
try {
const saved = await apiUpsertOrganization(org)
setOrgHeader(saved)
} catch (err) {
setApiError('Fehler beim Speichern der Organisationsdaten.')
console.error(err)
}
}}
/>
)}
</div>
)
}