refactor(admin): split loeschfristen and vvt pages
Reduce both page.tsx files below the 500-LOC hard cap by extracting all inline tab components and API helpers into colocated _components/. - loeschfristen/page.tsx: 2720 → 467 LOC - vvt/page.tsx: 2297 → 256 LOC New files: LoeschkonzeptTab, loeschfristen/api, TabDokument, TabProcessor Updated: TabVerzeichnis (template picker + badge), vvt/api (template helpers) Fixed: VVTLinkSection wrong field name (linkedVVTActivityIds), VendorLinkSection added Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -370,18 +370,18 @@ export function VVTLinkSection({
|
||||
Verknuepfen Sie diese Loeschfrist mit einer Verarbeitungstaetigkeit aus Ihrem VVT.
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
{policy.linkedVvtIds && policy.linkedVvtIds.length > 0 && (
|
||||
{policy.linkedVVTActivityIds && policy.linkedVVTActivityIds.length > 0 && (
|
||||
<div className="mb-3">
|
||||
<label className="block text-xs font-medium text-gray-500 mb-1">Verknuepfte Taetigkeiten:</label>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{policy.linkedVvtIds.map((vvtId: string) => {
|
||||
{policy.linkedVVTActivityIds.map((vvtId: string) => {
|
||||
const activity = vvtActivities.find((a: any) => a.id === vvtId)
|
||||
return (
|
||||
<span key={vvtId} className="inline-flex items-center gap-1 bg-blue-100 text-blue-800 text-xs font-medium px-2 py-0.5 rounded-full">
|
||||
{activity?.name || vvtId}
|
||||
<button type="button"
|
||||
onClick={() => updatePolicy(pid, (p) => ({
|
||||
...p, linkedVvtIds: (p.linkedVvtIds || []).filter((id: string) => id !== vvtId),
|
||||
...p, linkedVVTActivityIds: (p.linkedVVTActivityIds || []).filter((id: string) => id !== vvtId),
|
||||
}))}
|
||||
className="text-blue-600 hover:text-blue-900">x</button>
|
||||
</span>
|
||||
@@ -393,15 +393,15 @@ export function VVTLinkSection({
|
||||
<select
|
||||
onChange={(e) => {
|
||||
const val = e.target.value
|
||||
if (val && !(policy.linkedVvtIds || []).includes(val)) {
|
||||
updatePolicy(pid, (p) => ({ ...p, linkedVvtIds: [...(p.linkedVvtIds || []), val] }))
|
||||
if (val && !(policy.linkedVVTActivityIds || []).includes(val)) {
|
||||
updatePolicy(pid, (p) => ({ ...p, linkedVVTActivityIds: [...(p.linkedVVTActivityIds || []), val] }))
|
||||
}
|
||||
e.target.value = ''
|
||||
}}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500">
|
||||
<option value="">Verarbeitungstaetigkeit verknuepfen...</option>
|
||||
{vvtActivities
|
||||
.filter((a: any) => !(policy.linkedVvtIds || []).includes(a.id))
|
||||
.filter((a: any) => !(policy.linkedVVTActivityIds || []).includes(a.id))
|
||||
.map((a: any) => (<option key={a.id} value={a.id}>{a.name || a.id}</option>))}
|
||||
</select>
|
||||
</div>
|
||||
@@ -415,6 +415,70 @@ export function VVTLinkSection({
|
||||
)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Sektion 5b: Auftragsverarbeiter-Verknuepfung
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function VendorLinkSection({
|
||||
policy, pid, vendorList, updatePolicy,
|
||||
}: {
|
||||
policy: LoeschfristPolicy; pid: string; vendorList: Array<{id: string; name: string}>
|
||||
updatePolicy: (id: string, updater: (p: LoeschfristPolicy) => LoeschfristPolicy) => void
|
||||
}) {
|
||||
return (
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6 space-y-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900">5b. Verknuepfte Auftragsverarbeiter</h3>
|
||||
{vendorList.length > 0 ? (
|
||||
<div>
|
||||
<p className="text-sm text-gray-500 mb-3">
|
||||
Verknuepfen Sie diese Loeschfrist mit relevanten Auftragsverarbeitern.
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
{policy.linkedVendorIds && policy.linkedVendorIds.length > 0 && (
|
||||
<div className="mb-3">
|
||||
<label className="block text-xs font-medium text-gray-500 mb-1">Verknuepfte Auftragsverarbeiter:</label>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{policy.linkedVendorIds.map((vendorId: string) => {
|
||||
const vendor = vendorList.find((v) => v.id === vendorId)
|
||||
return (
|
||||
<span key={vendorId} className="inline-flex items-center gap-1 bg-orange-100 text-orange-800 text-xs font-medium px-2 py-0.5 rounded-full">
|
||||
{vendor?.name || vendorId}
|
||||
<button type="button"
|
||||
onClick={() => updatePolicy(pid, (p) => ({
|
||||
...p, linkedVendorIds: (p.linkedVendorIds || []).filter((id: string) => id !== vendorId),
|
||||
}))}
|
||||
className="text-orange-600 hover:text-orange-900">x</button>
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<select
|
||||
onChange={(e) => {
|
||||
const val = e.target.value
|
||||
if (val && !(policy.linkedVendorIds || []).includes(val)) {
|
||||
updatePolicy(pid, (p) => ({ ...p, linkedVendorIds: [...(p.linkedVendorIds || []), val] }))
|
||||
}
|
||||
e.target.value = ''
|
||||
}}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500">
|
||||
<option value="">Auftragsverarbeiter verknuepfen...</option>
|
||||
{vendorList
|
||||
.filter((v) => !(policy.linkedVendorIds || []).includes(v.id))
|
||||
.map((v) => (<option key={v.id} value={v.id}>{v.name || v.id}</option>))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-gray-400">
|
||||
Keine Auftragsverarbeiter gefunden. Erstellen Sie zuerst Auftragsverarbeiter im Vendor-Compliance-Modul, um hier Verknuepfungen herstellen zu koennen.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Sektion 6: Review-Einstellungen
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
import { renderStatusBadge } from './UebersichtTab'
|
||||
import {
|
||||
DataObjectSection, DeletionLogicSection, StorageSection,
|
||||
ResponsibilitySection, VVTLinkSection, ReviewSection,
|
||||
ResponsibilitySection, VVTLinkSection, VendorLinkSection, ReviewSection,
|
||||
} from './EditorSections'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -19,6 +19,7 @@ interface EditorTabProps {
|
||||
editingId: string | null
|
||||
editingPolicy: LoeschfristPolicy | null
|
||||
vvtActivities: any[]
|
||||
vendorList: Array<{id: string; name: string}>
|
||||
saving: boolean
|
||||
setEditingId: (id: string | null) => void
|
||||
setTab: (tab: 'uebersicht' | 'editor' | 'generator' | 'export') => void
|
||||
@@ -79,7 +80,7 @@ function EditorNoSelection({
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function EditorForm({
|
||||
policy, vvtActivities, saving, setEditingId, setTab,
|
||||
policy, vvtActivities, vendorList, saving, setEditingId, setTab,
|
||||
updatePolicy, deletePolicy, addLegalHold, removeLegalHold,
|
||||
addStorageLocation, removeStorageLocation, handleSaveAndClose,
|
||||
}: Omit<EditorTabProps, 'policies' | 'editingId' | 'editingPolicy' | 'createNewPolicy'> & {
|
||||
@@ -127,6 +128,7 @@ function EditorForm({
|
||||
addStorageLocation={addStorageLocation} removeStorageLocation={removeStorageLocation} />
|
||||
<ResponsibilitySection policy={policy} set={set} />
|
||||
<VVTLinkSection policy={policy} pid={pid} vvtActivities={vvtActivities} updatePolicy={updatePolicy} />
|
||||
<VendorLinkSection policy={policy} pid={pid} vendorList={vendorList} updatePolicy={updatePolicy} />
|
||||
<ReviewSection policy={policy} set={set} />
|
||||
|
||||
{/* Action buttons */}
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import { LoeschfristPolicy } from '@/lib/sdk/loeschfristen-types'
|
||||
import { ComplianceCheckResult } from '@/lib/sdk/loeschfristen-compliance'
|
||||
import {
|
||||
buildLoeschkonzeptHtml,
|
||||
type LoeschkonzeptOrgHeader,
|
||||
type LoeschkonzeptRevision,
|
||||
} from '@/lib/sdk/loeschfristen-document'
|
||||
import { downloadFile } from '@/lib/sdk/loeschfristen-export'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface LoeschkonzeptTabProps {
|
||||
policies: LoeschfristPolicy[]
|
||||
orgHeader: LoeschkonzeptOrgHeader
|
||||
revisions: LoeschkonzeptRevision[]
|
||||
complianceResult: ComplianceCheckResult | null
|
||||
vvtActivities: any[]
|
||||
onOrgHeaderChange: (field: keyof LoeschkonzeptOrgHeader, value: string | string[]) => void
|
||||
onAddRevision: () => void
|
||||
onUpdateRevision: (index: number, field: keyof LoeschkonzeptRevision, value: string) => void
|
||||
onRemoveRevision: (index: number) => void
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Component
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function LoeschkonzeptTab({
|
||||
policies,
|
||||
orgHeader,
|
||||
revisions,
|
||||
complianceResult,
|
||||
vvtActivities,
|
||||
onOrgHeaderChange,
|
||||
onAddRevision,
|
||||
onUpdateRevision,
|
||||
onRemoveRevision,
|
||||
}: LoeschkonzeptTabProps) {
|
||||
const activePolicies = policies.filter(p => p.status !== 'ARCHIVED')
|
||||
|
||||
function handlePrintLoeschkonzept() {
|
||||
const htmlContent = buildLoeschkonzeptHtml(policies, orgHeader, vvtActivities, complianceResult, revisions)
|
||||
const printWindow = window.open('', '_blank')
|
||||
if (printWindow) {
|
||||
printWindow.document.write(htmlContent)
|
||||
printWindow.document.close()
|
||||
printWindow.focus()
|
||||
setTimeout(() => printWindow.print(), 300)
|
||||
}
|
||||
}
|
||||
|
||||
function handleDownloadLoeschkonzeptHtml() {
|
||||
const htmlContent = buildLoeschkonzeptHtml(policies, orgHeader, vvtActivities, complianceResult, revisions)
|
||||
downloadFile(htmlContent, `loeschkonzept-${new Date().toISOString().split('T')[0]}.html`, 'text/html;charset=utf-8')
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Action bar */}
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
Loeschkonzept (Art. 5/17/30 DSGVO)
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 mt-0.5">
|
||||
Druckfertiges Loeschkonzept mit Deckblatt, Loeschregeln, VVT-Verknuepfung und Compliance-Status.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleDownloadLoeschkonzeptHtml}
|
||||
disabled={activePolicies.length === 0}
|
||||
className="bg-gray-100 text-gray-700 hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed rounded-lg px-4 py-2 text-sm font-medium transition flex items-center gap-1.5"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 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>
|
||||
HTML herunterladen
|
||||
</button>
|
||||
<button
|
||||
onClick={handlePrintLoeschkonzept}
|
||||
disabled={activePolicies.length === 0}
|
||||
className="bg-purple-600 text-white hover:bg-purple-700 disabled:opacity-50 disabled:cursor-not-allowed rounded-lg px-4 py-2 text-sm font-medium transition flex items-center gap-1.5"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z" /></svg>
|
||||
Als PDF drucken
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{activePolicies.length === 0 && (
|
||||
<div className="bg-yellow-50 text-yellow-700 text-sm rounded-lg p-3 border border-yellow-200">
|
||||
Keine aktiven Policies vorhanden. Erstellen Sie mindestens eine Policy, um das Loeschkonzept zu generieren.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Org Header Form */}
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||
<h4 className="text-sm font-semibold text-gray-900 mb-4">Organisationsdaten (Deckblatt)</h4>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-600 mb-1">Organisation</label>
|
||||
<input type="text" value={orgHeader.organizationName}
|
||||
onChange={e => onOrgHeaderChange('organizationName', e.target.value)}
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
||||
placeholder="Name der Organisation" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-600 mb-1">Branche</label>
|
||||
<input type="text" value={orgHeader.industry}
|
||||
onChange={e => onOrgHeaderChange('industry', e.target.value)}
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
||||
placeholder="z.B. IT / Software" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-600 mb-1">Datenschutzbeauftragter</label>
|
||||
<input type="text" value={orgHeader.dpoName}
|
||||
onChange={e => onOrgHeaderChange('dpoName', e.target.value)}
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
||||
placeholder="Name des DSB" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-600 mb-1">DSB-Kontakt</label>
|
||||
<input type="text" value={orgHeader.dpoContact}
|
||||
onChange={e => onOrgHeaderChange('dpoContact', e.target.value)}
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
||||
placeholder="E-Mail oder Telefon" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-600 mb-1">Verantwortlicher (Art. 4 Nr. 7)</label>
|
||||
<input type="text" value={orgHeader.responsiblePerson}
|
||||
onChange={e => onOrgHeaderChange('responsiblePerson', e.target.value)}
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
||||
placeholder="Name des Verantwortlichen" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-600 mb-1">Mitarbeiter</label>
|
||||
<input type="text" value={orgHeader.employeeCount}
|
||||
onChange={e => onOrgHeaderChange('employeeCount', e.target.value)}
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
||||
placeholder="z.B. 50-249" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-600 mb-1">Version</label>
|
||||
<input type="text" value={orgHeader.loeschkonzeptVersion}
|
||||
onChange={e => onOrgHeaderChange('loeschkonzeptVersion', e.target.value)}
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
||||
placeholder="1.0" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-600 mb-1">Pruefintervall</label>
|
||||
<select value={orgHeader.reviewInterval}
|
||||
onChange={e => onOrgHeaderChange('reviewInterval', e.target.value)}
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500">
|
||||
<option value="Vierteljaehrlich">Vierteljaehrlich</option>
|
||||
<option value="Halbjaehrlich">Halbjaehrlich</option>
|
||||
<option value="Jaehrlich">Jaehrlich</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-600 mb-1">Letzte Pruefung</label>
|
||||
<input type="date" value={orgHeader.lastReviewDate}
|
||||
onChange={e => onOrgHeaderChange('lastReviewDate', e.target.value)}
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-600 mb-1">Naechste Pruefung</label>
|
||||
<input type="date" value={orgHeader.nextReviewDate}
|
||||
onChange={e => onOrgHeaderChange('nextReviewDate', e.target.value)}
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Revisions */}
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h4 className="text-sm font-semibold text-gray-900">Aenderungshistorie</h4>
|
||||
<button onClick={onAddRevision}
|
||||
className="text-xs bg-purple-50 text-purple-700 hover:bg-purple-100 rounded-lg px-3 py-1.5 font-medium transition">
|
||||
+ Revision hinzufuegen
|
||||
</button>
|
||||
</div>
|
||||
{revisions.length === 0 ? (
|
||||
<p className="text-sm text-gray-400">
|
||||
Noch keine Revisionen. Die Erstversion wird automatisch im Dokument eingefuegt.
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{revisions.map((rev, idx) => (
|
||||
<div key={idx} className="grid grid-cols-[80px_120px_1fr_1fr_32px] gap-2 items-start">
|
||||
<input type="text" value={rev.version}
|
||||
onChange={e => onUpdateRevision(idx, 'version', e.target.value)}
|
||||
className="rounded-lg border border-gray-300 px-2 py-1.5 text-xs" placeholder="1.1" />
|
||||
<input type="date" value={rev.date}
|
||||
onChange={e => onUpdateRevision(idx, 'date', e.target.value)}
|
||||
className="rounded-lg border border-gray-300 px-2 py-1.5 text-xs" />
|
||||
<input type="text" value={rev.author}
|
||||
onChange={e => onUpdateRevision(idx, 'author', e.target.value)}
|
||||
className="rounded-lg border border-gray-300 px-2 py-1.5 text-xs" placeholder="Autor" />
|
||||
<input type="text" value={rev.changes}
|
||||
onChange={e => onUpdateRevision(idx, 'changes', e.target.value)}
|
||||
className="rounded-lg border border-gray-300 px-2 py-1.5 text-xs" placeholder="Beschreibung der Aenderungen" />
|
||||
<button onClick={() => onRemoveRevision(idx)}
|
||||
className="text-red-400 hover:text-red-600 p-1" title="Revision entfernen">
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Document Preview */}
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||
<h4 className="text-sm font-semibold text-gray-900 mb-4">Dokument-Vorschau</h4>
|
||||
<div className="bg-gray-50 rounded-lg p-6 border border-gray-200">
|
||||
<div className="text-center mb-6">
|
||||
<div className="text-2xl font-bold text-purple-700 mb-1">Loeschkonzept</div>
|
||||
<div className="text-sm text-purple-500 mb-4">gemaess Art. 5/17/30 DSGVO</div>
|
||||
<div className="text-sm text-gray-600">
|
||||
{orgHeader.organizationName || <span className="text-gray-400 italic">Organisation nicht angegeben</span>}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 mt-2">
|
||||
Version {orgHeader.loeschkonzeptVersion} | {new Date().toLocaleDateString('de-DE')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-t border-gray-200 pt-4">
|
||||
<div className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-2">12 Sektionen</div>
|
||||
<div className="grid grid-cols-2 gap-1 text-xs text-gray-600">
|
||||
<div>1. Ziel und Zweck</div><div>7. Auftragsverarbeiter</div>
|
||||
<div>2. Geltungsbereich</div><div>8. Legal Hold Verfahren</div>
|
||||
<div>3. Grundprinzipien</div><div>9. Verantwortlichkeiten</div>
|
||||
<div>4. Loeschregeln-Uebersicht</div><div>10. Pruef-/Revisionszyklus</div>
|
||||
<div>5. Detaillierte Loeschregeln</div><div>11. Compliance-Status</div>
|
||||
<div>6. VVT-Verknuepfung</div><div>12. Aenderungshistorie</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-t border-gray-200 pt-4 mt-4 flex gap-6 text-xs text-gray-500">
|
||||
<span><strong className="text-gray-700">{activePolicies.length}</strong> Loeschregeln</span>
|
||||
<span><strong className="text-gray-700">{policies.filter(p => p.linkedVVTActivityIds.length > 0).length}</strong> VVT-Verknuepfungen</span>
|
||||
<span><strong className="text-gray-700">{policies.filter(p => p.linkedVendorIds.length > 0).length}</strong> Vendor-Verknuepfungen</span>
|
||||
<span><strong className="text-gray-700">{revisions.length}</strong> Revisionen</span>
|
||||
{complianceResult && (
|
||||
<span>Compliance-Score: <strong className={complianceResult.score >= 75 ? 'text-green-600' : complianceResult.score >= 50 ? 'text-yellow-600' : 'text-red-600'}>{complianceResult.score}/100</strong></span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
74
admin-compliance/app/sdk/loeschfristen/_components/api.ts
Normal file
74
admin-compliance/app/sdk/loeschfristen/_components/api.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { LoeschfristPolicy, createEmptyPolicy } from '@/lib/sdk/loeschfristen-types'
|
||||
|
||||
export const LOESCHFRISTEN_API = '/api/sdk/v1/compliance/loeschfristen'
|
||||
|
||||
export function apiToPolicy(raw: any): LoeschfristPolicy {
|
||||
const base = createEmptyPolicy()
|
||||
return {
|
||||
...base,
|
||||
id: raw.id,
|
||||
policyId: raw.policy_id || base.policyId,
|
||||
dataObjectName: raw.data_object_name || '',
|
||||
description: raw.description || '',
|
||||
affectedGroups: raw.affected_groups || [],
|
||||
dataCategories: raw.data_categories || [],
|
||||
primaryPurpose: raw.primary_purpose || '',
|
||||
deletionTrigger: raw.deletion_trigger || 'PURPOSE_END',
|
||||
retentionDriver: raw.retention_driver || null,
|
||||
retentionDriverDetail: raw.retention_driver_detail || '',
|
||||
retentionDuration: raw.retention_duration ?? null,
|
||||
retentionUnit: raw.retention_unit || null,
|
||||
retentionDescription: raw.retention_description || '',
|
||||
startEvent: raw.start_event || '',
|
||||
hasActiveLegalHold: raw.has_active_legal_hold || false,
|
||||
legalHolds: raw.legal_holds || [],
|
||||
storageLocations: raw.storage_locations || [],
|
||||
deletionMethod: raw.deletion_method || 'MANUAL_REVIEW_DELETE',
|
||||
deletionMethodDetail: raw.deletion_method_detail || '',
|
||||
responsibleRole: raw.responsible_role || '',
|
||||
responsiblePerson: raw.responsible_person || '',
|
||||
releaseProcess: raw.release_process || '',
|
||||
linkedVVTActivityIds: raw.linked_vvt_activity_ids || [],
|
||||
linkedVendorIds: raw.linked_vendor_ids || [],
|
||||
status: raw.status || 'DRAFT',
|
||||
lastReviewDate: raw.last_review_date || base.lastReviewDate,
|
||||
nextReviewDate: raw.next_review_date || base.nextReviewDate,
|
||||
reviewInterval: raw.review_interval || 'ANNUAL',
|
||||
tags: raw.tags || [],
|
||||
createdAt: raw.created_at || base.createdAt,
|
||||
updatedAt: raw.updated_at || base.updatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
export function policyToPayload(p: LoeschfristPolicy): any {
|
||||
return {
|
||||
policy_id: p.policyId,
|
||||
data_object_name: p.dataObjectName,
|
||||
description: p.description,
|
||||
affected_groups: p.affectedGroups,
|
||||
data_categories: p.dataCategories,
|
||||
primary_purpose: p.primaryPurpose,
|
||||
deletion_trigger: p.deletionTrigger,
|
||||
retention_driver: p.retentionDriver || null,
|
||||
retention_driver_detail: p.retentionDriverDetail,
|
||||
retention_duration: p.retentionDuration || null,
|
||||
retention_unit: p.retentionUnit || null,
|
||||
retention_description: p.retentionDescription,
|
||||
start_event: p.startEvent,
|
||||
has_active_legal_hold: p.hasActiveLegalHold,
|
||||
legal_holds: p.legalHolds,
|
||||
storage_locations: p.storageLocations,
|
||||
deletion_method: p.deletionMethod,
|
||||
deletion_method_detail: p.deletionMethodDetail,
|
||||
responsible_role: p.responsibleRole,
|
||||
responsible_person: p.responsiblePerson,
|
||||
release_process: p.releaseProcess,
|
||||
linked_vvt_activity_ids: p.linkedVVTActivityIds,
|
||||
linked_vendor_ids: p.linkedVendorIds,
|
||||
status: p.status,
|
||||
last_review_date: p.lastReviewDate || null,
|
||||
next_review_date: p.nextReviewDate || null,
|
||||
review_interval: p.reviewInterval,
|
||||
tags: p.tags,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user