A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
368 lines
13 KiB
TypeScript
368 lines
13 KiB
TypeScript
'use client'
|
|
|
|
import { useMemo } from 'react'
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardDescription,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from '@/components/ui/card'
|
|
import {
|
|
Accordion,
|
|
AccordionContent,
|
|
AccordionItem,
|
|
AccordionTrigger,
|
|
} from '@/components/ui/accordion'
|
|
import { Badge } from '@/components/ui/badge'
|
|
import { Button } from '@/components/ui/button'
|
|
import { ScrollArea } from '@/components/ui/scroll-area'
|
|
import { Separator } from '@/components/ui/separator'
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipProvider,
|
|
TooltipTrigger,
|
|
} from '@/components/ui/tooltip'
|
|
import {
|
|
Database,
|
|
FileText,
|
|
List,
|
|
Table,
|
|
AlertTriangle,
|
|
Shield,
|
|
Clock,
|
|
Users,
|
|
Globe,
|
|
} from 'lucide-react'
|
|
import {
|
|
DataPoint,
|
|
DataPointCategory,
|
|
CATEGORY_METADATA,
|
|
RISK_LEVEL_STYLING,
|
|
RiskLevel,
|
|
} from '@/lib/sdk/einwilligungen/types'
|
|
|
|
interface DataPointsPreviewProps {
|
|
dataPoints: DataPoint[]
|
|
onInsertPlaceholder: (placeholder: string) => void
|
|
language?: 'de' | 'en'
|
|
}
|
|
|
|
/**
|
|
* Platzhalter-Definitionen mit Beschreibungen
|
|
*/
|
|
const PLACEHOLDERS = [
|
|
{
|
|
placeholder: '[DATENPUNKTE_TABLE]',
|
|
label: { de: 'Tabelle', en: 'Table' },
|
|
description: { de: 'Fügt eine Markdown-Tabelle mit allen Datenpunkten ein', en: 'Inserts a markdown table with all data points' },
|
|
icon: Table,
|
|
},
|
|
{
|
|
placeholder: '[DATENPUNKTE_LIST]',
|
|
label: { de: 'Liste', en: 'List' },
|
|
description: { de: 'Kommaseparierte Liste der Datenpunkt-Namen', en: 'Comma-separated list of data point names' },
|
|
icon: List,
|
|
},
|
|
{
|
|
placeholder: '[VERARBEITUNGSZWECKE]',
|
|
label: { de: 'Zwecke', en: 'Purposes' },
|
|
description: { de: 'Alle Verarbeitungszwecke (dedupliziert)', en: 'All processing purposes (deduplicated)' },
|
|
icon: FileText,
|
|
},
|
|
{
|
|
placeholder: '[RECHTSGRUNDLAGEN]',
|
|
label: { de: 'Rechtsgrundlagen', en: 'Legal Bases' },
|
|
description: { de: 'Verwendete DSGVO-Artikel', en: 'Used GDPR articles' },
|
|
icon: Shield,
|
|
},
|
|
{
|
|
placeholder: '[SPEICHERFRISTEN]',
|
|
label: { de: 'Speicherfristen', en: 'Retention' },
|
|
description: { de: 'Fristen gruppiert nach Kategorie', en: 'Periods grouped by category' },
|
|
icon: Clock,
|
|
},
|
|
{
|
|
placeholder: '[EMPFAENGER]',
|
|
label: { de: 'Empfänger', en: 'Recipients' },
|
|
description: { de: 'Liste aller Drittparteien', en: 'List of all third parties' },
|
|
icon: Users,
|
|
},
|
|
{
|
|
placeholder: '[BESONDERE_KATEGORIEN]',
|
|
label: { de: 'Art. 9 Abschnitt', en: 'Art. 9 Section' },
|
|
description: { de: 'DSGVO-konformer Abschnitt für sensible Daten', en: 'GDPR-compliant section for sensitive data' },
|
|
icon: AlertTriangle,
|
|
},
|
|
{
|
|
placeholder: '[DRITTLAND_TRANSFERS]',
|
|
label: { de: 'Drittländer', en: 'Third Countries' },
|
|
description: { de: 'Abschnitt zu Datenübermittlung außerhalb EU', en: 'Section about data transfers outside EU' },
|
|
icon: Globe,
|
|
},
|
|
]
|
|
|
|
/**
|
|
* Risiko-Badge Varianten mapping
|
|
*/
|
|
function getRiskBadgeVariant(riskLevel: RiskLevel): 'default' | 'secondary' | 'destructive' | 'outline' {
|
|
switch (riskLevel) {
|
|
case 'HIGH':
|
|
return 'destructive'
|
|
case 'MEDIUM':
|
|
return 'secondary'
|
|
case 'LOW':
|
|
default:
|
|
return 'outline'
|
|
}
|
|
}
|
|
|
|
/**
|
|
* DataPointsPreview Komponente
|
|
*
|
|
* Zeigt eine Vorschau der ausgewählten Einwilligungen-Datenpunkte im Dokumentengenerator.
|
|
* Ermöglicht das schnelle Einfügen von Platzhaltern.
|
|
*/
|
|
export function DataPointsPreview({
|
|
dataPoints,
|
|
onInsertPlaceholder,
|
|
language = 'de',
|
|
}: DataPointsPreviewProps) {
|
|
// Gruppiere Datenpunkte nach Kategorie
|
|
const byCategory = useMemo(() => {
|
|
return dataPoints.reduce((acc, dp) => {
|
|
if (!acc[dp.category]) {
|
|
acc[dp.category] = []
|
|
}
|
|
acc[dp.category].push(dp)
|
|
return acc
|
|
}, {} as Record<DataPointCategory, DataPoint[]>)
|
|
}, [dataPoints])
|
|
|
|
// Statistiken berechnen
|
|
const stats = useMemo(() => {
|
|
const riskCounts: Record<RiskLevel, number> = { LOW: 0, MEDIUM: 0, HIGH: 0 }
|
|
let specialCategoryCount = 0
|
|
let explicitConsentCount = 0
|
|
const recipients = new Set<string>()
|
|
|
|
dataPoints.forEach(dp => {
|
|
riskCounts[dp.riskLevel]++
|
|
if (dp.isSpecialCategory) specialCategoryCount++
|
|
if (dp.requiresExplicitConsent) explicitConsentCount++
|
|
dp.thirdPartyRecipients?.forEach(r => recipients.add(r))
|
|
})
|
|
|
|
return {
|
|
riskCounts,
|
|
specialCategoryCount,
|
|
explicitConsentCount,
|
|
recipientCount: recipients.size,
|
|
categoryCount: Object.keys(byCategory).length,
|
|
}
|
|
}, [dataPoints, byCategory])
|
|
|
|
// Sortierte Kategorien (nach Code)
|
|
const sortedCategories = useMemo(() => {
|
|
return Object.entries(byCategory).sort((a, b) => {
|
|
const codeA = CATEGORY_METADATA[a[0] as DataPointCategory]?.code || 'Z'
|
|
const codeB = CATEGORY_METADATA[b[0] as DataPointCategory]?.code || 'Z'
|
|
return codeA.localeCompare(codeB)
|
|
})
|
|
}, [byCategory])
|
|
|
|
if (dataPoints.length === 0) {
|
|
return (
|
|
<Card className="h-full">
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="text-base flex items-center gap-2">
|
|
<Database className="h-4 w-4" />
|
|
{language === 'de' ? 'Einwilligungen' : 'Consents'}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<p className="text-sm text-muted-foreground">
|
|
{language === 'de'
|
|
? 'Keine Datenpunkte ausgewählt. Wählen Sie Datenpunkte im Einwilligungs-Schritt aus, um sie hier zu sehen.'
|
|
: 'No data points selected. Select data points in the consent step to see them here.'}
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<Card className="h-full flex flex-col">
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="text-base flex items-center gap-2">
|
|
<Database className="h-4 w-4" />
|
|
{language === 'de' ? 'Einwilligungen' : 'Consents'}
|
|
</CardTitle>
|
|
<CardDescription>
|
|
{dataPoints.length} {language === 'de' ? 'Datenpunkte aus' : 'data points from'}{' '}
|
|
{stats.categoryCount} {language === 'de' ? 'Kategorien' : 'categories'}
|
|
</CardDescription>
|
|
</CardHeader>
|
|
|
|
<CardContent className="flex-1 flex flex-col gap-4 overflow-hidden">
|
|
{/* Statistik-Badges */}
|
|
<div className="flex flex-wrap gap-2">
|
|
{stats.riskCounts.HIGH > 0 && (
|
|
<Badge variant="destructive">
|
|
{stats.riskCounts.HIGH} {language === 'de' ? 'Hoch' : 'High'}
|
|
</Badge>
|
|
)}
|
|
{stats.riskCounts.MEDIUM > 0 && (
|
|
<Badge variant="secondary">
|
|
{stats.riskCounts.MEDIUM} {language === 'de' ? 'Mittel' : 'Medium'}
|
|
</Badge>
|
|
)}
|
|
{stats.riskCounts.LOW > 0 && (
|
|
<Badge variant="outline">
|
|
{stats.riskCounts.LOW} {language === 'de' ? 'Niedrig' : 'Low'}
|
|
</Badge>
|
|
)}
|
|
{stats.specialCategoryCount > 0 && (
|
|
<Badge variant="destructive" className="bg-orange-500 hover:bg-orange-600">
|
|
<AlertTriangle className="h-3 w-3 mr-1" />
|
|
{stats.specialCategoryCount} Art. 9
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
{/* Datenpunkte nach Kategorie */}
|
|
<ScrollArea className="flex-1 -mr-4 pr-4">
|
|
<Accordion type="multiple" className="w-full">
|
|
{sortedCategories.map(([category, points]) => {
|
|
const metadata = CATEGORY_METADATA[category as DataPointCategory]
|
|
if (!metadata) return null
|
|
|
|
return (
|
|
<AccordionItem key={category} value={category}>
|
|
<AccordionTrigger className="text-sm hover:no-underline">
|
|
<div className="flex items-center gap-2">
|
|
<span className="font-mono text-xs text-muted-foreground">
|
|
{metadata.code}
|
|
</span>
|
|
<span>
|
|
{language === 'de' ? metadata.name.de : metadata.name.en}
|
|
</span>
|
|
<Badge variant="secondary" className="ml-auto mr-2">
|
|
{points.length}
|
|
</Badge>
|
|
</div>
|
|
</AccordionTrigger>
|
|
<AccordionContent>
|
|
<ul className="space-y-1 pl-6">
|
|
{points.map(dp => (
|
|
<li
|
|
key={dp.id}
|
|
className="flex items-center justify-between text-sm py-1"
|
|
>
|
|
<span className="truncate max-w-[180px]">
|
|
{language === 'de' ? dp.name.de : dp.name.en}
|
|
</span>
|
|
<div className="flex items-center gap-1">
|
|
{dp.isSpecialCategory && (
|
|
<TooltipProvider>
|
|
<Tooltip>
|
|
<TooltipTrigger>
|
|
<AlertTriangle className="h-3 w-3 text-orange-500" />
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
{language === 'de'
|
|
? 'Besondere Kategorie (Art. 9 DSGVO)'
|
|
: 'Special Category (Art. 9 GDPR)'}
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
)}
|
|
<Badge
|
|
variant={getRiskBadgeVariant(dp.riskLevel)}
|
|
className="text-xs px-1.5 py-0"
|
|
>
|
|
{RISK_LEVEL_STYLING[dp.riskLevel].label[language]}
|
|
</Badge>
|
|
</div>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</AccordionContent>
|
|
</AccordionItem>
|
|
)
|
|
})}
|
|
</Accordion>
|
|
</ScrollArea>
|
|
|
|
<Separator />
|
|
|
|
{/* Schnell-Einfügen Buttons */}
|
|
<div className="space-y-2">
|
|
<p className="text-xs font-medium text-muted-foreground">
|
|
{language === 'de' ? 'Platzhalter einfügen:' : 'Insert placeholder:'}
|
|
</p>
|
|
<div className="flex flex-wrap gap-1.5">
|
|
<TooltipProvider>
|
|
{PLACEHOLDERS.slice(0, 4).map(({ placeholder, label, description, icon: Icon }) => (
|
|
<Tooltip key={placeholder}>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
className="h-7 text-xs px-2"
|
|
onClick={() => onInsertPlaceholder(placeholder)}
|
|
>
|
|
<Icon className="h-3 w-3 mr-1" />
|
|
{language === 'de' ? label.de : label.en}
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="bottom">
|
|
<p className="text-xs">
|
|
{language === 'de' ? description.de : description.en}
|
|
</p>
|
|
<p className="text-xs text-muted-foreground font-mono mt-1">
|
|
{placeholder}
|
|
</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
))}
|
|
</TooltipProvider>
|
|
</div>
|
|
<div className="flex flex-wrap gap-1.5">
|
|
<TooltipProvider>
|
|
{PLACEHOLDERS.slice(4).map(({ placeholder, label, description, icon: Icon }) => (
|
|
<Tooltip key={placeholder}>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
className="h-7 text-xs px-2"
|
|
onClick={() => onInsertPlaceholder(placeholder)}
|
|
>
|
|
<Icon className="h-3 w-3 mr-1" />
|
|
{language === 'de' ? label.de : label.en}
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="bottom">
|
|
<p className="text-xs">
|
|
{language === 'de' ? description.de : description.en}
|
|
</p>
|
|
<p className="text-xs text-muted-foreground font-mono mt-1">
|
|
{placeholder}
|
|
</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
))}
|
|
</TooltipProvider>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|
|
|
|
export default DataPointsPreview
|