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>
373 lines
17 KiB
TypeScript
373 lines
17 KiB
TypeScript
'use client'
|
|
|
|
import React, { useState } from 'react'
|
|
import {
|
|
DSFA,
|
|
DSFAConsultationRequirement,
|
|
DSFA_AUTHORITY_RESOURCES,
|
|
getFederalStateOptions,
|
|
getAuthorityResource,
|
|
} from '@/lib/sdk/dsfa/types'
|
|
|
|
interface Art36WarningProps {
|
|
dsfa: DSFA
|
|
onUpdate: (data: Record<string, unknown>) => Promise<void>
|
|
isSubmitting: boolean
|
|
}
|
|
|
|
export function Art36Warning({ dsfa, onUpdate, isSubmitting }: Art36WarningProps) {
|
|
const isHighResidualRisk = dsfa.residual_risk_level === 'high' || dsfa.residual_risk_level === 'very_high'
|
|
const consultationReq = dsfa.consultation_requirement
|
|
|
|
const [federalState, setFederalState] = useState(dsfa.federal_state || '')
|
|
const [authorityNotified, setAuthorityNotified] = useState(consultationReq?.authority_notified || false)
|
|
const [notificationDate, setNotificationDate] = useState(consultationReq?.notification_date || '')
|
|
const [waitingPeriodObserved, setWaitingPeriodObserved] = useState(consultationReq?.waiting_period_observed || false)
|
|
const [authorityResponse, setAuthorityResponse] = useState(consultationReq?.authority_response || '')
|
|
const [recommendations, setRecommendations] = useState<string[]>(consultationReq?.authority_recommendations || [])
|
|
const [newRecommendation, setNewRecommendation] = useState('')
|
|
|
|
const federalStateOptions = getFederalStateOptions()
|
|
const selectedAuthority = federalState ? getAuthorityResource(federalState) : null
|
|
|
|
const handleSave = async () => {
|
|
const requirement: DSFAConsultationRequirement = {
|
|
high_residual_risk: isHighResidualRisk,
|
|
consultation_required: isHighResidualRisk,
|
|
consultation_reason: isHighResidualRisk
|
|
? 'Trotz geplanter Massnahmen verbleibt ein hohes Restrisiko. Gem. Art. 36 Abs. 1 DSGVO ist vor der Verarbeitung die Aufsichtsbehoerde zu konsultieren.'
|
|
: undefined,
|
|
authority_notified: authorityNotified,
|
|
notification_date: notificationDate || undefined,
|
|
authority_response: authorityResponse || undefined,
|
|
authority_recommendations: recommendations.length > 0 ? recommendations : undefined,
|
|
waiting_period_observed: waitingPeriodObserved,
|
|
}
|
|
|
|
await onUpdate({
|
|
consultation_requirement: requirement,
|
|
federal_state: federalState,
|
|
authority_resource_id: federalState,
|
|
})
|
|
}
|
|
|
|
const addRecommendation = () => {
|
|
if (newRecommendation.trim()) {
|
|
setRecommendations([...recommendations, newRecommendation.trim()])
|
|
setNewRecommendation('')
|
|
}
|
|
}
|
|
|
|
const removeRecommendation = (index: number) => {
|
|
setRecommendations(recommendations.filter((_, i) => i !== index))
|
|
}
|
|
|
|
// Don't show if residual risk is not high
|
|
if (!isHighResidualRisk) {
|
|
return (
|
|
<div className="bg-green-50 border border-green-200 rounded-xl p-6">
|
|
<div className="flex items-start gap-4">
|
|
<div className="w-12 h-12 bg-green-100 rounded-full flex items-center justify-center flex-shrink-0">
|
|
<svg className="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<h3 className="font-semibold text-green-800">Keine Behoerdenkonsultation erforderlich</h3>
|
|
<p className="text-sm text-green-600 mt-1">
|
|
Das Restrisiko nach Umsetzung der geplanten Massnahmen ist nicht hoch.
|
|
Eine vorherige Konsultation der Aufsichtsbehoerde gem. Art. 36 DSGVO ist nicht erforderlich.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Warning Banner */}
|
|
<div className="bg-red-50 border-2 border-red-300 rounded-xl p-6">
|
|
<div className="flex items-start gap-4">
|
|
<div className="w-12 h-12 bg-red-100 rounded-full flex items-center justify-center flex-shrink-0">
|
|
<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 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<h3 className="text-lg font-semibold text-red-800">
|
|
Behoerdenkonsultation erforderlich (Art. 36 DSGVO)
|
|
</h3>
|
|
<p className="text-sm text-red-700 mt-2">
|
|
Das Restrisiko nach Umsetzung aller geplanten Massnahmen wurde als
|
|
<span className="font-bold"> {dsfa.residual_risk_level === 'very_high' ? 'SEHR HOCH' : 'HOCH'} </span>
|
|
eingestuft.
|
|
</p>
|
|
<p className="text-sm text-red-600 mt-2">
|
|
Gemaess Art. 36 Abs. 1 DSGVO muessen Sie <strong>vor Beginn der Verarbeitung</strong> die
|
|
zustaendige Aufsichtsbehoerde konsultieren. Die Behoerde hat eine Frist von 8 Wochen
|
|
zur Stellungnahme (Art. 36 Abs. 2 DSGVO).
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Federal State Selection */}
|
|
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
|
<h4 className="font-semibold text-gray-900 mb-4">Zustaendige Aufsichtsbehoerde</h4>
|
|
|
|
<div className="mb-4">
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Bundesland / Zustaendigkeit *
|
|
</label>
|
|
<select
|
|
value={federalState}
|
|
onChange={(e) => setFederalState(e.target.value)}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
|
|
>
|
|
<option value="">Bitte waehlen...</option>
|
|
{federalStateOptions.map((option) => (
|
|
<option key={option.value} value={option.value}>
|
|
{option.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
{/* Authority Details */}
|
|
{selectedAuthority && (
|
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
|
<h5 className="font-medium text-blue-900">{selectedAuthority.name}</h5>
|
|
<p className="text-sm text-blue-700 mt-1">({selectedAuthority.shortName})</p>
|
|
|
|
<div className="mt-4 flex flex-wrap gap-2">
|
|
<a
|
|
href={selectedAuthority.overviewUrl}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="inline-flex items-center gap-1 px-3 py-1.5 bg-blue-100 text-blue-700 rounded-lg text-sm hover:bg-blue-200 transition-colors"
|
|
>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
|
</svg>
|
|
DSFA-Informationen
|
|
</a>
|
|
|
|
{selectedAuthority.publicSectorListUrl && (
|
|
<a
|
|
href={selectedAuthority.publicSectorListUrl}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="inline-flex items-center gap-1 px-3 py-1.5 bg-green-100 text-green-700 rounded-lg text-sm hover:bg-green-200 transition-colors"
|
|
>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 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>
|
|
Muss-Liste (oeffentlich)
|
|
</a>
|
|
)}
|
|
|
|
{selectedAuthority.privateSectorListUrl && (
|
|
<a
|
|
href={selectedAuthority.privateSectorListUrl}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="inline-flex items-center gap-1 px-3 py-1.5 bg-orange-100 text-orange-700 rounded-lg text-sm hover:bg-orange-200 transition-colors"
|
|
>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 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>
|
|
Muss-Liste (nicht-oeffentlich)
|
|
</a>
|
|
)}
|
|
|
|
{selectedAuthority.templateUrl && (
|
|
<a
|
|
href={selectedAuthority.templateUrl}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="inline-flex items-center gap-1 px-3 py-1.5 bg-purple-100 text-purple-700 rounded-lg text-sm hover:bg-purple-200 transition-colors"
|
|
>
|
|
<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>
|
|
DSFA-Vorlage
|
|
</a>
|
|
)}
|
|
</div>
|
|
|
|
{selectedAuthority.additionalResources && selectedAuthority.additionalResources.length > 0 && (
|
|
<div className="mt-4 pt-4 border-t border-blue-200">
|
|
<p className="text-xs font-medium text-blue-800 mb-2">Weitere Ressourcen:</p>
|
|
<div className="flex flex-wrap gap-2">
|
|
{selectedAuthority.additionalResources.map((resource, idx) => (
|
|
<a
|
|
key={idx}
|
|
href={resource.url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-xs text-blue-600 hover:text-blue-800 underline"
|
|
>
|
|
{resource.title}
|
|
</a>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Consultation Documentation */}
|
|
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
|
<h4 className="font-semibold text-gray-900 mb-4">Konsultation dokumentieren</h4>
|
|
|
|
<div className="space-y-4">
|
|
{/* Authority Notified Checkbox */}
|
|
<label className={`flex items-start gap-3 p-4 rounded-lg border cursor-pointer transition-all ${
|
|
authorityNotified
|
|
? 'bg-green-50 border-green-300'
|
|
: 'bg-white border-gray-200 hover:bg-gray-50'
|
|
}`}>
|
|
<input
|
|
type="checkbox"
|
|
checked={authorityNotified}
|
|
onChange={(e) => setAuthorityNotified(e.target.checked)}
|
|
className="mt-1 rounded border-gray-300 text-green-600 focus:ring-green-500"
|
|
/>
|
|
<div>
|
|
<span className="font-medium text-gray-900">Aufsichtsbehoerde wurde konsultiert</span>
|
|
<p className="text-sm text-gray-500 mt-1">
|
|
Die DSFA wurde der zustaendigen Aufsichtsbehoerde vorgelegt.
|
|
</p>
|
|
</div>
|
|
</label>
|
|
|
|
{authorityNotified && (
|
|
<>
|
|
{/* Notification Date */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Datum der Konsultation
|
|
</label>
|
|
<input
|
|
type="date"
|
|
value={notificationDate}
|
|
onChange={(e) => setNotificationDate(e.target.value)}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
|
|
/>
|
|
</div>
|
|
|
|
{/* 8-Week Waiting Period */}
|
|
<label className={`flex items-start gap-3 p-4 rounded-lg border cursor-pointer transition-all ${
|
|
waitingPeriodObserved
|
|
? 'bg-green-50 border-green-300'
|
|
: 'bg-yellow-50 border-yellow-300'
|
|
}`}>
|
|
<input
|
|
type="checkbox"
|
|
checked={waitingPeriodObserved}
|
|
onChange={(e) => setWaitingPeriodObserved(e.target.checked)}
|
|
className="mt-1 rounded border-gray-300 text-green-600 focus:ring-green-500"
|
|
/>
|
|
<div>
|
|
<span className="font-medium text-gray-900">
|
|
8-Wochen-Frist eingehalten (Art. 36 Abs. 2 DSGVO)
|
|
</span>
|
|
<p className="text-sm text-gray-500 mt-1">
|
|
Die Aufsichtsbehoerde hat innerhalb von 8 Wochen nach Eingang der Konsultation
|
|
schriftlich Stellung genommen, oder die Frist ist abgelaufen.
|
|
</p>
|
|
{notificationDate && (
|
|
<p className="text-xs text-blue-600 mt-2">
|
|
8-Wochen-Frist endet am:{' '}
|
|
{new Date(new Date(notificationDate).getTime() + 8 * 7 * 24 * 60 * 60 * 1000).toLocaleDateString('de-DE')}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</label>
|
|
|
|
{/* Authority Response */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Stellungnahme / Entscheidung der Behoerde
|
|
</label>
|
|
<textarea
|
|
value={authorityResponse}
|
|
onChange={(e) => setAuthorityResponse(e.target.value)}
|
|
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
|
|
rows={4}
|
|
placeholder="Zusammenfassung der behoerdlichen Stellungnahme..."
|
|
/>
|
|
</div>
|
|
|
|
{/* Authority Recommendations */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Auflagen / Empfehlungen der Behoerde
|
|
</label>
|
|
<div className="flex flex-wrap gap-2 mb-2">
|
|
{recommendations.map((rec, idx) => (
|
|
<span key={idx} className="px-3 py-1 bg-blue-100 text-blue-700 rounded-full text-sm flex items-center gap-2">
|
|
{rec}
|
|
<button onClick={() => removeRecommendation(idx)} className="hover:text-blue-900">
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
</span>
|
|
))}
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<input
|
|
type="text"
|
|
value={newRecommendation}
|
|
onChange={(e) => setNewRecommendation(e.target.value)}
|
|
onKeyPress={(e) => e.key === 'Enter' && (e.preventDefault(), addRecommendation())}
|
|
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
|
|
placeholder="Auflage oder Empfehlung hinzufuegen..."
|
|
/>
|
|
<button
|
|
onClick={addRecommendation}
|
|
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
|
|
>
|
|
+
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Important Note */}
|
|
<div className="bg-yellow-50 border border-yellow-200 rounded-xl p-4">
|
|
<div className="flex items-start gap-3">
|
|
<svg className="w-5 h-5 text-yellow-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 className="text-sm text-yellow-800">
|
|
<p className="font-medium mb-1">Wichtiger Hinweis</p>
|
|
<p>
|
|
Die Verarbeitung darf erst beginnen, nachdem die Aufsichtsbehoerde konsultiert wurde
|
|
und entweder ihre Zustimmung erteilt hat oder die 8-Wochen-Frist abgelaufen ist.
|
|
Die Behoerde kann diese Frist um weitere 6 Wochen verlaengern.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Save Button */}
|
|
<div className="flex justify-end pt-4 border-t border-gray-200">
|
|
<button
|
|
onClick={handleSave}
|
|
disabled={isSubmitting || !federalState}
|
|
className="px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50 transition-colors"
|
|
>
|
|
{isSubmitting ? 'Speichern...' : 'Dokumentation speichern'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|