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>
459 lines
21 KiB
TypeScript
459 lines
21 KiB
TypeScript
'use client'
|
|
|
|
import React, { useState } from 'react'
|
|
import { DSFA, DSFAStakeholderConsultation } from '@/lib/sdk/dsfa/types'
|
|
|
|
interface StakeholderConsultationSectionProps {
|
|
dsfa: DSFA
|
|
onUpdate: (data: Record<string, unknown>) => Promise<void>
|
|
isSubmitting: boolean
|
|
}
|
|
|
|
const STAKEHOLDER_TYPES = [
|
|
{ value: 'data_subjects', label: 'Betroffene Personen', description: 'Direkt von der Verarbeitung betroffene Personen' },
|
|
{ value: 'representatives', label: 'Vertreter der Betroffenen', description: 'Z.B. Verbraucherorganisationen, Patientenvertreter' },
|
|
{ value: 'works_council', label: 'Betriebsrat / Personalrat', description: 'Bei Arbeitnehmer-Datenverarbeitung' },
|
|
{ value: 'other', label: 'Sonstige Stakeholder', description: 'Andere relevante Interessengruppen' },
|
|
]
|
|
|
|
const CONSULTATION_METHODS = [
|
|
{ value: 'survey', label: 'Umfrage', description: 'Schriftliche oder Online-Befragung' },
|
|
{ value: 'interview', label: 'Interview', description: 'Persoenliche oder telefonische Gespraeche' },
|
|
{ value: 'workshop', label: 'Workshop', description: 'Gemeinsame Erarbeitung in Gruppensitzung' },
|
|
{ value: 'written', label: 'Schriftliche Stellungnahme', description: 'Formelle schriftliche Anfrage' },
|
|
{ value: 'other', label: 'Andere Methode', description: 'Sonstige Konsultationsform' },
|
|
]
|
|
|
|
export function StakeholderConsultationSection({ dsfa, onUpdate, isSubmitting }: StakeholderConsultationSectionProps) {
|
|
const [consultations, setConsultations] = useState<DSFAStakeholderConsultation[]>(
|
|
dsfa.stakeholder_consultations || []
|
|
)
|
|
const [notAppropriate, setNotAppropriate] = useState(
|
|
dsfa.stakeholder_consultation_not_appropriate || false
|
|
)
|
|
const [notAppropriateReason, setNotAppropriateReason] = useState(
|
|
dsfa.stakeholder_consultation_not_appropriate_reason || ''
|
|
)
|
|
const [showAddForm, setShowAddForm] = useState(false)
|
|
|
|
// New consultation form state
|
|
const [newConsultation, setNewConsultation] = useState<Partial<DSFAStakeholderConsultation>>({
|
|
stakeholder_type: 'data_subjects',
|
|
stakeholder_description: '',
|
|
consultation_method: 'survey',
|
|
consultation_date: '',
|
|
summary: '',
|
|
concerns_raised: [],
|
|
addressed_in_dsfa: false,
|
|
})
|
|
const [newConcern, setNewConcern] = useState('')
|
|
|
|
const addConsultation = () => {
|
|
if (!newConsultation.stakeholder_description || !newConsultation.summary) return
|
|
|
|
const consultation: DSFAStakeholderConsultation = {
|
|
id: crypto.randomUUID(),
|
|
stakeholder_type: newConsultation.stakeholder_type as DSFAStakeholderConsultation['stakeholder_type'],
|
|
stakeholder_description: newConsultation.stakeholder_description || '',
|
|
consultation_method: newConsultation.consultation_method as DSFAStakeholderConsultation['consultation_method'],
|
|
consultation_date: newConsultation.consultation_date || undefined,
|
|
summary: newConsultation.summary || '',
|
|
concerns_raised: newConsultation.concerns_raised || [],
|
|
addressed_in_dsfa: newConsultation.addressed_in_dsfa || false,
|
|
}
|
|
|
|
setConsultations([...consultations, consultation])
|
|
setNewConsultation({
|
|
stakeholder_type: 'data_subjects',
|
|
stakeholder_description: '',
|
|
consultation_method: 'survey',
|
|
consultation_date: '',
|
|
summary: '',
|
|
concerns_raised: [],
|
|
addressed_in_dsfa: false,
|
|
})
|
|
setShowAddForm(false)
|
|
}
|
|
|
|
const removeConsultation = (id: string) => {
|
|
setConsultations(consultations.filter(c => c.id !== id))
|
|
}
|
|
|
|
const addConcern = () => {
|
|
if (newConcern.trim()) {
|
|
setNewConsultation({
|
|
...newConsultation,
|
|
concerns_raised: [...(newConsultation.concerns_raised || []), newConcern.trim()],
|
|
})
|
|
setNewConcern('')
|
|
}
|
|
}
|
|
|
|
const removeConcern = (index: number) => {
|
|
setNewConsultation({
|
|
...newConsultation,
|
|
concerns_raised: (newConsultation.concerns_raised || []).filter((_, i) => i !== index),
|
|
})
|
|
}
|
|
|
|
const handleSave = async () => {
|
|
await onUpdate({
|
|
stakeholder_consultations: notAppropriate ? [] : consultations,
|
|
stakeholder_consultation_not_appropriate: notAppropriate,
|
|
stakeholder_consultation_not_appropriate_reason: notAppropriate ? notAppropriateReason : undefined,
|
|
})
|
|
}
|
|
|
|
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. 9 DSGVO</p>
|
|
<p className="text-sm text-blue-600 mt-1">
|
|
"Der Verantwortliche holt gegebenenfalls den Standpunkt der betroffenen Personen oder
|
|
ihrer Vertreter zu der beabsichtigten Verarbeitung ein, ohne dass dadurch der Schutz
|
|
gewerblicher oder oeffentlicher Interessen oder die Sicherheit der Verarbeitungsvorgaenge
|
|
beeintraechtigt wird."
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Not Appropriate Option */}
|
|
<div className={`p-4 rounded-xl border transition-all ${
|
|
notAppropriate
|
|
? 'bg-orange-50 border-orange-300'
|
|
: 'bg-white border-gray-200 hover:bg-gray-50'
|
|
}`}>
|
|
<label className="flex items-start gap-3 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={notAppropriate}
|
|
onChange={(e) => setNotAppropriate(e.target.checked)}
|
|
className="mt-1 rounded border-gray-300 text-orange-600 focus:ring-orange-500"
|
|
/>
|
|
<div>
|
|
<span className="font-medium text-gray-900">
|
|
Konsultation nicht angemessen
|
|
</span>
|
|
<p className="text-sm text-gray-500 mt-1">
|
|
Die Einholung des Standpunkts der Betroffenen ist in diesem Fall nicht angemessen
|
|
(z.B. bei Gefaehrdung der Sicherheit oder gewerblicher Interessen).
|
|
</p>
|
|
</div>
|
|
</label>
|
|
|
|
{notAppropriate && (
|
|
<div className="mt-4 ml-7">
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Begruendung *
|
|
</label>
|
|
<textarea
|
|
value={notAppropriateReason}
|
|
onChange={(e) => setNotAppropriateReason(e.target.value)}
|
|
className="w-full px-4 py-3 border border-orange-300 rounded-lg focus:ring-2 focus:ring-orange-500 bg-white"
|
|
rows={3}
|
|
placeholder="Begruenden Sie, warum eine Konsultation nicht angemessen ist..."
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Consultations List */}
|
|
{!notAppropriate && (
|
|
<>
|
|
<div>
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h3 className="text-lg font-semibold text-gray-900">
|
|
Durchgefuehrte Konsultationen ({consultations.length})
|
|
</h3>
|
|
<button
|
|
onClick={() => setShowAddForm(true)}
|
|
className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
|
|
>
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
|
</svg>
|
|
Konsultation hinzufuegen
|
|
</button>
|
|
</div>
|
|
|
|
{consultations.length === 0 && !showAddForm ? (
|
|
<div className="text-center py-8 text-gray-500 bg-gray-50 rounded-xl border border-dashed border-gray-300">
|
|
<svg className="w-12 h-12 mx-auto text-gray-300 mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
|
|
</svg>
|
|
<p>Noch keine Konsultationen dokumentiert</p>
|
|
<p className="text-sm mt-1">Fuegen Sie Ihre Stakeholder-Konsultationen hinzu.</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-4">
|
|
{consultations.map((consultation) => {
|
|
const typeInfo = STAKEHOLDER_TYPES.find(t => t.value === consultation.stakeholder_type)
|
|
const methodInfo = CONSULTATION_METHODS.find(m => m.value === consultation.consultation_method)
|
|
|
|
return (
|
|
<div
|
|
key={consultation.id}
|
|
className="bg-white rounded-xl border border-gray-200 p-4"
|
|
>
|
|
<div className="flex items-start justify-between mb-3">
|
|
<div>
|
|
<span className="inline-block px-2 py-1 bg-purple-100 text-purple-700 rounded text-xs font-medium mb-2">
|
|
{typeInfo?.label}
|
|
</span>
|
|
<h4 className="font-medium text-gray-900">
|
|
{consultation.stakeholder_description}
|
|
</h4>
|
|
</div>
|
|
<button
|
|
onClick={() => removeConsultation(consultation.id)}
|
|
className="p-1 text-gray-400 hover:text-red-500 transition-colors"
|
|
>
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4 text-sm mb-3">
|
|
<div>
|
|
<span className="text-gray-500">Methode:</span>
|
|
<span className="ml-2 text-gray-700">{methodInfo?.label}</span>
|
|
</div>
|
|
{consultation.consultation_date && (
|
|
<div>
|
|
<span className="text-gray-500">Datum:</span>
|
|
<span className="ml-2 text-gray-700">
|
|
{new Date(consultation.consultation_date).toLocaleDateString('de-DE')}
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="text-sm text-gray-600 mb-3">
|
|
<p className="font-medium text-gray-700">Zusammenfassung:</p>
|
|
<p>{consultation.summary}</p>
|
|
</div>
|
|
|
|
{consultation.concerns_raised.length > 0 && (
|
|
<div className="mb-3">
|
|
<p className="text-sm font-medium text-gray-700 mb-2">Geaeusserte Bedenken:</p>
|
|
<ul className="space-y-1">
|
|
{consultation.concerns_raised.map((concern, idx) => (
|
|
<li key={idx} className="flex items-start gap-2 text-sm text-gray-600">
|
|
<span className="text-orange-500 mt-0.5">-</span>
|
|
{concern}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex items-center gap-2">
|
|
{consultation.addressed_in_dsfa ? (
|
|
<span className="inline-flex items-center gap-1 px-2 py-1 bg-green-100 text-green-700 rounded text-xs">
|
|
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
In DSFA beruecksichtigt
|
|
</span>
|
|
) : (
|
|
<span className="inline-flex items-center gap-1 px-2 py-1 bg-gray-100 text-gray-600 rounded text-xs">
|
|
Noch nicht beruecksichtigt
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Add Consultation Form */}
|
|
{showAddForm && (
|
|
<div className="bg-gray-50 rounded-xl border border-gray-200 p-6">
|
|
<h4 className="font-semibold text-gray-900 mb-4">Neue Konsultation hinzufuegen</h4>
|
|
|
|
<div className="space-y-4">
|
|
{/* Stakeholder Type */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Stakeholder-Typ *
|
|
</label>
|
|
<div className="grid grid-cols-2 gap-3">
|
|
{STAKEHOLDER_TYPES.map((type) => (
|
|
<label
|
|
key={type.value}
|
|
className={`flex items-start gap-3 p-3 rounded-lg border cursor-pointer transition-all ${
|
|
newConsultation.stakeholder_type === type.value
|
|
? 'bg-purple-50 border-purple-300'
|
|
: 'bg-white border-gray-200 hover:bg-gray-50'
|
|
}`}
|
|
>
|
|
<input
|
|
type="radio"
|
|
name="stakeholderType"
|
|
value={type.value}
|
|
checked={newConsultation.stakeholder_type === type.value}
|
|
onChange={(e) => setNewConsultation({ ...newConsultation, stakeholder_type: e.target.value as DSFAStakeholderConsultation['stakeholder_type'] })}
|
|
className="mt-1 text-purple-600 focus:ring-purple-500"
|
|
/>
|
|
<div>
|
|
<span className="font-medium text-gray-900 text-sm">{type.label}</span>
|
|
<p className="text-xs text-gray-500">{type.description}</p>
|
|
</div>
|
|
</label>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Stakeholder Description */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Beschreibung der Stakeholder *
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={newConsultation.stakeholder_description}
|
|
onChange={(e) => setNewConsultation({ ...newConsultation, stakeholder_description: e.target.value })}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
|
|
placeholder="Z.B. Mitarbeiter der IT-Abteilung, Kunden des Online-Shops..."
|
|
/>
|
|
</div>
|
|
|
|
{/* Consultation Method */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Konsultationsmethode *
|
|
</label>
|
|
<select
|
|
value={newConsultation.consultation_method}
|
|
onChange={(e) => setNewConsultation({ ...newConsultation, consultation_method: e.target.value as DSFAStakeholderConsultation['consultation_method'] })}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
|
|
>
|
|
{CONSULTATION_METHODS.map((method) => (
|
|
<option key={method.value} value={method.value}>
|
|
{method.label} - {method.description}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
{/* Consultation Date */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Datum der Konsultation
|
|
</label>
|
|
<input
|
|
type="date"
|
|
value={newConsultation.consultation_date}
|
|
onChange={(e) => setNewConsultation({ ...newConsultation, consultation_date: e.target.value })}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
|
|
/>
|
|
</div>
|
|
|
|
{/* Summary */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Zusammenfassung der Ergebnisse *
|
|
</label>
|
|
<textarea
|
|
value={newConsultation.summary}
|
|
onChange={(e) => setNewConsultation({ ...newConsultation, summary: e.target.value })}
|
|
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
|
|
rows={3}
|
|
placeholder="Fassen Sie die wichtigsten Ergebnisse der Konsultation zusammen..."
|
|
/>
|
|
</div>
|
|
|
|
{/* Concerns */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Geaeusserte Bedenken
|
|
</label>
|
|
<div className="flex flex-wrap gap-2 mb-2">
|
|
{(newConsultation.concerns_raised || []).map((concern, idx) => (
|
|
<span key={idx} className="px-3 py-1 bg-orange-100 text-orange-700 rounded-full text-sm flex items-center gap-2">
|
|
{concern}
|
|
<button onClick={() => removeConcern(idx)} className="hover:text-orange-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={newConcern}
|
|
onChange={(e) => setNewConcern(e.target.value)}
|
|
onKeyPress={(e) => e.key === 'Enter' && (e.preventDefault(), addConcern())}
|
|
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
|
|
placeholder="Bedenken hinzufuegen..."
|
|
/>
|
|
<button
|
|
onClick={addConcern}
|
|
className="px-4 py-2 bg-orange-600 text-white rounded-lg hover:bg-orange-700"
|
|
>
|
|
+
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Addressed in DSFA */}
|
|
<label className="flex items-center gap-3 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={newConsultation.addressed_in_dsfa}
|
|
onChange={(e) => setNewConsultation({ ...newConsultation, addressed_in_dsfa: e.target.checked })}
|
|
className="rounded border-gray-300 text-green-600 focus:ring-green-500"
|
|
/>
|
|
<span className="text-sm text-gray-700">
|
|
Bedenken wurden in der DSFA beruecksichtigt
|
|
</span>
|
|
</label>
|
|
|
|
{/* Form Actions */}
|
|
<div className="flex gap-3 pt-4">
|
|
<button
|
|
onClick={() => setShowAddForm(false)}
|
|
className="px-4 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50"
|
|
>
|
|
Abbrechen
|
|
</button>
|
|
<button
|
|
onClick={addConsultation}
|
|
disabled={!newConsultation.stakeholder_description || !newConsultation.summary}
|
|
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50"
|
|
>
|
|
Konsultation hinzufuegen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{/* Save Button */}
|
|
<div className="flex justify-end pt-4 border-t border-gray-200">
|
|
<button
|
|
onClick={handleSave}
|
|
disabled={isSubmitting || (notAppropriate && !notAppropriateReason)}
|
|
className="px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50 transition-colors"
|
|
>
|
|
{isSubmitting ? 'Speichern...' : 'Abschnitt speichern'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|