Some checks failed
Tests / Go Tests (push) Has been cancelled
Tests / Python Tests (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / Go Lint (push) Has been cancelled
Tests / Python Lint (push) Has been cancelled
Tests / Security Scan (push) Has been cancelled
Tests / All Checks Passed (push) Has been cancelled
Security Scanning / Secret Scanning (push) Has been cancelled
Security Scanning / Dependency Vulnerability Scan (push) Has been cancelled
Security Scanning / Go Security Scan (push) Has been cancelled
Security Scanning / Python Security Scan (push) Has been cancelled
Security Scanning / Node.js Security Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
CI/CD Pipeline / Go Tests (push) Has been cancelled
CI/CD Pipeline / Python Tests (push) Has been cancelled
CI/CD Pipeline / Website Tests (push) Has been cancelled
CI/CD Pipeline / Linting (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Docker Build & Push (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / CI Summary (push) Has been cancelled
ci/woodpecker/manual/build-ci-image Pipeline was successful
ci/woodpecker/manual/main Pipeline failed
All services: admin-v2, studio-v2, website, ai-compliance-sdk, consent-service, klausur-service, voice-service, and infrastructure. Large PDFs and compiled binaries excluded via .gitignore.
223 lines
9.1 KiB
TypeScript
223 lines
9.1 KiB
TypeScript
'use client'
|
|
|
|
import React from 'react'
|
|
|
|
// =============================================================================
|
|
// RE-EXPORTS FROM SEPARATE FILES
|
|
// =============================================================================
|
|
|
|
export { ThresholdAnalysisSection } from './ThresholdAnalysisSection'
|
|
export { DSFASidebar } from './DSFASidebar'
|
|
export { StakeholderConsultationSection } from './StakeholderConsultationSection'
|
|
export { Art36Warning } from './Art36Warning'
|
|
export { ReviewScheduleSection } from './ReviewScheduleSection'
|
|
export { SourceAttribution, InlineSourceRef, AttributionFooter } from './SourceAttribution'
|
|
|
|
// =============================================================================
|
|
// DSFA Card Component
|
|
// =============================================================================
|
|
|
|
interface DSFACardProps {
|
|
dsfa: {
|
|
id: string
|
|
title: string
|
|
status: string
|
|
risk_level?: string
|
|
created_at?: string
|
|
updated_at?: string
|
|
processing_description?: string
|
|
}
|
|
onDelete?: (id: string) => void
|
|
onExport?: (id: string) => void
|
|
}
|
|
|
|
export function DSFACard({ dsfa, onDelete, onExport }: DSFACardProps) {
|
|
const statusColors: Record<string, string> = {
|
|
draft: 'bg-slate-100 text-slate-700',
|
|
in_progress: 'bg-blue-100 text-blue-700',
|
|
review: 'bg-amber-100 text-amber-700',
|
|
approved: 'bg-green-100 text-green-700',
|
|
rejected: 'bg-red-100 text-red-700',
|
|
}
|
|
|
|
return React.createElement('div', {
|
|
className: 'bg-white rounded-xl border border-slate-200 p-5 hover:shadow-md transition-shadow'
|
|
},
|
|
React.createElement('div', { className: 'flex items-start justify-between mb-3' },
|
|
React.createElement('h3', { className: 'font-semibold text-slate-900 text-lg' }, dsfa.title),
|
|
React.createElement('span', {
|
|
className: `px-2.5 py-1 rounded-full text-xs font-medium ${statusColors[dsfa.status] || statusColors.draft}`
|
|
}, dsfa.status)
|
|
),
|
|
dsfa.processing_description && React.createElement('p', {
|
|
className: 'text-sm text-slate-500 mb-4 line-clamp-2'
|
|
}, dsfa.processing_description),
|
|
React.createElement('div', { className: 'flex items-center gap-2' },
|
|
React.createElement('a', {
|
|
href: `/sdk/dsfa/${dsfa.id}`,
|
|
className: 'px-3 py-1.5 bg-primary-600 text-white rounded-lg text-sm hover:bg-primary-700 transition-colors'
|
|
}, 'Bearbeiten'),
|
|
onExport && React.createElement('button', {
|
|
onClick: () => onExport(dsfa.id),
|
|
className: 'px-3 py-1.5 bg-slate-100 text-slate-700 rounded-lg text-sm hover:bg-slate-200 transition-colors'
|
|
}, 'Export'),
|
|
onDelete && React.createElement('button', {
|
|
onClick: () => onDelete(dsfa.id),
|
|
className: 'px-3 py-1.5 text-red-600 rounded-lg text-sm hover:bg-red-50 transition-colors'
|
|
}, 'Loeschen')
|
|
)
|
|
)
|
|
}
|
|
|
|
// =============================================================================
|
|
// Risk Matrix Component
|
|
// =============================================================================
|
|
|
|
// DSFARisk type matching lib/sdk/dsfa/types.ts
|
|
interface DSFARiskInput {
|
|
id: string
|
|
category?: string
|
|
description: string
|
|
likelihood: 'low' | 'medium' | 'high'
|
|
impact: 'low' | 'medium' | 'high'
|
|
risk_level?: string
|
|
affected_data?: string[]
|
|
}
|
|
|
|
interface RiskMatrixProps {
|
|
risks: DSFARiskInput[]
|
|
onRiskSelect?: (risk: DSFARiskInput) => void
|
|
onRiskClick?: (riskId: string) => void
|
|
onAddRisk?: (likelihood: 'low' | 'medium' | 'high', impact: 'low' | 'medium' | 'high') => void
|
|
selectedRiskId?: string
|
|
readOnly?: boolean
|
|
}
|
|
|
|
export function RiskMatrix({ risks, onRiskSelect, onRiskClick, onAddRisk, selectedRiskId, readOnly }: RiskMatrixProps) {
|
|
const likelihoodLevels: Array<'low' | 'medium' | 'high'> = ['high', 'medium', 'low']
|
|
const impactLevels: Array<'low' | 'medium' | 'high'> = ['low', 'medium', 'high']
|
|
const levelLabels = { low: 'Niedrig', medium: 'Mittel', high: 'Hoch' }
|
|
|
|
const cellColors: Record<string, string> = {
|
|
low: 'bg-green-100 hover:bg-green-200',
|
|
medium: 'bg-yellow-100 hover:bg-yellow-200',
|
|
high: 'bg-orange-100 hover:bg-orange-200',
|
|
very_high: 'bg-red-100 hover:bg-red-200',
|
|
}
|
|
|
|
const getRiskColor = (likelihood: 'low' | 'medium' | 'high', impact: 'low' | 'medium' | 'high') => {
|
|
const matrix: Record<string, Record<string, string>> = {
|
|
low: { low: 'low', medium: 'low', high: 'medium' },
|
|
medium: { low: 'low', medium: 'medium', high: 'high' },
|
|
high: { low: 'medium', medium: 'high', high: 'very_high' },
|
|
}
|
|
return cellColors[matrix[likelihood]?.[impact] || 'medium']
|
|
}
|
|
|
|
const handleCellClick = (likelihood: 'low' | 'medium' | 'high', impact: 'low' | 'medium' | 'high') => {
|
|
const cellRisks = risks.filter(r => r.likelihood === likelihood && r.impact === impact)
|
|
if (cellRisks.length > 0 && onRiskSelect) {
|
|
onRiskSelect(cellRisks[0])
|
|
} else if (cellRisks.length > 0 && onRiskClick) {
|
|
onRiskClick(cellRisks[0].id)
|
|
} else if (!readOnly && onAddRisk) {
|
|
onAddRisk(likelihood, impact)
|
|
}
|
|
}
|
|
|
|
return React.createElement('div', { className: 'bg-white rounded-xl border border-slate-200 p-5' },
|
|
React.createElement('h3', { className: 'font-semibold text-slate-900 mb-4' }, 'Risikomatrix'),
|
|
React.createElement('div', { className: 'text-xs text-slate-500 mb-2' }, 'Eintrittswahrscheinlichkeit ↑ | Schwere →'),
|
|
React.createElement('div', { className: 'grid grid-cols-4 gap-1' },
|
|
// Header row
|
|
React.createElement('div'),
|
|
...impactLevels.map(i => React.createElement('div', {
|
|
key: `h-${i}`,
|
|
className: 'text-center text-xs text-slate-500 py-1'
|
|
}, levelLabels[i])),
|
|
// Grid rows
|
|
...likelihoodLevels.map(likelihood =>
|
|
[
|
|
React.createElement('div', {
|
|
key: `l-${likelihood}`,
|
|
className: 'text-right text-xs text-slate-500 pr-2 flex items-center justify-end'
|
|
}, levelLabels[likelihood]),
|
|
...impactLevels.map(impact => {
|
|
const cellRisks = risks.filter(r => r.likelihood === likelihood && r.impact === impact)
|
|
const isSelected = cellRisks.some(r => r.id === selectedRiskId)
|
|
return React.createElement('div', {
|
|
key: `${likelihood}-${impact}`,
|
|
className: `aspect-square rounded ${getRiskColor(likelihood, impact)} flex items-center justify-center text-xs font-medium cursor-pointer ${isSelected ? 'ring-2 ring-purple-500' : ''}`,
|
|
onClick: () => handleCellClick(likelihood, impact)
|
|
}, cellRisks.length > 0 ? String(cellRisks.length) : (readOnly ? '' : '+'))
|
|
})
|
|
]
|
|
).flat()
|
|
)
|
|
)
|
|
}
|
|
|
|
// =============================================================================
|
|
// Approval Panel Component
|
|
// =============================================================================
|
|
|
|
interface ApprovalPanelProps {
|
|
dsfa: {
|
|
id: string
|
|
status: string
|
|
approved_by?: string
|
|
approved_at?: string
|
|
rejection_reason?: string
|
|
}
|
|
onApprove?: () => void
|
|
onReject?: (reason: string) => void
|
|
}
|
|
|
|
export function ApprovalPanel({ dsfa, onApprove, onReject }: ApprovalPanelProps) {
|
|
const [rejectionReason, setRejectionReason] = React.useState('')
|
|
const [showRejectForm, setShowRejectForm] = React.useState(false)
|
|
|
|
if (dsfa.status === 'approved') {
|
|
return React.createElement('div', {
|
|
className: 'bg-green-50 border border-green-200 rounded-xl p-5'
|
|
},
|
|
React.createElement('div', { className: 'flex items-center gap-2 mb-2' },
|
|
React.createElement('span', { className: 'text-green-600 text-lg' }, '\u2713'),
|
|
React.createElement('h3', { className: 'font-semibold text-green-800' }, 'DSFA genehmigt')
|
|
),
|
|
dsfa.approved_by && React.createElement('p', { className: 'text-sm text-green-700' },
|
|
`Genehmigt von: ${dsfa.approved_by}`
|
|
)
|
|
)
|
|
}
|
|
|
|
return React.createElement('div', {
|
|
className: 'bg-white rounded-xl border border-slate-200 p-5'
|
|
},
|
|
React.createElement('h3', { className: 'font-semibold text-slate-900 mb-4' }, 'Freigabe'),
|
|
React.createElement('div', { className: 'flex gap-3' },
|
|
onApprove && React.createElement('button', {
|
|
onClick: onApprove,
|
|
className: 'px-4 py-2 bg-green-600 text-white rounded-lg text-sm hover:bg-green-700 transition-colors'
|
|
}, 'Genehmigen'),
|
|
onReject && React.createElement('button', {
|
|
onClick: () => setShowRejectForm(true),
|
|
className: 'px-4 py-2 bg-red-600 text-white rounded-lg text-sm hover:bg-red-700 transition-colors'
|
|
}, 'Ablehnen')
|
|
),
|
|
showRejectForm && React.createElement('div', { className: 'mt-4' },
|
|
React.createElement('textarea', {
|
|
value: rejectionReason,
|
|
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => setRejectionReason(e.target.value),
|
|
placeholder: 'Ablehnungsgrund...',
|
|
className: 'w-full p-3 border border-slate-200 rounded-lg text-sm resize-none',
|
|
rows: 3
|
|
}),
|
|
React.createElement('button', {
|
|
onClick: () => { onReject?.(rejectionReason); setShowRejectForm(false) },
|
|
className: 'mt-2 px-4 py-2 bg-red-600 text-white rounded-lg text-sm hover:bg-red-700'
|
|
}, 'Ablehnung senden')
|
|
)
|
|
)
|
|
}
|