feat: BetrVG-Compliance-Modul — Obligations, Konflikt-Score, Frontend

1. BetrVG Obligations (JSON V2): 12 Pflichten basierend auf §87, §90, §94, §95, §99, §111
   - BAG-Rechtsprechung referenziert (M365, SAP, Standardsoftware)
   - Applicability: DE + >=5 Mitarbeiter
2. Betriebsrats-Konflikt-Score (0-100): Gewichtete Formel aus 8 Faktoren
   - Ueberwachungseignung, HR-Bezug, Individualisierbarkeit, Automation
   - Escalation-Trigger: Score>=50 ohne BR → E2, Score>=75 → E3
3. Frontend: 3 neue Intake-Felder (Monitoring, HR, BR-Konsultation)
   - BR-Konflikt-Badge in Use-Case-Liste + Detail-Seite
   - Farbcodierung: gruen/gelb/orange/rot

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-04-12 10:49:56 +02:00
parent bc75b4455d
commit c55a6ab995
10 changed files with 437 additions and 1 deletions

View File

@@ -333,6 +333,10 @@ function AdvisoryBoardPageInner() {
purposes: [] as string[],
// Automation (single-select tile)
automation: '' as string,
// BetrVG / works council
employee_monitoring: false,
hr_decision_support: false,
works_council_consulted: false,
// Hosting (single-select tile)
hosting_provider: '' as string,
hosting_region: '' as string,
@@ -420,6 +424,9 @@ function AdvisoryBoardPageInner() {
retention_purpose: form.retention_purpose,
contracts_list: form.contracts,
subprocessors: form.subprocessors,
employee_monitoring: form.employee_monitoring,
hr_decision_support: form.hr_decision_support,
works_council_consulted: form.works_council_consulted,
store_raw_text: true,
}
@@ -777,6 +784,52 @@ function AdvisoryBoardPageInner() {
Informationspflicht, Recht auf menschliche Ueberpruefung und Anfechtungsmoeglichkeit.
</p>
</div>
{/* BetrVG Section */}
<div className="mt-6 pt-6 border-t border-gray-200">
<h3 className="text-sm font-semibold text-gray-900 mb-1">Betriebsrat & Beschaeftigtendaten</h3>
<p className="text-xs text-gray-500 mb-4">
Relevant fuer deutsche Unternehmen mit Betriebsrat (§87 Abs.1 Nr.6 BetrVG).
</p>
<div className="space-y-3">
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input
type="checkbox"
checked={form.employee_monitoring}
onChange={(e) => updateForm({ employee_monitoring: e.target.checked })}
className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500"
/>
<div>
<span className="text-sm font-medium text-gray-900">System kann Verhalten/Leistung ueberwachen</span>
<p className="text-xs text-gray-500">Nutzungslogs, Produktivitaetskennzahlen, Kommunikationsanalyse</p>
</div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input
type="checkbox"
checked={form.hr_decision_support}
onChange={(e) => updateForm({ hr_decision_support: e.target.checked })}
className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500"
/>
<div>
<span className="text-sm font-medium text-gray-900">System unterstuetzt HR-Entscheidungen</span>
<p className="text-xs text-gray-500">Recruiting, Bewertung, Befoerderung, Kuendigung</p>
</div>
</label>
<label className="flex items-center gap-3 p-3 rounded-lg border border-gray-200 hover:bg-gray-50 cursor-pointer">
<input
type="checkbox"
checked={form.works_council_consulted}
onChange={(e) => updateForm({ works_council_consulted: e.target.checked })}
className="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500"
/>
<div>
<span className="text-sm font-medium text-gray-900">Betriebsrat wurde konsultiert</span>
<p className="text-xs text-gray-500">Betriebsvereinbarung liegt vor oder ist in Verhandlung</p>
</div>
</label>
</div>
</div>
</div>
)}

View File

@@ -57,6 +57,8 @@ interface FullAssessment {
dsfa_recommended: boolean
art22_risk: boolean
training_allowed: string
betrvg_conflict_score?: number
betrvg_consultation_required?: boolean
triggered_rules?: TriggeredRule[]
required_controls?: RequiredControl[]
recommended_architecture?: PatternRecommendation[]
@@ -167,6 +169,8 @@ export default function AssessmentDetailPage() {
dsfa_recommended: assessment.dsfa_recommended,
art22_risk: assessment.art22_risk,
training_allowed: assessment.training_allowed,
betrvg_conflict_score: assessment.betrvg_conflict_score,
betrvg_consultation_required: assessment.betrvg_consultation_required,
// AssessmentResultCard expects rule_code; backend stores code — map here
triggered_rules: assessment.triggered_rules?.map(r => ({
rule_code: r.code,

View File

@@ -10,6 +10,8 @@ interface Assessment {
feasibility: string
risk_level: string
risk_score: number
betrvg_conflict_score?: number
betrvg_consultation_required?: boolean
domain: string
created_at: string
}
@@ -194,6 +196,16 @@ export default function UseCasesPage() {
<span className={`px-2 py-0.5 text-xs rounded-full ${feasibility.bg} ${feasibility.text}`}>
{feasibility.label}
</span>
{assessment.betrvg_conflict_score != null && assessment.betrvg_conflict_score > 0 && (
<span className={`px-2 py-0.5 text-xs rounded-full ${
assessment.betrvg_conflict_score >= 75 ? 'bg-red-100 text-red-700' :
assessment.betrvg_conflict_score >= 50 ? 'bg-orange-100 text-orange-700' :
assessment.betrvg_conflict_score >= 25 ? 'bg-yellow-100 text-yellow-700' :
'bg-green-100 text-green-700'
}`}>
BR {assessment.betrvg_conflict_score}
</span>
)}
</div>
<div className="flex items-center gap-4 text-sm text-gray-500">
<span>{assessment.domain}</span>

View File

@@ -10,6 +10,8 @@ interface AssessmentResult {
dsfa_recommended: boolean
art22_risk: boolean
training_allowed: string
betrvg_conflict_score?: number
betrvg_consultation_required?: boolean
summary: string
recommendation: string
alternative_approach?: string
@@ -76,6 +78,21 @@ export function AssessmentResultCard({ result }: AssessmentResultCardProps) {
Art. 22 Risiko
</span>
)}
{result.betrvg_conflict_score != null && result.betrvg_conflict_score > 0 && (
<span className={`px-3 py-1 rounded-full text-sm font-medium ${
result.betrvg_conflict_score >= 75 ? 'bg-red-100 text-red-700' :
result.betrvg_conflict_score >= 50 ? 'bg-orange-100 text-orange-700' :
result.betrvg_conflict_score >= 25 ? 'bg-yellow-100 text-yellow-700' :
'bg-green-100 text-green-700'
}`}>
BR-Konflikt: {result.betrvg_conflict_score}/100
</span>
)}
{result.betrvg_consultation_required && (
<span className="px-3 py-1 rounded-full text-sm bg-purple-100 text-purple-700">
BR-Konsultation erforderlich
</span>
)}
</div>
<p className="text-gray-700">{result.summary}</p>
<p className="text-sm text-gray-500 mt-2">{result.recommendation}</p>