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/Art36Warning.tsx
BreakPilot Dev aa0fbc0e64 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

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>
)
}