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>
258 lines
14 KiB
TypeScript
258 lines
14 KiB
TypeScript
'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>
|
|
)
|
|
}
|