Initial commit: breakpilot-compliance - Compliance SDK Platform
Services: Admin-Compliance, Backend-Compliance, AI-Compliance-SDK, Consent-SDK, Developer-Portal, PCA-Platform, DSMS Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,474 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import {
|
||||
DSFA,
|
||||
WP248_CRITERIA,
|
||||
ART35_ABS3_CASES,
|
||||
AI_DSFA_TRIGGERS,
|
||||
checkDSFARequiredByWP248,
|
||||
DSFAThresholdAnalysis,
|
||||
} from '@/lib/sdk/dsfa/types'
|
||||
|
||||
interface ThresholdAnalysisSectionProps {
|
||||
dsfa: DSFA
|
||||
onUpdate: (data: Record<string, unknown>) => Promise<void>
|
||||
isSubmitting: boolean
|
||||
}
|
||||
|
||||
export function ThresholdAnalysisSection({ dsfa, onUpdate, isSubmitting }: ThresholdAnalysisSectionProps) {
|
||||
// Initialize state from existing data
|
||||
const existingAnalysis = dsfa.threshold_analysis
|
||||
|
||||
const [wp248Selected, setWp248Selected] = useState<string[]>(
|
||||
dsfa.wp248_criteria_met ||
|
||||
existingAnalysis?.criteria_assessment?.filter(c => c.applies).map(c => c.criterion_id) ||
|
||||
[]
|
||||
)
|
||||
const [art35Selected, setArt35Selected] = useState<string[]>(
|
||||
dsfa.art35_abs3_triggered ||
|
||||
existingAnalysis?.art35_abs3_assessment?.filter(c => c.applies).map(c => c.case_id) ||
|
||||
[]
|
||||
)
|
||||
const [aiTriggersSelected, setAiTriggersSelected] = useState<string[]>(
|
||||
dsfa.ai_trigger_ids || []
|
||||
)
|
||||
const [dsfaRequired, setDsfaRequired] = useState<boolean | null>(
|
||||
existingAnalysis?.dsfa_required ?? null
|
||||
)
|
||||
const [justification, setJustification] = useState(
|
||||
existingAnalysis?.decision_justification || ''
|
||||
)
|
||||
|
||||
// Calculate recommendation based on selections
|
||||
const wp248Result = checkDSFARequiredByWP248(wp248Selected)
|
||||
const hasArt35Trigger = art35Selected.length > 0
|
||||
const hasAITrigger = aiTriggersSelected.length > 0
|
||||
|
||||
const recommendation = wp248Result.required || hasArt35Trigger || hasAITrigger
|
||||
? 'required'
|
||||
: wp248Selected.length === 1
|
||||
? 'possible'
|
||||
: 'not_required'
|
||||
|
||||
// Auto-generate justification when selections change
|
||||
useEffect(() => {
|
||||
if (dsfaRequired === null && !justification) {
|
||||
const parts: string[] = []
|
||||
|
||||
if (wp248Selected.length > 0) {
|
||||
const criteriaNames = wp248Selected.map(id =>
|
||||
WP248_CRITERIA.find(c => c.id === id)?.code
|
||||
).filter(Boolean).join(', ')
|
||||
parts.push(`${wp248Selected.length} WP248-Kriterien erfuellt (${criteriaNames})`)
|
||||
}
|
||||
|
||||
if (art35Selected.length > 0) {
|
||||
parts.push(`Art. 35 Abs. 3 Regelbeispiel${art35Selected.length > 1 ? 'e' : ''} erfuellt`)
|
||||
}
|
||||
|
||||
if (aiTriggersSelected.length > 0) {
|
||||
parts.push(`${aiTriggersSelected.length} KI-spezifische${aiTriggersSelected.length > 1 ? '' : 'r'} Trigger erfuellt`)
|
||||
}
|
||||
|
||||
if (parts.length > 0) {
|
||||
setJustification(parts.join('. ') + '.')
|
||||
}
|
||||
}
|
||||
}, [wp248Selected, art35Selected, aiTriggersSelected, dsfaRequired, justification])
|
||||
|
||||
const toggleWp248 = (criterionId: string) => {
|
||||
setWp248Selected(prev =>
|
||||
prev.includes(criterionId)
|
||||
? prev.filter(id => id !== criterionId)
|
||||
: [...prev, criterionId]
|
||||
)
|
||||
}
|
||||
|
||||
const toggleArt35 = (caseId: string) => {
|
||||
setArt35Selected(prev =>
|
||||
prev.includes(caseId)
|
||||
? prev.filter(id => id !== caseId)
|
||||
: [...prev, caseId]
|
||||
)
|
||||
}
|
||||
|
||||
const toggleAITrigger = (triggerId: string) => {
|
||||
setAiTriggersSelected(prev =>
|
||||
prev.includes(triggerId)
|
||||
? prev.filter(id => id !== triggerId)
|
||||
: [...prev, triggerId]
|
||||
)
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
const thresholdAnalysis: DSFAThresholdAnalysis = {
|
||||
id: existingAnalysis?.id || crypto.randomUUID(),
|
||||
dsfa_id: dsfa.id,
|
||||
performed_at: new Date().toISOString(),
|
||||
performed_by: 'current_user', // Would come from auth context
|
||||
criteria_assessment: WP248_CRITERIA.map(c => ({
|
||||
criterion_id: c.id,
|
||||
applies: wp248Selected.includes(c.id),
|
||||
justification: '',
|
||||
})),
|
||||
art35_abs3_assessment: ART35_ABS3_CASES.map(c => ({
|
||||
case_id: c.id,
|
||||
applies: art35Selected.includes(c.id),
|
||||
justification: '',
|
||||
})),
|
||||
dsfa_required: dsfaRequired ?? recommendation === 'required',
|
||||
decision_justification: justification,
|
||||
documented: true,
|
||||
}
|
||||
|
||||
await onUpdate({
|
||||
threshold_analysis: thresholdAnalysis,
|
||||
wp248_criteria_met: wp248Selected,
|
||||
art35_abs3_triggered: art35Selected,
|
||||
ai_trigger_ids: aiTriggersSelected,
|
||||
involves_ai: aiTriggersSelected.length > 0,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* Step 1: WP248 Criteria */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||
Schritt 1: WP248 Kriterien pruefen
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 mb-4">
|
||||
Pruefen Sie, welche der 9 Kriterien der Artikel-29-Datenschutzgruppe auf Ihre Verarbeitung zutreffen.
|
||||
Bei 2 oder mehr erfuellten Kriterien ist eine DSFA in den meisten Faellen erforderlich.
|
||||
</p>
|
||||
|
||||
<div className="space-y-3">
|
||||
{WP248_CRITERIA.map((criterion) => (
|
||||
<label
|
||||
key={criterion.id}
|
||||
className={`flex items-start gap-3 p-4 rounded-xl border cursor-pointer transition-all ${
|
||||
wp248Selected.includes(criterion.id)
|
||||
? 'bg-purple-50 border-purple-300 ring-1 ring-purple-300'
|
||||
: 'bg-white border-gray-200 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={wp248Selected.includes(criterion.id)}
|
||||
onChange={() => toggleWp248(criterion.id)}
|
||||
className="mt-1 rounded border-gray-300 text-purple-600 focus:ring-purple-500"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-gray-900">{criterion.code}:</span>
|
||||
<span className="text-gray-900">{criterion.title}</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 mt-1">{criterion.description}</p>
|
||||
{criterion.examples.length > 0 && (
|
||||
<p className="text-xs text-gray-400 mt-1">
|
||||
Beispiele: {criterion.examples.join(', ')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* WP248 Summary */}
|
||||
<div className={`mt-4 p-4 rounded-xl border ${
|
||||
wp248Selected.length >= 2
|
||||
? 'bg-orange-50 border-orange-200'
|
||||
: wp248Selected.length === 1
|
||||
? 'bg-yellow-50 border-yellow-200'
|
||||
: 'bg-green-50 border-green-200'
|
||||
}`}>
|
||||
<div className="flex items-center gap-2">
|
||||
{wp248Selected.length >= 2 ? (
|
||||
<svg className="w-5 h-5 text-orange-500" 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>
|
||||
) : (
|
||||
<svg className="w-5 h-5 text-green-500" 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>
|
||||
)}
|
||||
<span className="font-medium">
|
||||
{wp248Selected.length} von 9 Kriterien erfuellt
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm mt-1 text-gray-600">
|
||||
{wp248Result.reason}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Annex-Trigger: Empfehlung bei >= 2 WP248 Kriterien */}
|
||||
{wp248Selected.length >= 2 && (
|
||||
<div className="mt-4 p-4 rounded-xl border bg-indigo-50 border-indigo-200">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-8 h-8 rounded-full bg-indigo-100 flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<svg className="w-4 h-4 text-indigo-600" 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>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold text-indigo-800 text-sm">Annex mit separater Risikobewertung empfohlen</p>
|
||||
<p className="text-sm text-indigo-700 mt-1">
|
||||
Bei {wp248Selected.length} erfuellten WP248-Kriterien wird ein Annex mit detaillierter Risikobewertung empfohlen.
|
||||
</p>
|
||||
<div className="mt-2">
|
||||
<p className="text-xs font-medium text-indigo-700 mb-1">Vorgeschlagene Annex-Scopes basierend auf Ihren Kriterien:</p>
|
||||
<ul className="text-xs text-indigo-600 space-y-1">
|
||||
{wp248Selected.includes('scoring_profiling') && (
|
||||
<li>- Annex: Profiling & Scoring — Detailanalyse der Bewertungslogik</li>
|
||||
)}
|
||||
{wp248Selected.includes('automated_decision') && (
|
||||
<li>- Annex: Automatisierte Einzelentscheidung — Art. 22 Pruefung</li>
|
||||
)}
|
||||
{wp248Selected.includes('systematic_monitoring') && (
|
||||
<li>- Annex: Systematische Ueberwachung — Verhaeltnismaessigkeitspruefung</li>
|
||||
)}
|
||||
{wp248Selected.includes('sensitive_data') && (
|
||||
<li>- Annex: Besondere Datenkategorien — Schutzbedarfsanalyse Art. 9</li>
|
||||
)}
|
||||
{wp248Selected.includes('large_scale') && (
|
||||
<li>- Annex: Umfangsanalyse — Quantitative Bewertung der Verarbeitung</li>
|
||||
)}
|
||||
{wp248Selected.includes('matching_combining') && (
|
||||
<li>- Annex: Datenzusammenfuehrung — Zweckbindungspruefung</li>
|
||||
)}
|
||||
{wp248Selected.includes('vulnerable_subjects') && (
|
||||
<li>- Annex: Schutzbeduerftige Betroffene — Verstaerkte Schutzmassnahmen</li>
|
||||
)}
|
||||
{wp248Selected.includes('innovative_technology') && (
|
||||
<li>- Annex: Innovative Technologie — Technikfolgenabschaetzung</li>
|
||||
)}
|
||||
{wp248Selected.includes('preventing_rights') && (
|
||||
<li>- Annex: Rechteausuebung — Barrierefreiheit der Betroffenenrechte</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
{aiTriggersSelected.length > 0 && (
|
||||
<p className="text-xs text-indigo-500 mt-2">
|
||||
+ KI-Trigger aktiv: Zusaetzlicher Annex fuer KI-Risikobewertung empfohlen (AI Act Konformitaet).
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Step 2: Art. 35 Abs. 3 Cases */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||
Schritt 2: Art. 35 Abs. 3 Regelbeispiele
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 mb-4">
|
||||
Bei Erfuellung eines Regelbeispiels ist eine DSFA zwingend erforderlich.
|
||||
</p>
|
||||
|
||||
<div className="space-y-3">
|
||||
{ART35_ABS3_CASES.map((caseItem) => (
|
||||
<label
|
||||
key={caseItem.id}
|
||||
className={`flex items-start gap-3 p-4 rounded-xl border cursor-pointer transition-all ${
|
||||
art35Selected.includes(caseItem.id)
|
||||
? 'bg-red-50 border-red-300 ring-1 ring-red-300'
|
||||
: 'bg-white border-gray-200 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={art35Selected.includes(caseItem.id)}
|
||||
onChange={() => toggleArt35(caseItem.id)}
|
||||
className="mt-1 rounded border-gray-300 text-red-600 focus:ring-red-500"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-gray-900">lit. {caseItem.lit}:</span>
|
||||
<span className="text-gray-900">{caseItem.title}</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 mt-1">{caseItem.description}</p>
|
||||
<span className="text-xs text-blue-600 mt-1 inline-block">{caseItem.gdprRef}</span>
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Step 3: AI-specific Triggers */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||
Schritt 3: KI-spezifische Trigger
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 mb-4">
|
||||
Wird kuenstliche Intelligenz eingesetzt? Diese Trigger sind in der deutschen DSFA-Muss-Liste enthalten.
|
||||
</p>
|
||||
|
||||
<div className="space-y-3">
|
||||
{AI_DSFA_TRIGGERS.map((trigger) => (
|
||||
<label
|
||||
key={trigger.id}
|
||||
className={`flex items-start gap-3 p-4 rounded-xl border cursor-pointer transition-all ${
|
||||
aiTriggersSelected.includes(trigger.id)
|
||||
? 'bg-blue-50 border-blue-300 ring-1 ring-blue-300'
|
||||
: 'bg-white border-gray-200 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={aiTriggersSelected.includes(trigger.id)}
|
||||
onChange={() => toggleAITrigger(trigger.id)}
|
||||
className="mt-1 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<span className="font-medium text-gray-900">{trigger.title}</span>
|
||||
<p className="text-sm text-gray-500 mt-1">{trigger.description}</p>
|
||||
{trigger.examples.length > 0 && (
|
||||
<p className="text-xs text-gray-400 mt-1">
|
||||
Beispiele: {trigger.examples.join(', ')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Step 4: Decision */}
|
||||
<div className="border-t border-gray-200 pt-8">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||
Schritt 4: Entscheidung
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 mb-4">
|
||||
Dokumentieren Sie Ihre Entscheidung, ob eine DSFA erforderlich ist.
|
||||
</p>
|
||||
|
||||
{/* Recommendation Banner */}
|
||||
<div className={`mb-6 p-4 rounded-xl border ${
|
||||
recommendation === 'required'
|
||||
? 'bg-red-50 border-red-200'
|
||||
: recommendation === 'possible'
|
||||
? 'bg-yellow-50 border-yellow-200'
|
||||
: 'bg-green-50 border-green-200'
|
||||
}`}>
|
||||
<div className="flex items-center gap-3">
|
||||
{recommendation === 'required' ? (
|
||||
<>
|
||||
<div className="w-10 h-10 rounded-full bg-red-100 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 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>
|
||||
<p className="font-semibold text-red-800">DSFA erforderlich</p>
|
||||
<p className="text-sm text-red-600">
|
||||
Basierend auf Ihrer Auswahl ist eine DSFA in den meisten Faellen Pflicht.
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
) : recommendation === 'possible' ? (
|
||||
<>
|
||||
<div className="w-10 h-10 rounded-full bg-yellow-100 flex items-center justify-center">
|
||||
<svg className="w-6 h-6 text-yellow-600" 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>
|
||||
<div>
|
||||
<p className="font-semibold text-yellow-800">DSFA moeglicherweise erforderlich</p>
|
||||
<p className="text-sm text-yellow-600">
|
||||
Einzelfallpruefung empfohlen. Bei Unsicherheit DSFA durchfuehren.
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="w-10 h-10 rounded-full bg-green-100 flex items-center justify-center">
|
||||
<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>
|
||||
<p className="font-semibold text-green-800">DSFA wahrscheinlich nicht erforderlich</p>
|
||||
<p className="text-sm text-green-600">
|
||||
Keine Pflichtkriterien erfuellt. Dokumentieren Sie diese Entscheidung.
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Decision Radio Buttons */}
|
||||
<div className="space-y-3 mb-6">
|
||||
<label className={`flex items-center gap-3 p-4 rounded-xl border cursor-pointer transition-all ${
|
||||
dsfaRequired === true
|
||||
? 'bg-purple-50 border-purple-300 ring-1 ring-purple-300'
|
||||
: 'bg-white border-gray-200 hover:bg-gray-50'
|
||||
}`}>
|
||||
<input
|
||||
type="radio"
|
||||
name="dsfaRequired"
|
||||
checked={dsfaRequired === true}
|
||||
onChange={() => setDsfaRequired(true)}
|
||||
className="text-purple-600 focus:ring-purple-500"
|
||||
/>
|
||||
<div>
|
||||
<span className="font-medium text-gray-900">DSFA erforderlich</span>
|
||||
<p className="text-sm text-gray-500">Ich fuehre eine vollstaendige DSFA durch.</p>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label className={`flex items-center gap-3 p-4 rounded-xl border cursor-pointer transition-all ${
|
||||
dsfaRequired === false
|
||||
? 'bg-purple-50 border-purple-300 ring-1 ring-purple-300'
|
||||
: 'bg-white border-gray-200 hover:bg-gray-50'
|
||||
}`}>
|
||||
<input
|
||||
type="radio"
|
||||
name="dsfaRequired"
|
||||
checked={dsfaRequired === false}
|
||||
onChange={() => setDsfaRequired(false)}
|
||||
className="text-purple-600 focus:ring-purple-500"
|
||||
/>
|
||||
<div>
|
||||
<span className="font-medium text-gray-900">DSFA nicht erforderlich</span>
|
||||
<p className="text-sm text-gray-500">
|
||||
Die Verarbeitung erfordert keine DSFA. Die Entscheidung wird dokumentiert.
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Justification */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Begruendung der Entscheidung *
|
||||
</label>
|
||||
<p className="text-xs text-gray-500 mb-2">
|
||||
Gem. DSK Kurzpapier Nr. 5 ist die Entscheidung und ihre Begruendung zu dokumentieren.
|
||||
</p>
|
||||
<textarea
|
||||
value={justification}
|
||||
onChange={(e) => setJustification(e.target.value)}
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
||||
rows={4}
|
||||
placeholder="Begruenden Sie, warum eine DSFA erforderlich/nicht erforderlich ist..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Save Button */}
|
||||
<div className="flex justify-end pt-4 border-t border-gray-200">
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={isSubmitting || dsfaRequired === null || !justification.trim()}
|
||||
className="px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{isSubmitting ? 'Speichern...' : 'Entscheidung speichern & fortfahren'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user