fix(sdk): Align scope types with engine output + project isolation + optional block progress
Some checks failed
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 46s
CI/CD / test-python-backend-compliance (push) Successful in 42s
CI/CD / test-python-document-crawler (push) Successful in 29s
CI/CD / test-python-dsms-gateway (push) Successful in 25s
CI/CD / deploy-hetzner (push) Failing after 2s
Some checks failed
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 46s
CI/CD / test-python-backend-compliance (push) Successful in 42s
CI/CD / test-python-document-crawler (push) Successful in 29s
CI/CD / test-python-dsms-gateway (push) Successful in 25s
CI/CD / deploy-hetzner (push) Failing after 2s
Type alignment (root cause of client-side crash):
- RiskFlag: id/title/description → severity/category/message/recommendation
- ScopeGap: id/title/recommendation/relatedDocuments → gapType/currentState/targetState/effort
- NextAction: id/priority:number/effortDays → actionType/priority:string/estimatedEffort
- ScopeReasoning: details → factors + impact
- TriggeredHardTrigger: {rule: HardTriggerRule} → flat fields (ruleId, description, etc.)
- All UI components updated to match engine output shape
Project isolation:
- Scope localStorage key now includes projectId (prevents data leak between projects)
Optional block progress:
- Blocks with only optional questions now show green checkmark when any question answered
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -37,14 +37,31 @@ const TABS: { id: TabId; label: string; icon: string }[] = [
|
||||
export default function ComplianceScopePage() {
|
||||
const { state: sdkState, dispatch } = useSDK()
|
||||
|
||||
// Project-specific storage key
|
||||
const projectStorageKey = sdkState.projectId
|
||||
? `${STORAGE_KEY}_${sdkState.projectId}`
|
||||
: STORAGE_KEY
|
||||
|
||||
// Active tab state
|
||||
const [activeTab, setActiveTab] = useState<TabId>('overview')
|
||||
|
||||
// Migrate old decision format: drop decision if it has old-format fields
|
||||
const migrateState = (state: ComplianceScopeState): ComplianceScopeState => {
|
||||
if (state.decision) {
|
||||
const d = state.decision as Record<string, unknown>
|
||||
// Old format had 'level' instead of 'determinedLevel', or docs with 'isMandatory'
|
||||
if (d.level || !d.determinedLevel) {
|
||||
return { ...state, decision: null }
|
||||
}
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
// Local scope state
|
||||
const [scopeState, setScopeState] = useState<ComplianceScopeState>(() => {
|
||||
// Try to load from SDK context first
|
||||
if (sdkState.complianceScope) {
|
||||
return sdkState.complianceScope
|
||||
return migrateState(sdkState.complianceScope)
|
||||
}
|
||||
return createEmptyScopeState()
|
||||
})
|
||||
@@ -68,14 +85,14 @@ export default function ComplianceScopePage() {
|
||||
const ctxScope = sdkState.complianceScope
|
||||
if (ctxScope && ctxScope.answers?.length > 0) {
|
||||
syncingFromSdk.current = true
|
||||
setScopeState(ctxScope)
|
||||
setScopeState(migrateState(ctxScope))
|
||||
setIsLoading(false)
|
||||
} else if (isLoading) {
|
||||
// SDK has no scope data — try localStorage fallback, then give up
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY)
|
||||
const stored = localStorage.getItem(projectStorageKey)
|
||||
if (stored) {
|
||||
const parsed = JSON.parse(stored) as ComplianceScopeState
|
||||
const parsed = migrateState(JSON.parse(stored) as ComplianceScopeState)
|
||||
if (parsed.answers?.length > 0) {
|
||||
setScopeState(parsed)
|
||||
dispatch({ type: 'SET_COMPLIANCE_SCOPE', payload: parsed })
|
||||
@@ -106,7 +123,7 @@ export default function ComplianceScopePage() {
|
||||
return
|
||||
}
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(scopeState))
|
||||
localStorage.setItem(projectStorageKey, JSON.stringify(scopeState))
|
||||
dispatch({ type: 'SET_COMPLIANCE_SCOPE', payload: scopeState })
|
||||
} catch (error) {
|
||||
console.error('Failed to save compliance scope state:', error)
|
||||
@@ -194,7 +211,7 @@ export default function ComplianceScopePage() {
|
||||
const emptyState = createEmptyScopeState()
|
||||
setScopeState(emptyState)
|
||||
setActiveTab('overview')
|
||||
localStorage.removeItem(STORAGE_KEY)
|
||||
localStorage.removeItem(projectStorageKey)
|
||||
}, [])
|
||||
|
||||
// Calculate completion statistics
|
||||
|
||||
@@ -58,22 +58,23 @@ export function ScopeDecisionTab({
|
||||
return 'from-green-500 to-green-600'
|
||||
}
|
||||
|
||||
const getSeverityBadge = (severity: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL') => {
|
||||
const colors = {
|
||||
LOW: 'bg-gray-100 text-gray-800',
|
||||
MEDIUM: 'bg-yellow-100 text-yellow-800',
|
||||
HIGH: 'bg-orange-100 text-orange-800',
|
||||
CRITICAL: 'bg-red-100 text-red-800',
|
||||
const getSeverityBadge = (severity: string) => {
|
||||
const s = severity.toLowerCase()
|
||||
const colors: Record<string, string> = {
|
||||
low: 'bg-gray-100 text-gray-800',
|
||||
medium: 'bg-yellow-100 text-yellow-800',
|
||||
high: 'bg-orange-100 text-orange-800',
|
||||
critical: 'bg-red-100 text-red-800',
|
||||
}
|
||||
const labels = {
|
||||
LOW: 'Niedrig',
|
||||
MEDIUM: 'Mittel',
|
||||
HIGH: 'Hoch',
|
||||
CRITICAL: 'Kritisch',
|
||||
const labels: Record<string, string> = {
|
||||
low: 'Niedrig',
|
||||
medium: 'Mittel',
|
||||
high: 'Hoch',
|
||||
critical: 'Kritisch',
|
||||
}
|
||||
return (
|
||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${colors[severity]}`}>
|
||||
{labels[severity]}
|
||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${colors[s] || colors.medium}`}>
|
||||
{labels[s] || severity}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -254,9 +255,9 @@ export function ScopeDecisionTab({
|
||||
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>
|
||||
<span className="font-medium text-gray-900">{trigger.rule.description}</span>
|
||||
<span className="font-medium text-gray-900">{trigger.description}</span>
|
||||
<span className="text-xs px-2 py-0.5 rounded-full bg-red-200 text-red-800 font-medium">
|
||||
Min. {trigger.rule.minimumLevel}
|
||||
Min. {trigger.minimumLevel}
|
||||
</span>
|
||||
</div>
|
||||
<svg
|
||||
@@ -272,18 +273,18 @@ export function ScopeDecisionTab({
|
||||
</button>
|
||||
{expandedTrigger === idx && (
|
||||
<div className="px-4 pb-4 pt-2 border-t border-gray-200">
|
||||
<p className="text-sm text-gray-700 mb-2">{trigger.explanation}</p>
|
||||
{trigger.rule.legalReference && (
|
||||
<p className="text-sm text-gray-700 mb-2">{trigger.description}</p>
|
||||
{trigger.legalReference && (
|
||||
<p className="text-xs text-gray-600 mb-2">
|
||||
<span className="font-medium">Rechtsgrundlage:</span> {trigger.rule.legalReference}
|
||||
<span className="font-medium">Rechtsgrundlage:</span> {trigger.legalReference}
|
||||
</p>
|
||||
)}
|
||||
{trigger.rule.mandatoryDocuments && trigger.rule.mandatoryDocuments.length > 0 && (
|
||||
{trigger.mandatoryDocuments && trigger.mandatoryDocuments.length > 0 && (
|
||||
<p className="text-xs text-gray-700">
|
||||
<span className="font-medium">Pflichtdokumente:</span> {trigger.rule.mandatoryDocuments.join(', ')}
|
||||
<span className="font-medium">Pflichtdokumente:</span> {trigger.mandatoryDocuments.join(', ')}
|
||||
</p>
|
||||
)}
|
||||
{trigger.rule.dsfaRequired && (
|
||||
{trigger.requiresDSFA && (
|
||||
<p className="text-xs text-orange-700 font-medium mt-1">DSFA erforderlich</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -302,10 +303,10 @@ export function ScopeDecisionTab({
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-200">
|
||||
<th className="text-left py-3 px-4 text-sm font-semibold text-gray-700">Typ</th>
|
||||
<th className="text-left py-3 px-4 text-sm font-semibold text-gray-700">Tiefe</th>
|
||||
<th className="text-left py-3 px-4 text-sm font-semibold text-gray-700">Dokument</th>
|
||||
<th className="text-left py-3 px-4 text-sm font-semibold text-gray-700">Priorität</th>
|
||||
<th className="text-left py-3 px-4 text-sm font-semibold text-gray-700">Aufwand</th>
|
||||
<th className="text-left py-3 px-4 text-sm font-semibold text-gray-700">Status</th>
|
||||
<th className="text-left py-3 px-4 text-sm font-semibold text-gray-700">Trigger</th>
|
||||
<th className="text-left py-3 px-4 text-sm font-semibold text-gray-700">Aktion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -317,16 +318,16 @@ export function ScopeDecisionTab({
|
||||
<span className="font-medium text-gray-900">
|
||||
{DOCUMENT_TYPE_LABELS[doc.documentType] || doc.documentType}
|
||||
</span>
|
||||
{doc.required && (
|
||||
{doc.requirement === 'mandatory' && (
|
||||
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-purple-100 text-purple-800">
|
||||
Pflicht
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-3 px-4 text-sm text-gray-700">{doc.depth}</td>
|
||||
<td className="py-3 px-4 text-sm text-gray-700 capitalize">{doc.priority}</td>
|
||||
<td className="py-3 px-4 text-sm text-gray-700">
|
||||
{doc.estimatedEffort || '-'}
|
||||
{doc.estimatedEffort ? `${doc.estimatedEffort}h` : '-'}
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
{doc.triggeredBy && doc.triggeredBy.length > 0 && (
|
||||
@@ -361,10 +362,12 @@ export function ScopeDecisionTab({
|
||||
{decision.riskFlags.map((flag, idx) => (
|
||||
<div key={idx} className="border border-gray-200 rounded-lg p-4">
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<h4 className="font-semibold text-gray-900">{flag.title}</h4>
|
||||
<h4 className="font-semibold text-gray-900">{flag.message}</h4>
|
||||
{getSeverityBadge(flag.severity)}
|
||||
</div>
|
||||
<p className="text-sm text-gray-700 mb-2">{flag.description}</p>
|
||||
{flag.legalReference && (
|
||||
<p className="text-xs text-gray-500 mb-2">{flag.legalReference}</p>
|
||||
)}
|
||||
<p className="text-sm text-gray-600">
|
||||
<span className="font-medium">Empfehlung:</span> {flag.recommendation}
|
||||
</p>
|
||||
@@ -382,26 +385,19 @@ export function ScopeDecisionTab({
|
||||
{decision.gaps.map((gap, idx) => (
|
||||
<div key={idx} className="border border-gray-200 rounded-lg p-4">
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<h4 className="font-semibold text-gray-900">{gap.title}</h4>
|
||||
<h4 className="font-semibold text-gray-900">{gap.description}</h4>
|
||||
{getSeverityBadge(gap.severity)}
|
||||
</div>
|
||||
<p className="text-sm text-gray-700 mb-2">{gap.description}</p>
|
||||
<p className="text-sm text-gray-600 mb-2">
|
||||
<span className="font-medium">Empfehlung:</span> {gap.recommendation}
|
||||
<p className="text-sm text-gray-700 mb-2">
|
||||
<span className="font-medium">Ist:</span> {gap.currentState}
|
||||
</p>
|
||||
{gap.relatedDocuments && gap.relatedDocuments.length > 0 && (
|
||||
<div className="mt-2">
|
||||
<span className="text-xs text-gray-500">Betroffene Dokumente: </span>
|
||||
{gap.relatedDocuments.map((doc, docIdx) => (
|
||||
<span
|
||||
key={docIdx}
|
||||
className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-800 mr-1"
|
||||
>
|
||||
{DOCUMENT_TYPE_LABELS[doc] || doc}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<p className="text-sm text-gray-600 mb-2">
|
||||
<span className="font-medium">Soll:</span> {gap.targetState}
|
||||
</p>
|
||||
<div className="flex items-center gap-4 text-xs text-gray-500">
|
||||
<span>Aufwand: ~{gap.effort}h</span>
|
||||
<span>Level: {gap.requiredFor}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -416,21 +412,21 @@ export function ScopeDecisionTab({
|
||||
{decision.nextActions.map((action, idx) => (
|
||||
<div key={idx} className="flex gap-4">
|
||||
<div className="flex-shrink-0 w-8 h-8 bg-purple-100 rounded-full flex items-center justify-center">
|
||||
<span className="text-sm font-bold text-purple-700">{action.priority}</span>
|
||||
<span className="text-sm font-bold text-purple-700">{idx + 1}</span>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h4 className="font-semibold text-gray-900 mb-1">{action.title}</h4>
|
||||
<p className="text-sm text-gray-700 mb-2">{action.description}</p>
|
||||
<div className="flex items-center gap-3">
|
||||
{action.effortDays && (
|
||||
{action.estimatedEffort > 0 && (
|
||||
<span className="text-xs text-gray-600">
|
||||
<span className="font-medium">Aufwand:</span> {action.effortDays} Tage
|
||||
<span className="font-medium">Aufwand:</span> ~{action.estimatedEffort}h
|
||||
</span>
|
||||
)}
|
||||
{action.relatedDocuments && action.relatedDocuments.length > 0 && (
|
||||
<span className="text-xs text-gray-600">
|
||||
<span className="font-medium">Dokumente:</span> {action.relatedDocuments.length}
|
||||
</span>
|
||||
{action.sdkStepUrl && (
|
||||
<a href={action.sdkStepUrl} className="text-xs text-purple-600 hover:text-purple-700">
|
||||
Zum SDK-Schritt →
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -469,8 +465,8 @@ export function ScopeDecisionTab({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Audit Trail */}
|
||||
{decision.auditTrail && decision.auditTrail.length > 0 && (
|
||||
{/* Audit Trail (from reasoning) */}
|
||||
{decision.reasoning && decision.reasoning.length > 0 && (
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||
<button
|
||||
type="button"
|
||||
@@ -489,17 +485,20 @@ export function ScopeDecisionTab({
|
||||
</button>
|
||||
{showAuditTrail && (
|
||||
<div className="space-y-3">
|
||||
{decision.auditTrail.map((entry, idx) => (
|
||||
{decision.reasoning.map((entry, idx) => (
|
||||
<div key={idx} className="border-l-2 border-purple-300 pl-4 py-2">
|
||||
<h4 className="font-medium text-gray-900 mb-1">{entry.step}</h4>
|
||||
<p className="text-sm text-gray-700 mb-2">{entry.description}</p>
|
||||
{entry.details && entry.details.length > 0 && (
|
||||
{entry.factors && entry.factors.length > 0 && (
|
||||
<ul className="text-xs text-gray-600 space-y-1">
|
||||
{entry.details.map((detail, detailIdx) => (
|
||||
<li key={detailIdx}>• {detail}</li>
|
||||
{entry.factors.map((factor, factorIdx) => (
|
||||
<li key={factorIdx}>• {factor}</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
{entry.impact && (
|
||||
<p className="text-xs text-purple-700 font-medium mt-1">{entry.impact}</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -31,12 +31,12 @@ export function ScopeExportTab({ decision: decisionProp, answers: answersProp, s
|
||||
const handleDownloadCSV = useCallback(() => {
|
||||
if (!decision || !decision.requiredDocuments) return
|
||||
|
||||
const headers = ['Typ', 'Tiefe', 'Aufwand (Tage)', 'Pflicht', 'Hard-Trigger']
|
||||
const headers = ['Typ', 'Priorität', 'Aufwand (Stunden)', 'Pflicht', 'Hard-Trigger']
|
||||
const rows = decision.requiredDocuments.map((doc) => [
|
||||
DOCUMENT_TYPE_LABELS[doc.documentType] || doc.documentType,
|
||||
doc.depth,
|
||||
doc.estimatedEffort || '0',
|
||||
doc.required ? 'Ja' : 'Nein',
|
||||
doc.priority,
|
||||
String(doc.estimatedEffort || 0),
|
||||
doc.requirement === 'mandatory' ? 'Ja' : 'Nein',
|
||||
doc.triggeredBy.length > 0 ? 'Ja' : 'Nein',
|
||||
])
|
||||
|
||||
@@ -58,8 +58,8 @@ export function ScopeExportTab({ decision: decisionProp, answers: answersProp, s
|
||||
markdown += `**Datum:** ${new Date().toLocaleDateString('de-DE')}\n\n`
|
||||
markdown += `## Einstufung\n\n`
|
||||
markdown += `**Level:** ${decision.determinedLevel} - ${DEPTH_LEVEL_LABELS[decision.determinedLevel]}\n\n`
|
||||
if (decision.reasoning) {
|
||||
markdown += `**Begründung:** ${decision.reasoning}\n\n`
|
||||
if (decision.reasoning && decision.reasoning.length > 0) {
|
||||
markdown += `**Begründung:** ${decision.reasoning.map(r => r.description).filter(Boolean).join('. ')}\n\n`
|
||||
}
|
||||
|
||||
if (decision.scores) {
|
||||
@@ -73,10 +73,10 @@ export function ScopeExportTab({ decision: decisionProp, answers: answersProp, s
|
||||
if (decision.triggeredHardTriggers && decision.triggeredHardTriggers.length > 0) {
|
||||
markdown += `## Aktive Hard-Trigger\n\n`
|
||||
decision.triggeredHardTriggers.forEach((trigger) => {
|
||||
markdown += `- **${trigger.rule.label}**\n`
|
||||
markdown += ` - ${trigger.rule.description}\n`
|
||||
if (trigger.rule.legalReference) {
|
||||
markdown += ` - Rechtsgrundlage: ${trigger.rule.legalReference}\n`
|
||||
markdown += `- **${trigger.description}**\n`
|
||||
markdown += ` - ${trigger.description}\n`
|
||||
if (trigger.legalReference) {
|
||||
markdown += ` - Rechtsgrundlage: ${trigger.legalReference}\n`
|
||||
}
|
||||
})
|
||||
markdown += `\n`
|
||||
@@ -84,12 +84,12 @@ export function ScopeExportTab({ decision: decisionProp, answers: answersProp, s
|
||||
|
||||
if (decision.requiredDocuments && decision.requiredDocuments.length > 0) {
|
||||
markdown += `## Erforderliche Dokumente\n\n`
|
||||
markdown += `| Typ | Tiefe | Aufwand | Pflicht | Hard-Trigger |\n`
|
||||
markdown += `|-----|-------|---------|---------|-------------|\n`
|
||||
markdown += `| Typ | Priorität | Aufwand (h) | Pflicht | Hard-Trigger |\n`
|
||||
markdown += `|-----|-----------|-------------|---------|-------------|\n`
|
||||
decision.requiredDocuments.forEach((doc) => {
|
||||
markdown += `| ${DOCUMENT_TYPE_LABELS[doc.documentType] || doc.documentType} | ${doc.depth} | ${
|
||||
doc.estimatedEffort || '0'
|
||||
} | ${doc.required ? 'Ja' : 'Nein'} | ${doc.triggeredBy.length > 0 ? 'Ja' : 'Nein'} |\n`
|
||||
markdown += `| ${DOCUMENT_TYPE_LABELS[doc.documentType] || doc.documentType} | ${doc.priority} | ${
|
||||
doc.estimatedEffort || 0
|
||||
} | ${doc.requirement === 'mandatory' ? 'Ja' : 'Nein'} | ${doc.triggeredBy.length > 0 ? 'Ja' : 'Nein'} |\n`
|
||||
})
|
||||
markdown += `\n`
|
||||
}
|
||||
@@ -97,19 +97,29 @@ export function ScopeExportTab({ decision: decisionProp, answers: answersProp, s
|
||||
if (decision.riskFlags && decision.riskFlags.length > 0) {
|
||||
markdown += `## Risiko-Flags\n\n`
|
||||
decision.riskFlags.forEach((flag) => {
|
||||
markdown += `### ${flag.title} (${flag.severity})\n\n`
|
||||
markdown += `${flag.description}\n\n`
|
||||
markdown += `### ${flag.message} (${flag.severity})\n\n`
|
||||
if (flag.legalReference) markdown += `Rechtsgrundlage: ${flag.legalReference}\n\n`
|
||||
markdown += `**Empfehlung:** ${flag.recommendation}\n\n`
|
||||
})
|
||||
}
|
||||
|
||||
if (decision.gaps && decision.gaps.length > 0) {
|
||||
markdown += `## Gap-Analyse\n\n`
|
||||
decision.gaps.forEach((gap) => {
|
||||
markdown += `### ${gap.description} (${gap.severity})\n\n`
|
||||
markdown += `- **Ist:** ${gap.currentState}\n`
|
||||
markdown += `- **Soll:** ${gap.targetState}\n`
|
||||
markdown += `- **Aufwand:** ~${gap.effort}h\n\n`
|
||||
})
|
||||
}
|
||||
|
||||
if (decision.nextActions && decision.nextActions.length > 0) {
|
||||
markdown += `## Nächste Schritte\n\n`
|
||||
decision.nextActions.forEach((action) => {
|
||||
markdown += `${action.priority}. **${action.title}**\n`
|
||||
decision.nextActions.forEach((action, idx) => {
|
||||
markdown += `${idx + 1}. **${action.title}**\n`
|
||||
markdown += ` ${action.description}\n`
|
||||
if (action.estimatedEffort) {
|
||||
markdown += ` Aufwand: ${action.estimatedEffort}\n`
|
||||
markdown += ` Aufwand: ~${action.estimatedEffort}h\n`
|
||||
}
|
||||
markdown += `\n`
|
||||
})
|
||||
|
||||
@@ -51,7 +51,7 @@ export function ScopeOverviewTab({ scopeState, completionStats, onStartProfiling
|
||||
}
|
||||
|
||||
const renderLevelBadge = () => {
|
||||
if (!decision?.level) {
|
||||
if (!decision?.determinedLevel) {
|
||||
return (
|
||||
<div className="bg-gray-100 border border-gray-300 rounded-xl p-8 text-center">
|
||||
<div className="inline-flex items-center justify-center w-24 h-24 bg-gray-200 rounded-full mb-4">
|
||||
@@ -80,13 +80,7 @@ export function ScopeOverviewTab({ scopeState, completionStats, onStartProfiling
|
||||
}
|
||||
|
||||
const renderActiveHardTriggers = () => {
|
||||
if (!decision?.hardTriggers || decision.triggeredHardTriggers.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const activeHardTriggers = decision.triggeredHardTriggers.filter((ht) => ht.matched)
|
||||
|
||||
if (activeHardTriggers.length === 0) {
|
||||
if (!decision?.triggeredHardTriggers || decision.triggeredHardTriggers.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -104,23 +98,22 @@ export function ScopeOverviewTab({ scopeState, completionStats, onStartProfiling
|
||||
<h3 className="text-lg font-semibold text-gray-900">Aktive Hard-Trigger</h3>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{activeHardTriggers.map((trigger, idx) => (
|
||||
{decision.triggeredHardTriggers.map((trigger, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="border-l-4 border-red-500 bg-red-50 rounded-r-lg p-4"
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<h4 className="font-semibold text-gray-900">{trigger.label}</h4>
|
||||
<p className="text-sm text-gray-600 mt-1">{trigger.description}</p>
|
||||
<h4 className="font-semibold text-gray-900">{trigger.description}</h4>
|
||||
{trigger.legalReference && (
|
||||
<p className="text-xs text-gray-500 mt-2">
|
||||
<span className="font-medium">Rechtsgrundlage:</span> {trigger.legalReference}
|
||||
</p>
|
||||
)}
|
||||
{trigger.matchedValue && (
|
||||
{trigger.minimumLevel && (
|
||||
<p className="text-xs text-gray-700 mt-1">
|
||||
<span className="font-medium">Erfasster Wert:</span> {trigger.matchedValue}
|
||||
<span className="font-medium">Mindest-Level:</span> {trigger.minimumLevel}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -137,12 +130,9 @@ export function ScopeOverviewTab({ scopeState, completionStats, onStartProfiling
|
||||
return null
|
||||
}
|
||||
|
||||
const mandatoryDocs = decision.requiredDocuments.filter((doc) => doc.isMandatory)
|
||||
const optionalDocs = decision.requiredDocuments.filter((doc) => !doc.isMandatory)
|
||||
const totalEffortDays = decision.requiredDocuments.reduce(
|
||||
(sum, doc) => sum + (doc.effortEstimate?.days ?? 0),
|
||||
0
|
||||
)
|
||||
const mandatoryDocs = decision.requiredDocuments.filter((doc) => doc.requirement === 'mandatory')
|
||||
const optionalDocs = decision.requiredDocuments.filter((doc) => doc.requirement === 'recommended')
|
||||
const totalEffortHours = decision.requiredDocuments.reduce((sum, doc) => sum + (doc.estimatedEffort ?? 0), 0)
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||
@@ -157,8 +147,8 @@ export function ScopeOverviewTab({ scopeState, completionStats, onStartProfiling
|
||||
<div className="text-sm text-gray-600 mt-1">Optional</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-3xl font-bold text-gray-900">{totalEffortDays}</div>
|
||||
<div className="text-sm text-gray-600 mt-1">Tage Aufwand (geschätzt)</div>
|
||||
<div className="text-3xl font-bold text-gray-900">{totalEffortHours}h</div>
|
||||
<div className="text-sm text-gray-600 mt-1">Aufwand (geschätzt)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -227,11 +217,11 @@ export function ScopeOverviewTab({ scopeState, completionStats, onStartProfiling
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Score-Übersicht</h3>
|
||||
<div className="space-y-4">
|
||||
{renderScoreGauge('Risiko-Score', decision.scores?.riskScore)}
|
||||
{renderScoreGauge('Komplexitäts-Score', decision.scores?.complexityScore)}
|
||||
{renderScoreGauge('Assurance-Score', decision.scores?.assuranceScore)}
|
||||
{renderScoreGauge('Risiko-Score', decision.scores?.risk_score)}
|
||||
{renderScoreGauge('Komplexitäts-Score', decision.scores?.complexity_score)}
|
||||
{renderScoreGauge('Assurance-Score', decision.scores?.assurance_need)}
|
||||
<div className="pt-4 border-t border-gray-200">
|
||||
{renderScoreGauge('Gesamt-Score', decision.scores?.compositeScore)}
|
||||
{renderScoreGauge('Gesamt-Score', decision.scores?.composite_score)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -350,6 +350,10 @@ export function ScopeWizardTab({
|
||||
const unanswered = getUnansweredRequiredQuestions(answers, block.id)
|
||||
const hasRequired = block.questions.some(q => q.required)
|
||||
const allRequiredDone = hasRequired && unanswered.length === 0
|
||||
// For optional-only blocks: check if any questions were answered
|
||||
const answeredIds = new Set(answers.map(a => a.questionId))
|
||||
const hasAnyAnswer = block.questions.some(q => answeredIds.has(q.id))
|
||||
const optionalDone = !hasRequired && hasAnyAnswer
|
||||
|
||||
return (
|
||||
<button
|
||||
@@ -366,11 +370,12 @@ export function ScopeWizardTab({
|
||||
<span className={`text-sm font-medium ${isActive ? 'text-purple-700' : 'text-gray-700'}`}>
|
||||
{block.title}
|
||||
</span>
|
||||
{allRequiredDone ? (
|
||||
{allRequiredDone || optionalDone ? (
|
||||
<span className="flex items-center gap-1 text-xs font-semibold text-green-600">
|
||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
{!hasRequired && <span>(optional)</span>}
|
||||
</span>
|
||||
) : !hasRequired ? (
|
||||
<span className="text-xs text-gray-400">(nur optional)</span>
|
||||
@@ -383,7 +388,7 @@ export function ScopeWizardTab({
|
||||
<div className="w-full bg-gray-200 rounded-full h-1.5 overflow-hidden">
|
||||
<div
|
||||
className={`h-full transition-all ${
|
||||
allRequiredDone
|
||||
allRequiredDone || optionalDone
|
||||
? 'bg-green-500'
|
||||
: !hasRequired
|
||||
? 'bg-gray-300'
|
||||
|
||||
@@ -1664,7 +1664,7 @@ export class ComplianceScopeEngine {
|
||||
step: 'hard_trigger_evaluation',
|
||||
description: `${triggers.length} Hard Trigger Rule(s) aktiviert`,
|
||||
factors: triggers.map(
|
||||
(t) => `${t.ruleId}: ${t.description} (${t.legalReference})`
|
||||
(t) => `${t.ruleId}: ${t.description}${t.legalReference ? ` (${t.legalReference})` : ''}`
|
||||
),
|
||||
impact: `Höchstes Trigger-Level: ${this.getMaxTriggerLevel(triggers)}`,
|
||||
})
|
||||
|
||||
@@ -154,12 +154,20 @@ export interface HardTriggerRule {
|
||||
* Getriggerter Hard Trigger mit Kontext
|
||||
*/
|
||||
export interface TriggeredHardTrigger {
|
||||
/** Die getriggerte Regel */
|
||||
rule: HardTriggerRule;
|
||||
/** Der tatsächlich gefundene Wert */
|
||||
matchedValue: unknown;
|
||||
/** Erklärung warum getriggert */
|
||||
explanation: string;
|
||||
/** Regel-ID */
|
||||
ruleId: string;
|
||||
/** Kategorie */
|
||||
category: string;
|
||||
/** Beschreibung */
|
||||
description: string;
|
||||
/** Rechtsgrundlage */
|
||||
legalReference?: string;
|
||||
/** Mindest-Level */
|
||||
minimumLevel: ComplianceDepthLevel;
|
||||
/** DSFA erforderlich? */
|
||||
requiresDSFA: boolean;
|
||||
/** Pflichtdokumente */
|
||||
mandatoryDocuments: string[];
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -229,14 +237,12 @@ export interface RequiredDocument {
|
||||
documentType: ScopeDocumentType;
|
||||
/** Anzeigename */
|
||||
label: string;
|
||||
/** Ist Pflicht? */
|
||||
required: boolean;
|
||||
/** Erforderliche Tiefe (z.B. "Basis", "Standard", "Detailliert") */
|
||||
depth: string;
|
||||
/** Konkrete Anforderungen/Inhalte */
|
||||
detailItems: string[];
|
||||
/** Geschätzter Aufwand */
|
||||
estimatedEffort: string;
|
||||
/** Pflicht oder empfohlen */
|
||||
requirement: 'mandatory' | 'recommended';
|
||||
/** Priorität */
|
||||
priority: 'high' | 'medium' | 'low';
|
||||
/** Geschätzter Aufwand in Stunden */
|
||||
estimatedEffort: number;
|
||||
/** Von welchen Triggern/Regeln gefordert */
|
||||
triggeredBy: string[];
|
||||
/** Link zum SDK-Schritt */
|
||||
@@ -247,14 +253,12 @@ export interface RequiredDocument {
|
||||
* Risiko-Flag
|
||||
*/
|
||||
export interface RiskFlag {
|
||||
/** Eindeutige ID */
|
||||
id: string;
|
||||
/** Schweregrad */
|
||||
severity: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
|
||||
/** Titel */
|
||||
title: string;
|
||||
severity: string;
|
||||
/** Kategorie */
|
||||
category: string;
|
||||
/** Beschreibung */
|
||||
description: string;
|
||||
message: string;
|
||||
/** Rechtsgrundlage */
|
||||
legalReference?: string;
|
||||
/** Empfehlung zur Behebung */
|
||||
@@ -265,38 +269,44 @@ export interface RiskFlag {
|
||||
* Identifizierte Lücke in der Compliance
|
||||
*/
|
||||
export interface ScopeGap {
|
||||
/** Eindeutige ID */
|
||||
id: string;
|
||||
/** Gap-Typ */
|
||||
gapType: string;
|
||||
/** Schweregrad */
|
||||
severity: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
|
||||
/** Titel */
|
||||
title: string;
|
||||
severity: string;
|
||||
/** Beschreibung */
|
||||
description: string;
|
||||
/** Empfehlung zur Schließung */
|
||||
recommendation: string;
|
||||
/** Betroffene Dokumente */
|
||||
relatedDocuments: ScopeDocumentType[];
|
||||
/** Erforderlich für Level */
|
||||
requiredFor: ComplianceDepthLevel;
|
||||
/** Aktueller Zustand */
|
||||
currentState: string;
|
||||
/** Zielzustand */
|
||||
targetState: string;
|
||||
/** Aufwand in Stunden */
|
||||
effort: number;
|
||||
/** Priorität */
|
||||
priority: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Nächster empfohlener Schritt
|
||||
*/
|
||||
export interface NextAction {
|
||||
/** Eindeutige ID */
|
||||
id: string;
|
||||
/** Priorität (1 = höchste) */
|
||||
priority: number;
|
||||
/** Aktionstyp */
|
||||
actionType: 'create_document' | 'establish_process' | 'implement_technical' | 'organizational_change';
|
||||
/** Titel */
|
||||
title: string;
|
||||
/** Beschreibung */
|
||||
description: string;
|
||||
/** Geschätzter Aufwand */
|
||||
estimatedEffort: string;
|
||||
/** Betroffene Dokumente */
|
||||
relatedDocuments: ScopeDocumentType[];
|
||||
/** Priorität */
|
||||
priority: string;
|
||||
/** Geschätzter Aufwand in Stunden */
|
||||
estimatedEffort: number;
|
||||
/** Dokumenttyp (optional) */
|
||||
documentType?: ScopeDocumentType;
|
||||
/** Link zum SDK-Schritt */
|
||||
sdkStepUrl?: string;
|
||||
/** Blocker */
|
||||
blockers: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -307,8 +317,10 @@ export interface ScopeReasoning {
|
||||
step: string;
|
||||
/** Kurzbeschreibung */
|
||||
description: string;
|
||||
/** Detaillierte Punkte */
|
||||
details: string[];
|
||||
/** Faktoren */
|
||||
factors: string[];
|
||||
/** Auswirkung */
|
||||
impact: string;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
Reference in New Issue
Block a user