Files
breakpilot-compliance/admin-compliance/app/sdk/quality/_components/QuaidalCriterionDetail.tsx
T
Benjamin Admin e536247c20 feat(quaidal): backend API + frontend tab for BSI QUAIDAL data-quality controls
Wire the 195 Clean-Room QUAIDAL controls (from breakpilot-core migration 011)
into the compliance SaaS UI.

Backend:
- GET /api/v1/quaidal/stats           - counts by kind + source provenance
- GET /api/v1/quaidal/controls        - list, optional kind= filter
- GET /api/v1/quaidal/controls/{id}   - single derived control
- GET /api/v1/quaidal/criteria        - 10 QKB criteria
- GET /api/v1/quaidal/criteria/{id}   - QKB with QB/MA/QM tree

Frontend:
- /sdk/quality: new "Trainingsdaten-Qualität (BSI QUAIDAL)" tab with
  10 QKB cards and a drill-down modal showing the full QB→MA→QM tree
  plus original BSI source link and license note.
- /sdk/ai-act: Art. 10 tile on each high-risk/unacceptable result,
  linking to /sdk/quality?category=data_quality.

Pattern matches existing IACE module DIN-reference handling:
own wording, source section + URL preserved for due diligence.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 13:03:54 +02:00

153 lines
6.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import { useEffect, useState } from 'react'
import { fetchCriterionTree, type QuaidalControl, type QuaidalCriterionTree } from '../_hooks/useQuaidalData'
interface Props {
sectionId: string
onClose: () => void
}
function ControlBlock({ ctrl, badgeColor }: { ctrl: QuaidalControl; badgeColor: string }) {
return (
<div className="border border-gray-200 rounded-lg p-4 bg-white">
<div className="flex items-start justify-between gap-3 mb-2">
<h4 className="font-semibold text-gray-900">{ctrl.canonical_name}</h4>
<span className={`px-2 py-0.5 text-xs rounded-full ${badgeColor} shrink-0`}>{ctrl.source.section}</span>
</div>
<p className="text-sm text-gray-600 mb-3 whitespace-pre-line">{ctrl.description}</p>
{ctrl.source.url && (
<a
href={ctrl.source.url}
target="_blank"
rel="noreferrer noopener"
className="text-xs text-purple-600 hover:text-purple-800 underline"
>
BSI-Quelle ansehen ({ctrl.source.framework})
</a>
)}
</div>
)
}
export function QuaidalCriterionDetail({ sectionId, onClose }: Props) {
const [tree, setTree] = useState<QuaidalCriterionTree | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
let active = true
setLoading(true)
fetchCriterionTree(sectionId).then(t => {
if (active) {
setTree(t)
setLoading(false)
}
})
return () => { active = false }
}, [sectionId])
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4">
<div className="bg-white rounded-2xl shadow-xl w-full max-w-4xl max-h-[90vh] overflow-hidden flex flex-col">
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200">
<div>
<div className="text-xs text-gray-500 uppercase tracking-wide">QUAIDAL Kriterium</div>
<h2 className="text-xl font-bold text-gray-900">
{tree?.criterion.canonical_name || sectionId}
</h2>
</div>
<button
onClick={onClose}
className="w-8 h-8 rounded-full hover:bg-gray-100 flex items-center justify-center text-gray-500"
aria-label="Schliessen"
>×</button>
</div>
<div className="overflow-y-auto p-6 space-y-6">
{loading && <div className="text-center text-gray-400 py-12">Lade...</div>}
{tree && (
<>
<div>
<h3 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-2">
Anforderung (eigene Formulierung)
</h3>
<div className="bg-purple-50 border border-purple-200 rounded-lg p-4">
<p className="text-gray-800 whitespace-pre-line">{tree.criterion.description}</p>
</div>
<div className="mt-3 flex flex-wrap items-center gap-3 text-xs text-gray-500">
<span>Regulierung: <span className="font-medium text-gray-700">{tree.criterion.regulation_anchor || '—'}</span></span>
<span>Quelle: <span className="font-medium text-gray-700">{tree.criterion.source.framework} {tree.criterion.source.section}</span></span>
{tree.criterion.source.url && (
<a href={tree.criterion.source.url} target="_blank" rel="noreferrer noopener" className="text-purple-600 hover:text-purple-800 underline">
Originalquelle
</a>
)}
</div>
</div>
{tree.criterion.external_refs.length > 0 && (
<div>
<h3 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-2">
Externe Referenzen (nicht ingestiert, nur Verweis)
</h3>
<div className="flex flex-wrap gap-2">
{tree.criterion.external_refs.map((ref, i) => (
<span key={i} className="px-2 py-1 text-xs bg-gray-100 text-gray-700 rounded">
{ref.framework}{ref.citation ? `${ref.citation}` : ''}
</span>
))}
</div>
</div>
)}
{tree.building_blocks.length > 0 && (
<div>
<h3 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-3">
Bausteine ({tree.building_blocks.length})
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{tree.building_blocks.map(qb => (
<ControlBlock key={qb.derived_id} ctrl={qb} badgeColor="bg-blue-100 text-blue-700" />
))}
</div>
</div>
)}
{tree.measures.length > 0 && (
<div>
<h3 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-3">
Maßnahmen ({tree.measures.length})
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{tree.measures.map(m => (
<ControlBlock key={m.derived_id} ctrl={m} badgeColor="bg-green-100 text-green-700" />
))}
</div>
</div>
)}
{tree.metrics.length > 0 && (
<div>
<h3 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-3">
Metriken & Methoden ({tree.metrics.length})
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{tree.metrics.map(qm => (
<ControlBlock key={qm.derived_id} ctrl={qm} badgeColor="bg-amber-100 text-amber-700" />
))}
</div>
</div>
)}
</>
)}
</div>
<div className="px-6 py-3 border-t border-gray-200 bg-gray-50 text-xs text-gray-500">
Eigene Clean-Room-Ableitung von BSI QUAIDAL. Quellverweis und Lizenz-Note pro Eintrag.
</div>
</div>
</div>
)
}