From 5f55692ef05c54068692f1bfd3def42c0b5f1430 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Mon, 9 Feb 2026 10:09:36 +0100 Subject: [PATCH] fix(admin-v2): Add missing utils and DSFA components for build - Add cn() utility function for className merging - Add DSFACard, RiskMatrix, ApprovalPanel components Co-Authored-By: Claude Opus 4.6 --- admin-v2/components/sdk/dsfa/index.ts | 184 ++++++++++++++++++++++++++ admin-v2/lib/utils.ts | 7 + 2 files changed, 191 insertions(+) create mode 100644 admin-v2/components/sdk/dsfa/index.ts create mode 100644 admin-v2/lib/utils.ts diff --git a/admin-v2/components/sdk/dsfa/index.ts b/admin-v2/components/sdk/dsfa/index.ts new file mode 100644 index 0000000..b250257 --- /dev/null +++ b/admin-v2/components/sdk/dsfa/index.ts @@ -0,0 +1,184 @@ +'use client' + +import React from 'react' + +// ============================================================================= +// 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 = { + 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 +// ============================================================================= + +interface RiskMatrixProps { + risks: Array<{ + id: string + title: string + probability: number + impact: number + risk_level?: string + }> + onRiskClick?: (riskId: string) => void +} + +export function RiskMatrix({ risks, onRiskClick }: RiskMatrixProps) { + const levels = [1, 2, 3, 4, 5] + const levelLabels = ['Sehr gering', 'Gering', 'Mittel', 'Hoch', 'Sehr hoch'] + const cellColors: Record = { + low: 'bg-green-100 hover:bg-green-200', + medium: 'bg-yellow-100 hover:bg-yellow-200', + high: 'bg-orange-100 hover:bg-orange-200', + critical: 'bg-red-100 hover:bg-red-200', + } + + const getRiskColor = (prob: number, impact: number) => { + const score = prob * impact + if (score <= 4) return cellColors.low + if (score <= 9) return cellColors.medium + if (score <= 16) return cellColors.high + return cellColors.critical + } + + 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: 'grid grid-cols-6 gap-1' }, + React.createElement('div'), + ...levels.map(l => React.createElement('div', { + key: `h-${l}`, + className: 'text-center text-xs text-slate-500 py-1' + }, levelLabels[l - 1])), + ...levels.reverse().map(prob => + [ + React.createElement('div', { + key: `l-${prob}`, + className: 'text-right text-xs text-slate-500 pr-2 flex items-center justify-end' + }, levelLabels[prob - 1]), + ...levels.map(impact => { + const cellRisks = risks.filter(r => r.probability === prob && r.impact === impact) + return React.createElement('div', { + key: `${prob}-${impact}`, + className: `aspect-square rounded ${getRiskColor(prob, impact)} flex items-center justify-center text-xs font-medium cursor-pointer`, + onClick: () => cellRisks[0] && onRiskClick?.(cellRisks[0].id) + }, cellRisks.length > 0 ? String(cellRisks.length) : '') + }) + ] + ).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) => 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') + ) + ) +} diff --git a/admin-v2/lib/utils.ts b/admin-v2/lib/utils.ts new file mode 100644 index 0000000..aae9064 --- /dev/null +++ b/admin-v2/lib/utils.ts @@ -0,0 +1,7 @@ +/** + * Utility function for merging class names. + * Filters out falsy values and joins with spaces. + */ +export function cn(...classes: (string | undefined | null | false)[]): string { + return classes.filter(Boolean).join(' ') +}