This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/admin-v2/components/sdk/dsfa/ReviewScheduleSection.tsx
BreakPilot Dev 95e0a327c4 feat(dsfa): Add complete 8-section DSFA module with sidebar navigation
Implement DSFA optimization plan based on DSK Kurzpapier Nr. 5:
- Section 0: ThresholdAnalysisSection (WP248, Art. 35 Abs. 3, KI-Trigger)
- Section 5: StakeholderConsultationSection (Art. 35 Abs. 9)
- Section 6: Art36Warning for authority consultation (Art. 36)
- Section 7: ReviewScheduleSection (Art. 35 Abs. 11)
- DSFASidebar with progress tracking for all 8 sections
- Extended DSFASectionProgress for sections 0, 6, 7

Replaces tab navigation with sidebar layout (1/4 + 3/4 grid).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-09 11:50:04 +01:00

324 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: '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">
&quot;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.&quot;
</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>
)
}