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/StakeholderConsultationSection.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

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