fix(admin-v2): Restore complete admin-v2 application
The admin-v2 application was incomplete in the repository. This commit restores all missing components: - Admin pages (76 pages): dashboard, ai, compliance, dsgvo, education, infrastructure, communication, development, onboarding, rbac - SDK pages (45 pages): tom, dsfa, vvt, loeschfristen, einwilligungen, vendor-compliance, tom-generator, dsr, and more - Developer portal (25 pages): API docs, SDK guides, frameworks - All components, lib files, hooks, and types - Updated package.json with all dependencies The issue was caused by incomplete initial repository state - the full admin-v2 codebase existed in backend/admin-v2 and docs-src/admin-v2 but was never fully synced to the main admin-v2 directory. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
304
admin-v2/components/sdk/dsr/DSRDataExport.tsx
Normal file
304
admin-v2/components/sdk/dsr/DSRDataExport.tsx
Normal file
@@ -0,0 +1,304 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState } from 'react'
|
||||
import { DSRDataExport, DSRType } from '@/lib/sdk/dsr/types'
|
||||
|
||||
interface DSRDataExportProps {
|
||||
dsrId: string
|
||||
dsrType: DSRType // 'access' or 'portability'
|
||||
existingExport?: DSRDataExport
|
||||
onGenerate?: (format: 'json' | 'csv' | 'xml' | 'pdf') => Promise<void>
|
||||
onDownload?: () => Promise<void>
|
||||
isGenerating?: boolean
|
||||
}
|
||||
|
||||
const FORMAT_OPTIONS: {
|
||||
value: 'json' | 'csv' | 'xml' | 'pdf'
|
||||
label: string
|
||||
description: string
|
||||
icon: string
|
||||
recommended?: boolean
|
||||
}[] = [
|
||||
{
|
||||
value: 'json',
|
||||
label: 'JSON',
|
||||
description: 'Maschinenlesbar, ideal fuer technische Uebertragung',
|
||||
icon: 'M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4',
|
||||
recommended: true
|
||||
},
|
||||
{
|
||||
value: 'csv',
|
||||
label: 'CSV',
|
||||
description: 'Tabellen-Format, mit Excel oeffenbar',
|
||||
icon: 'M3 10h18M3 14h18m-9-4v8m-7 0h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z'
|
||||
},
|
||||
{
|
||||
value: 'xml',
|
||||
label: 'XML',
|
||||
description: 'Strukturiertes Format fuer System-Integration',
|
||||
icon: 'M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4'
|
||||
},
|
||||
{
|
||||
value: 'pdf',
|
||||
label: 'PDF',
|
||||
description: 'Menschenlesbar, ideal fuer direkte Zusendung',
|
||||
icon: 'M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z'
|
||||
}
|
||||
]
|
||||
|
||||
function formatFileSize(bytes: number): string {
|
||||
if (bytes < 1024) return `${bytes} B`
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
||||
}
|
||||
|
||||
function formatDate(dateString: string): string {
|
||||
return new Date(dateString).toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
export function DSRDataExportComponent({
|
||||
dsrId,
|
||||
dsrType,
|
||||
existingExport,
|
||||
onGenerate,
|
||||
onDownload,
|
||||
isGenerating = false
|
||||
}: DSRDataExportProps) {
|
||||
const [selectedFormat, setSelectedFormat] = useState<'json' | 'csv' | 'xml' | 'pdf'>(
|
||||
dsrType === 'portability' ? 'json' : 'pdf'
|
||||
)
|
||||
const [includeThirdParty, setIncludeThirdParty] = useState(true)
|
||||
const [transferRecipient, setTransferRecipient] = useState('')
|
||||
const [showTransferSection, setShowTransferSection] = useState(false)
|
||||
|
||||
const isPortability = dsrType === 'portability'
|
||||
|
||||
const handleGenerate = async () => {
|
||||
if (onGenerate) {
|
||||
await onGenerate(selectedFormat)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
{isPortability ? 'Datenexport (Art. 20)' : 'Datenauskunft (Art. 15)'}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
{isPortability
|
||||
? 'Exportieren Sie die Daten in einem maschinenlesbaren Format zur Uebertragung'
|
||||
: 'Erstellen Sie eine Uebersicht aller gespeicherten personenbezogenen Daten'
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Existing Export */}
|
||||
{existingExport && existingExport.generatedAt && (
|
||||
<div className="bg-green-50 border border-green-200 rounded-xl p-4">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<svg className="w-5 h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-green-800">Export vorhanden</div>
|
||||
<div className="text-sm text-green-700 mt-1 space-y-1">
|
||||
<div>Format: <span className="font-medium">{existingExport.format.toUpperCase()}</span></div>
|
||||
<div>Erstellt: <span className="font-medium">{formatDate(existingExport.generatedAt)}</span></div>
|
||||
{existingExport.fileName && (
|
||||
<div>Datei: <span className="font-medium">{existingExport.fileName}</span></div>
|
||||
)}
|
||||
{existingExport.fileSize && (
|
||||
<div>Groesse: <span className="font-medium">{formatFileSize(existingExport.fileSize)}</span></div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{onDownload && (
|
||||
<button
|
||||
onClick={onDownload}
|
||||
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors flex items-center gap-2"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
||||
</svg>
|
||||
Herunterladen
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Format Selection */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-3">
|
||||
Export-Format waehlen
|
||||
</label>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{FORMAT_OPTIONS.map(format => (
|
||||
<button
|
||||
key={format.value}
|
||||
onClick={() => setSelectedFormat(format.value)}
|
||||
className={`
|
||||
p-4 rounded-xl border-2 text-left transition-all
|
||||
${selectedFormat === format.value
|
||||
? 'border-purple-500 bg-purple-50'
|
||||
: 'border-gray-200 hover:border-gray-300 hover:bg-gray-50'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className={`
|
||||
w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0
|
||||
${selectedFormat === format.value ? 'bg-purple-100' : 'bg-gray-100'}
|
||||
`}>
|
||||
<svg
|
||||
className={`w-4 h-4 ${selectedFormat === format.value ? 'text-purple-600' : 'text-gray-500'}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={format.icon} />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`font-medium ${selectedFormat === format.value ? 'text-purple-700' : 'text-gray-900'}`}>
|
||||
{format.label}
|
||||
</span>
|
||||
{format.recommended && isPortability && (
|
||||
<span className="text-xs bg-purple-100 text-purple-700 px-1.5 py-0.5 rounded">
|
||||
Empfohlen
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 mt-0.5">
|
||||
{format.description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Options */}
|
||||
<div className="space-y-4">
|
||||
{/* Include Third-Party Data */}
|
||||
<label className="flex items-center gap-3 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={includeThirdParty}
|
||||
onChange={(e) => setIncludeThirdParty(e.target.checked)}
|
||||
className="w-5 h-5 rounded border-gray-300 text-purple-600 focus:ring-purple-500"
|
||||
/>
|
||||
<div>
|
||||
<div className="font-medium text-gray-900">Drittanbieter-Daten einbeziehen</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
Daten von externen Diensten (Google Analytics, etc.) mit exportieren
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
{/* Data Transfer (Art. 20 only) */}
|
||||
{isPortability && (
|
||||
<div className="pt-2">
|
||||
<label className="flex items-center gap-3 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showTransferSection}
|
||||
onChange={(e) => setShowTransferSection(e.target.checked)}
|
||||
className="w-5 h-5 rounded border-gray-300 text-purple-600 focus:ring-purple-500"
|
||||
/>
|
||||
<div>
|
||||
<div className="font-medium text-gray-900">Direkte Uebertragung an Dritten</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
Daten direkt an einen anderen Verantwortlichen uebertragen
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
{showTransferSection && (
|
||||
<div className="mt-3 ml-8 p-4 bg-gray-50 rounded-xl">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Empfaenger der Daten
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={transferRecipient}
|
||||
onChange={(e) => setTransferRecipient(e.target.value)}
|
||||
placeholder="Name des Unternehmens oder E-Mail-Adresse"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
||||
/>
|
||||
<p className="mt-2 text-xs text-gray-500">
|
||||
Die Daten werden an den angegebenen Empfaenger uebermittelt,
|
||||
sofern dies technisch machbar ist.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Info Box */}
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-xl p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<svg className="w-5 h-5 text-blue-600 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<div className="text-sm text-blue-700">
|
||||
<p className="font-medium">Enthaltene Datenkategorien</p>
|
||||
<ul className="mt-2 space-y-1 list-disc list-inside">
|
||||
<li>Stammdaten (Name, E-Mail, Adresse)</li>
|
||||
<li>Nutzungsdaten (Login-Historie, Aktivitaeten)</li>
|
||||
<li>Kommunikationsdaten (E-Mails, Support-Anfragen)</li>
|
||||
{includeThirdParty && <li>Tracking-Daten (Analytics, Cookies)</li>}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Generate Button */}
|
||||
{onGenerate && (
|
||||
<div className="flex items-center justify-end gap-3">
|
||||
<button
|
||||
onClick={handleGenerate}
|
||||
disabled={isGenerating}
|
||||
className={`
|
||||
px-6 py-2.5 rounded-lg font-medium transition-colors flex items-center gap-2
|
||||
${!isGenerating
|
||||
? 'bg-purple-600 text-white hover:bg-purple-700'
|
||||
: 'bg-gray-300 text-gray-500 cursor-not-allowed'
|
||||
}
|
||||
`}
|
||||
>
|
||||
{isGenerating ? (
|
||||
<>
|
||||
<svg className="animate-spin w-4 h-4" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
|
||||
</svg>
|
||||
Export wird erstellt...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
|
||||
</svg>
|
||||
{existingExport ? 'Neuen Export erstellen' : 'Export generieren'}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user