Files
breakpilot-compliance/admin-compliance/app/sdk/agent/_components/ComplianceResultTabs.tsx
T
Benjamin Admin 3f23a64d5f feat(agent): Impressum-Tab auf Haupt-Engine + Profil/§36-Fixes
Ergebnis-Tab rendert jetzt result.results (Haupt-Doc-Check) statt des
abweichenden v3-Agenten — BMW korrekt statt False Positives:
- DocResultView: ein Dokument als Pflichtangaben-Tabelle (Label + gefundener
  Text + 3-Tier-Status), KEINE MC-IDs. ComplianceResultTabs speist Tabs aus
  result.results; ChecklistView-Bausteine exportiert + wiederverwendet.
- profile_extractor: Firmenname/Rechtsform = fruehester Treffer + ausge-
  schriebene Formen (Aktiengesellschaft) -> BMW AG statt "juris GmbH".
- 36 VSBG (MC-010): reines b2c -> POSSIBLY_APPLICABLE (Pruef-Hinweis) statt
  MEDIUM-FAIL; hart nur bei ecommerce. possibly_hint pro MC.
- McCoverage traegt label + found (Snippet); mc_possibly-Aggregat.
- AgentFindingCard/Methodik: interne check_id/mc_id nicht mehr angezeigt.

Tests: test_four_status (16) + Frontend-Vitest gruen; CI-Suite 206, v3/GT
unveraendert. Nur eigene Dateien (geteilter Tree).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-10 23:44:01 +02:00

157 lines
7.4 KiB
TypeScript

