Extract components and hooks from oversized pages into colocated _components/ and _hooks/ subdirectories to enforce the 500-LOC hard cap. page.tsx files reduced to 205, 121, and 136 LOC respectively. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
153 lines
5.8 KiB
TypeScript
153 lines
5.8 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import { Risk, RiskStatus } from '@/lib/sdk'
|
|
|
|
export function RiskCard({
|
|
risk,
|
|
onEdit,
|
|
onDelete,
|
|
onStatusChange,
|
|
}: {
|
|
risk: Risk
|
|
onEdit: () => void
|
|
onDelete: () => void
|
|
onStatusChange: (status: RiskStatus) => void
|
|
}) {
|
|
const [showMitigations, setShowMitigations] = useState(false)
|
|
const severityColors = {
|
|
CRITICAL: 'border-red-200 bg-red-50',
|
|
HIGH: 'border-orange-200 bg-orange-50',
|
|
MEDIUM: 'border-yellow-200 bg-yellow-50',
|
|
LOW: 'border-green-200 bg-green-50',
|
|
}
|
|
|
|
return (
|
|
<div className={`bg-white rounded-xl border-2 p-6 ${severityColors[risk.severity]}`}>
|
|
<div className="flex items-start justify-between">
|
|
<div>
|
|
<div className="flex items-center gap-2">
|
|
<h4 className="font-semibold text-gray-900">{risk.title}</h4>
|
|
<span
|
|
className={`px-2 py-0.5 text-xs rounded-full ${
|
|
risk.severity === 'CRITICAL'
|
|
? 'bg-red-100 text-red-700'
|
|
: risk.severity === 'HIGH'
|
|
? 'bg-orange-100 text-orange-700'
|
|
: risk.severity === 'MEDIUM'
|
|
? 'bg-yellow-100 text-yellow-700'
|
|
: 'bg-green-100 text-green-700'
|
|
}`}
|
|
>
|
|
{risk.severity}
|
|
</span>
|
|
</div>
|
|
<p className="text-sm text-gray-500 mt-1">{risk.description}</p>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
onClick={onEdit}
|
|
className="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
|
|
>
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
<button
|
|
onClick={onDelete}
|
|
className="p-2 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors"
|
|
>
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-4 grid grid-cols-4 gap-4 text-sm">
|
|
<div>
|
|
<span className="text-gray-500">Wahrscheinlichkeit:</span>
|
|
<span className="ml-2 font-medium">{risk.likelihood}/5</span>
|
|
</div>
|
|
<div>
|
|
<span className="text-gray-500">Auswirkung:</span>
|
|
<span className="ml-2 font-medium">{risk.impact}/5</span>
|
|
</div>
|
|
<div>
|
|
<span className="text-gray-500">Inherent:</span>
|
|
<span className="ml-2 font-medium">{risk.inherentRiskScore}</span>
|
|
</div>
|
|
<div>
|
|
<span className="text-gray-500">Residual:</span>
|
|
<span className={`ml-2 font-medium ${
|
|
risk.residualRiskScore < risk.inherentRiskScore ? 'text-green-600' : ''
|
|
}`}>
|
|
{risk.residualRiskScore}
|
|
</span>
|
|
{risk.residualRiskScore < risk.inherentRiskScore && (
|
|
<span className="ml-1 text-xs text-green-600">
|
|
({risk.inherentRiskScore} → {risk.residualRiskScore})
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-4 pt-4 border-t border-gray-200 flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-sm text-gray-500">Status:</span>
|
|
<select
|
|
value={risk.status}
|
|
onChange={(e) => onStatusChange(e.target.value as RiskStatus)}
|
|
className="px-2 py-1 text-sm border border-gray-300 rounded-lg"
|
|
>
|
|
<option value="IDENTIFIED">Identifiziert</option>
|
|
<option value="ASSESSED">Bewertet</option>
|
|
<option value="MITIGATED">Mitigiert</option>
|
|
<option value="ACCEPTED">Akzeptiert</option>
|
|
<option value="CLOSED">Geschlossen</option>
|
|
</select>
|
|
</div>
|
|
{risk.mitigation.length > 0 && (
|
|
<button
|
|
onClick={() => setShowMitigations(!showMitigations)}
|
|
className="text-sm text-purple-600 hover:text-purple-700"
|
|
>
|
|
{showMitigations ? 'Mitigationen ausblenden' : `${risk.mitigation.length} Mitigation(en) anzeigen`}
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{showMitigations && risk.mitigation.length > 0 && (
|
|
<div className="mt-3 space-y-2">
|
|
{risk.mitigation.map((m, idx) => (
|
|
<div key={idx} className="p-3 bg-gray-50 rounded-lg text-sm">
|
|
<div className="flex items-center justify-between">
|
|
<span className="font-medium text-gray-700">{m.controlId || `Mitigation ${idx + 1}`}</span>
|
|
<span className={`px-2 py-0.5 text-xs rounded-full ${
|
|
m.status === 'IMPLEMENTED' ? 'bg-green-100 text-green-700' :
|
|
m.status === 'IN_PROGRESS' ? 'bg-yellow-100 text-yellow-700' :
|
|
'bg-gray-100 text-gray-500'
|
|
}`}>
|
|
{m.status === 'IMPLEMENTED' ? 'Implementiert' :
|
|
m.status === 'IN_PROGRESS' ? 'In Bearbeitung' : m.status || 'Geplant'}
|
|
</span>
|
|
</div>
|
|
{m.description && <p className="text-gray-500 mt-1">{m.description}</p>}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|