e21984e0ad
Der Compliance-Check legt zusätzlich einen strukturierten v3-AgentOutput pro Thema in result.agent_outputs ab (additiv; B18-HTML + Firehose-Mail bleiben unangetastet). Frontend: standardisiertes Ergebnis-Tab statt Firehose — Impressum-Tab (AgentResultTab) + "Alle Checks (roh)" (ChecklistView). - backend: _agent_outputs.py ruft den registrierten v3-ImpressumAgent, gewired in _orchestrator nach B18, surfaced via _phase_f_persist. - frontend: AgentResultView (aus AgentSlotCard extrahiert, DRY), AgentResultTab, ComplianceResultTabs; ComplianceCheckTab 490->391 Zeilen. - Tests: backend 2 passed, frontend 2 passed; tsc 0 neue Fehler; check-loc 0. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
161 lines
7.2 KiB
TypeScript
161 lines
7.2 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 type { SlotOutput } from './_agentTypes'
|
|
import { AgentResultTab } from './AgentResultTab'
|
|
import { ChecklistView } from './ChecklistView'
|
|
import { MigrationPanel } from './MigrationPanel'
|
|
|
|
const TOPIC_LABELS: Record<string, string> = {
|
|
impressum: 'Impressum',
|
|
cookie: 'Cookie-Banner',
|
|
}
|
|
|
|
export function ComplianceResultTabs({ results }: { results: any }) {
|
|
const agentOutputs: Record<string, SlotOutput> = results.agent_outputs || {}
|
|
const topicKeys = Object.keys(agentOutputs)
|
|
const tabs = [...topicKeys, 'raw']
|
|
const [active, setActive] = useState<string>(tabs[0])
|
|
|
|
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 — Themen-Agenten + Roh-Checkliste */}
|
|
<div className="flex gap-1 border-b border-gray-200 flex-wrap">
|
|
{tabs.map(t => {
|
|
const count = t !== 'raw' ? (agentOutputs[t]?.findings?.length ?? 0) : 0
|
|
return (
|
|
<button
|
|
key={t}
|
|
onClick={() => setActive(t)}
|
|
className={`px-3 py-1.5 text-sm font-medium border-b-2 -mb-px transition-colors ${
|
|
active === t
|
|
? 'border-purple-500 text-purple-700'
|
|
: 'border-transparent text-gray-500 hover:text-gray-700'
|
|
}`}
|
|
>
|
|
{t === 'raw' ? 'Alle Checks (roh)' : (TOPIC_LABELS[t] || t)}
|
|
{count > 0 && (
|
|
<span className="ml-1.5 text-xs bg-gray-100 text-gray-600 rounded-full px-1.5">
|
|
{count}
|
|
</span>
|
|
)}
|
|
</button>
|
|
)
|
|
})}
|
|
</div>
|
|
|
|
{/* Tab-Inhalt */}
|
|
{active === 'raw' ? (
|
|
<ChecklistView results={results.results} />
|
|
) : agentOutputs[active] ? (
|
|
<AgentResultTab
|
|
topicLabel={TOPIC_LABELS[active] || active}
|
|
output={agentOutputs[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>
|
|
)
|
|
}
|