'use client'
/**
* ComplianceResultTabs — standardisierte Ergebnis-Darstellung des
* Compliance-Checks: Kopf-Boxen (erkanntes Profil + Banner) ÜBER einer
* Tab-Leiste. Ein Tab je Themen-Agent (result.agent_outputs, P1: Impressum)
* via AgentResultTab + ein "Alle Checks (roh)"-Tab mit der bisherigen
* ChecklistView — so geht nichts verloren, während die Themen-Tabs wachsen.
*/
import React, { useState } from 'react'
import { ChecklistView, DOC_TYPE_LABELS, type DocResult } from './ChecklistView'
import { DocResultView } from './DocResultView'
import { MigrationPanel } from './MigrationPanel'
export function ComplianceResultTabs({ results }: { results: any }) {
// Themen-Tabs aus der HAUPT-Engine (result.results) — nicht aus dem
// v3-Agent. Jedes Dokument = ein Tab mit der genauen Pflichtangaben-Tabelle.
const docs: DocResult[] = results.results || []
const tabs = docs.map((_: DocResult, i: number) => String(i)).concat('raw')
const [active, setActive] = useState<string>(tabs[0] ?? 'raw')
return (
<div className="bg-white border border-gray-200 rounded-xl p-6 shadow-sm space-y-4">
{/* Kopf-Boxen über den Tabs */}
{results.business_profile && (
<div className="p-3 bg-blue-50 border border-blue-200 rounded-lg text-xs">
<div className="font-semibold text-blue-900 mb-1">Erkanntes Geschaeftsmodell</div>
<div className="flex flex-wrap gap-x-4 gap-y-1 text-blue-700">
<span>Typ: <strong>{results.business_profile.business_type?.toUpperCase()}</strong></span>
<span>Branche: {results.business_profile.industry}</span>
{results.business_profile.has_online_shop && <span className="text-amber-700">Online-Shop</span>}
{results.business_profile.is_regulated_profession && <span className="text-amber-700">Regulierter Beruf ({results.business_profile.regulated_profession_type})</span>}
</div>
</div>
)}
{results.extracted_profile?.company_profile && Object.keys(results.extracted_profile.company_profile).length > 0 && (
<div className="p-3 bg-emerald-50 border border-emerald-200 rounded-lg text-xs">
<div className="flex items-center justify-between mb-1">
<span className="font-semibold text-emerald-900">Aus Dokumenten extrahiert</span>
<button className="text-emerald-700 hover:text-emerald-900 text-xs font-medium underline"
onClick={() => { /* TODO: navigate to company profile with pre-fill */ }}>
In Company Profile uebernehmen
</button>
</div>
<div className="flex flex-wrap gap-x-4 gap-y-1 text-emerald-700">
{results.extracted_profile.company_profile.companyName && (
<span>Firma: <strong>{results.extracted_profile.company_profile.companyName}</strong></span>
)}
{results.extracted_profile.company_profile.legalForm && (
<span>Rechtsform: {results.extracted_profile.company_profile.legalForm.toUpperCase()}</span>
)}
{results.extracted_profile.company_profile.headquartersCity && (
<span>Sitz: {results.extracted_profile.company_profile.headquartersZip} {results.extracted_profile.company_profile.headquartersCity}</span>
)}
{results.extracted_profile.company_profile.dpoEmail && (
<span>DSB: {results.extracted_profile.company_profile.dpoEmail}</span>
)}
{results.extracted_profile.company_profile.ustIdNr && (
<span>USt-IdNr: {results.extracted_profile.company_profile.ustIdNr}</span>
)}
</div>
{results.extracted_profile.compliance_scope_hints?.length > 0 && (
<div className="mt-2 pt-2 border-t border-emerald-200 text-emerald-600">
<span className="font-medium">Scope-Hinweise: </span>
{results.extracted_profile.compliance_scope_hints.map((h: any, i: number) => (
<span key={i} className="inline-block bg-emerald-100 rounded px-1.5 py-0.5 mr-1 mb-1">
{h.source}
</span>
))}
</div>
)}
</div>
)}
{results.banner_result && (
<div className={`p-3 rounded-lg border text-xs ${
results.banner_result.violations > 0
? 'bg-amber-50 border-amber-200'
: results.banner_result.detected
? 'bg-green-50 border-green-200'
: 'bg-gray-50 border-gray-200'
}`}>
<div className="flex items-center gap-2">
<span className={`w-2 h-2 rounded-full ${
results.banner_result.violations > 0 ? 'bg-amber-500'
: results.banner_result.detected ? 'bg-green-500' : 'bg-gray-400'
}`} />
<span className="font-semibold text-gray-900">
Cookie-Banner-Check (automatisch)
</span>
</div>
<div className="mt-1 text-gray-600 ml-4">
{results.banner_result.detected ? (
<>
Banner erkannt{results.banner_result.provider ? ` (${results.banner_result.provider})` : ''}.
{results.banner_result.violations > 0
? ` ${results.banner_result.violations} Auffaelligkeit${results.banner_result.violations !== 1 ? 'en' : ''} gefunden.`
: ' Keine Auffaelligkeiten.'}
</>
) : (
'Kein Cookie-Banner erkannt oder Banner-Check nicht moeglich.'
)}
</div>
</div>
)}
{/* Tab-Leiste — ein Tab je Dokument (Haupt-Engine) + Übersicht */}
<div className="flex gap-1 border-b border-gray-200 flex-wrap">
{tabs.map(t => {
const tabClass = `px-3 py-1.5 text-sm font-medium border-b-2 -mb-px transition-colors flex items-center gap-1.5 ${
active === t
? 'border-purple-500 text-purple-700'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`
if (t === 'raw') {
return (
<button key={t} onClick={() => setActive(t)} className={tabClass}>
Alle Checks
</button>
)
}
const doc = docs[Number(t)]
const dot = doc.error ? 'bg-gray-300'
: doc.scenario === 'import' ? 'bg-green-500'
: doc.scenario === 'fix' ? 'bg-amber-500'
: doc.scenario === 'regenerate' ? 'bg-red-500' : 'bg-gray-400'
return (
<button key={t} onClick={() => setActive(t)} className={tabClass}>
<span className={`w-2 h-2 rounded-full ${dot}`} />
{DOC_TYPE_LABELS[doc.doc_type] || doc.doc_type}
</button>
)
})}
</div>
{/* Tab-Inhalt */}
{active === 'raw' ? (
<ChecklistView results={results.results} />
) : docs[Number(active)] ? (
<DocResultView doc={docs[Number(active)]} />
) : null}
{/* Check-Footer (themenübergreifend) */}
{results.email_status && (
<div className="text-xs text-gray-500 flex items-center gap-2 border-t border-gray-100 pt-3">
<span className={`w-2 h-2 rounded-full ${results.email_status === 'sent' ? 'bg-green-400' : 'bg-gray-300'}`} />
E-Mail: {results.email_status === 'sent' ? 'Gesendet' : results.email_status}
</div>
)}
{results.check_id && <MigrationPanel checkId={results.check_id} />}
</div>
)
}