All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 38s
CI / test-python-backend-compliance (push) Successful in 33s
CI / test-python-document-crawler (push) Successful in 24s
CI / test-python-dsms-gateway (push) Successful in 19s
- Migration 028: ai_use_case_modules JSONB + section_8_complete auf compliance_dsfas - Neues ai-use-case-types.ts: AIUseCaseModule Interface, 8 Typen, Art22Assessment, AI Act Risikoklassen, WP248-Kriterien, Privacy by Design, createEmptyModule() Helper - types.ts: Section 8 in DSFA_SECTIONS, ai_use_case_modules im DSFA Interface, section_8_complete in DSFASectionProgress - api.ts: addAIUseCaseModule, updateAIUseCaseModule, removeAIUseCaseModule - 5 neue UI-Komponenten: AIUseCaseTypeSelector, Art22AssessmentPanel, AIRiskCriteriaChecklist, AIUseCaseModuleEditor (7 Tabs), AIUseCaseSection - DSFASidebar: Section 8 Eintrag + calculateSectionProgress case 8 - ReviewScheduleSection: ai_use_case_module Trigger-Typ ergänzt - page.tsx: Section 8 Rendering + Weiter-Button auf activeSection < 8 + KI-Module Counter - scripts/ingest-dsfa-bundesland.sh: WP248 + alle 17 Behörden → bp_dsfa_corpus - Docs: dsfa.md Section 8 + RAG-Corpus, Developer Portal DSFA mit AI-Modul-Code-Beispielen Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
325 lines
14 KiB
TypeScript
325 lines
14 KiB
TypeScript
'use client'
|
|
|
|
import React, { useState } from 'react'
|
|
import { DSFA, DSFAReviewSchedule, DSFAReviewTrigger } from '@/lib/sdk/dsfa/types'
|
|
|
|
interface ReviewScheduleSectionProps {
|
|
dsfa: DSFA
|
|
onUpdate: (data: Record<string, unknown>) => Promise<void>
|
|
isSubmitting: boolean
|
|
}
|
|
|
|
const REVIEW_FREQUENCIES = [
|
|
{ value: 6, label: '6 Monate', description: 'Empfohlen bei hohem Risiko oder dynamischer Verarbeitung' },
|
|
{ value: 12, label: '12 Monate (jaehrlich)', description: 'Standardempfehlung fuer die meisten Verarbeitungen' },
|
|
{ value: 24, label: '24 Monate (alle 2 Jahre)', description: 'Bei stabilem, niedrigem Risiko' },
|
|
{ value: 36, label: '36 Monate (alle 3 Jahre)', description: 'Bei sehr stabiler Verarbeitung mit minimalem Risiko' },
|
|
]
|
|
|
|
const TRIGGER_TYPES = [
|
|
{ value: 'scheduled', label: 'Planmaessig', description: 'Regelmaessige Ueberpruefung nach Zeitplan', icon: '📅' },
|
|
{ value: 'risk_change', label: 'Risiko-Aenderung', description: 'Aenderung der Risikobewertung', icon: '⚠️' },
|
|
{ value: 'new_technology', label: 'Neue Technologie', description: 'Einfuehrung neuer technischer Systeme', icon: '🔧' },
|
|
{ value: 'new_purpose', label: 'Neuer Zweck', description: 'Aenderung oder Erweiterung des Verarbeitungszwecks', icon: '🎯' },
|
|
{ value: 'incident', label: 'Sicherheitsvorfall', description: 'Datenschutzvorfall oder Sicherheitsproblem', icon: '🚨' },
|
|
{ value: 'regulatory', label: 'Regulatorisch', description: 'Gesetzes- oder Behoerden-Aenderung', icon: '📜' },
|
|
{ value: 'ai_use_case_module', label: 'KI-Modul-Änderung', description: 'Änderung eines KI-Anwendungsfalls (Modell-Update, Datendrift)', icon: '🤖' },
|
|
{ value: 'other', label: 'Sonstiges', description: 'Anderer Ausloser', icon: '📋' },
|
|
]
|
|
|
|
export function ReviewScheduleSection({ dsfa, onUpdate, isSubmitting }: ReviewScheduleSectionProps) {
|
|
const existingSchedule = dsfa.review_schedule
|
|
const existingTriggers = dsfa.review_triggers || []
|
|
|
|
const [nextReviewDate, setNextReviewDate] = useState(existingSchedule?.next_review_date || '')
|
|
const [reviewFrequency, setReviewFrequency] = useState(existingSchedule?.review_frequency_months || 12)
|
|
const [reviewResponsible, setReviewResponsible] = useState(existingSchedule?.review_responsible || '')
|
|
const [triggers, setTriggers] = useState<DSFAReviewTrigger[]>(existingTriggers)
|
|
const [selectedTriggerTypes, setSelectedTriggerTypes] = useState<string[]>(
|
|
[...new Set(existingTriggers.map(t => t.trigger_type))]
|
|
)
|
|
|
|
// Calculate suggested next review date based on frequency
|
|
const suggestNextReviewDate = () => {
|
|
const date = new Date()
|
|
date.setMonth(date.getMonth() + reviewFrequency)
|
|
return date.toISOString().split('T')[0]
|
|
}
|
|
|
|
const toggleTriggerType = (type: string) => {
|
|
setSelectedTriggerTypes(prev =>
|
|
prev.includes(type)
|
|
? prev.filter(t => t !== type)
|
|
: [...prev, type]
|
|
)
|
|
}
|
|
|
|
const handleSave = async () => {
|
|
const schedule: DSFAReviewSchedule = {
|
|
next_review_date: nextReviewDate,
|
|
review_frequency_months: reviewFrequency,
|
|
last_review_date: existingSchedule?.last_review_date,
|
|
review_responsible: reviewResponsible,
|
|
}
|
|
|
|
// Generate triggers from selected types
|
|
const newTriggers: DSFAReviewTrigger[] = selectedTriggerTypes.map(type => {
|
|
const existingTrigger = triggers.find(t => t.trigger_type === type)
|
|
if (existingTrigger) return existingTrigger
|
|
|
|
const triggerInfo = TRIGGER_TYPES.find(t => t.value === type)
|
|
return {
|
|
id: crypto.randomUUID(),
|
|
trigger_type: type as DSFAReviewTrigger['trigger_type'],
|
|
description: triggerInfo?.description || '',
|
|
detected_at: new Date().toISOString(),
|
|
detected_by: 'system',
|
|
review_required: true,
|
|
review_completed: false,
|
|
changes_made: [],
|
|
}
|
|
})
|
|
|
|
await onUpdate({
|
|
review_schedule: schedule,
|
|
review_triggers: newTriggers,
|
|
})
|
|
}
|
|
|
|
// Check if review is overdue
|
|
const isOverdue = existingSchedule?.next_review_date
|
|
? new Date(existingSchedule.next_review_date) < new Date()
|
|
: false
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Info Banner */}
|
|
<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-500 mt-0.5" 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>
|
|
<p className="text-sm font-medium text-blue-800">Art. 35 Abs. 11 DSGVO</p>
|
|
<p className="text-sm text-blue-600 mt-1">
|
|
"Der Verantwortliche ueberprueft erforderlichenfalls, ob die Verarbeitung gemaess
|
|
der Datenschutz-Folgenabschaetzung durchgefuehrt wird, zumindest wenn hinsichtlich
|
|
des mit den Verarbeitungsvorgaengen verbundenen Risikos Aenderungen eingetreten sind."
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Overdue Warning */}
|
|
{isOverdue && (
|
|
<div className="bg-red-50 border-2 border-red-300 rounded-xl p-4">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 bg-red-100 rounded-full flex items-center justify-center">
|
|
<svg className="w-6 h-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<p className="font-semibold text-red-800">Review ueberfaellig!</p>
|
|
<p className="text-sm text-red-600">
|
|
Das naechste Review war fuer den{' '}
|
|
{new Date(existingSchedule!.next_review_date).toLocaleDateString('de-DE')} geplant.
|
|
Bitte aktualisieren Sie die DSFA.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Version Info */}
|
|
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
|
<h4 className="font-semibold text-gray-900 mb-4">Versionsinformation</h4>
|
|
|
|
<div className="grid grid-cols-2 gap-4 text-sm">
|
|
<div className="bg-gray-50 rounded-lg p-3">
|
|
<span className="text-gray-500">Aktuelle Version</span>
|
|
<p className="text-xl font-bold text-gray-900">{dsfa.version || 1}</p>
|
|
</div>
|
|
<div className="bg-gray-50 rounded-lg p-3">
|
|
<span className="text-gray-500">Erstellt am</span>
|
|
<p className="font-medium text-gray-900">
|
|
{new Date(dsfa.created_at).toLocaleDateString('de-DE')}
|
|
</p>
|
|
</div>
|
|
<div className="bg-gray-50 rounded-lg p-3">
|
|
<span className="text-gray-500">Letzte Aenderung</span>
|
|
<p className="font-medium text-gray-900">
|
|
{new Date(dsfa.updated_at).toLocaleDateString('de-DE')}
|
|
</p>
|
|
</div>
|
|
{existingSchedule?.last_review_date && (
|
|
<div className="bg-gray-50 rounded-lg p-3">
|
|
<span className="text-gray-500">Letztes Review</span>
|
|
<p className="font-medium text-gray-900">
|
|
{new Date(existingSchedule.last_review_date).toLocaleDateString('de-DE')}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Review Schedule */}
|
|
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
|
<h4 className="font-semibold text-gray-900 mb-4">Review-Planung</h4>
|
|
|
|
<div className="space-y-4">
|
|
{/* Review Frequency */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Review-Frequenz *
|
|
</label>
|
|
<div className="grid grid-cols-2 gap-3">
|
|
{REVIEW_FREQUENCIES.map((freq) => (
|
|
<label
|
|
key={freq.value}
|
|
className={`flex items-start gap-3 p-3 rounded-lg border cursor-pointer transition-all ${
|
|
reviewFrequency === freq.value
|
|
? 'bg-purple-50 border-purple-300 ring-1 ring-purple-300'
|
|
: 'bg-white border-gray-200 hover:bg-gray-50'
|
|
}`}
|
|
>
|
|
<input
|
|
type="radio"
|
|
name="reviewFrequency"
|
|
value={freq.value}
|
|
checked={reviewFrequency === freq.value}
|
|
onChange={(e) => setReviewFrequency(Number(e.target.value))}
|
|
className="mt-1 text-purple-600 focus:ring-purple-500"
|
|
/>
|
|
<div>
|
|
<span className="font-medium text-gray-900 text-sm">{freq.label}</span>
|
|
<p className="text-xs text-gray-500">{freq.description}</p>
|
|
</div>
|
|
</label>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Next Review Date */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Naechstes Review-Datum *
|
|
</label>
|
|
<div className="flex gap-2">
|
|
<input
|
|
type="date"
|
|
value={nextReviewDate}
|
|
onChange={(e) => setNextReviewDate(e.target.value)}
|
|
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
|
|
/>
|
|
<button
|
|
onClick={() => setNextReviewDate(suggestNextReviewDate())}
|
|
className="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors text-sm"
|
|
>
|
|
Vorschlag: +{reviewFrequency} Monate
|
|
</button>
|
|
</div>
|
|
{nextReviewDate && (
|
|
<p className="text-xs text-gray-500 mt-1">
|
|
Das naechste Review ist in{' '}
|
|
{Math.ceil((new Date(nextReviewDate).getTime() - Date.now()) / (1000 * 60 * 60 * 24))} Tagen faellig.
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Review Responsible */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Verantwortlich fuer Review *
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={reviewResponsible}
|
|
onChange={(e) => setReviewResponsible(e.target.value)}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
|
|
placeholder="Name oder Rolle der verantwortlichen Person/Abteilung..."
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Review Triggers */}
|
|
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
|
<h4 className="font-semibold text-gray-900 mb-2">Review-Ausloser definieren</h4>
|
|
<p className="text-sm text-gray-500 mb-4">
|
|
Waehlen Sie die Ereignisse aus, bei denen eine Ueberpruefung der DSFA ausgeloest werden soll.
|
|
</p>
|
|
|
|
<div className="space-y-3">
|
|
{TRIGGER_TYPES.map((trigger) => (
|
|
<label
|
|
key={trigger.value}
|
|
className={`flex items-start gap-3 p-4 rounded-lg border cursor-pointer transition-all ${
|
|
selectedTriggerTypes.includes(trigger.value)
|
|
? 'bg-green-50 border-green-300'
|
|
: 'bg-white border-gray-200 hover:bg-gray-50'
|
|
}`}
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
checked={selectedTriggerTypes.includes(trigger.value)}
|
|
onChange={() => toggleTriggerType(trigger.value)}
|
|
className="mt-1 rounded border-gray-300 text-green-600 focus:ring-green-500"
|
|
/>
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-lg">{trigger.icon}</span>
|
|
<span className="font-medium text-gray-900">{trigger.label}</span>
|
|
</div>
|
|
<p className="text-sm text-gray-500 mt-1">{trigger.description}</p>
|
|
</div>
|
|
</label>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Existing Pending Triggers */}
|
|
{triggers.filter(t => t.review_required && !t.review_completed).length > 0 && (
|
|
<div className="bg-orange-50 rounded-xl border border-orange-200 p-6">
|
|
<h4 className="font-semibold text-orange-900 mb-4">
|
|
Offene Review-Trigger ({triggers.filter(t => t.review_required && !t.review_completed).length})
|
|
</h4>
|
|
<div className="space-y-3">
|
|
{triggers
|
|
.filter(t => t.review_required && !t.review_completed)
|
|
.map((trigger) => {
|
|
const triggerInfo = TRIGGER_TYPES.find(t => t.value === trigger.trigger_type)
|
|
return (
|
|
<div
|
|
key={trigger.id}
|
|
className="bg-white rounded-lg border border-orange-200 p-3"
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-lg">{triggerInfo?.icon || '📋'}</span>
|
|
<span className="font-medium text-gray-900">{triggerInfo?.label || trigger.trigger_type}</span>
|
|
</div>
|
|
<p className="text-sm text-gray-600 mt-1">{trigger.description}</p>
|
|
<p className="text-xs text-gray-400 mt-2">
|
|
Erkannt am {new Date(trigger.detected_at).toLocaleDateString('de-DE')} von {trigger.detected_by}
|
|
</p>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Save Button */}
|
|
<div className="flex justify-end pt-4 border-t border-gray-200">
|
|
<button
|
|
onClick={handleSave}
|
|
disabled={isSubmitting || !nextReviewDate || !reviewResponsible}
|
|
className="px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50 transition-colors"
|
|
>
|
|
{isSubmitting ? 'Speichern...' : 'Review-Plan speichern'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|