refactor(admin): split requirements page.tsx into colocated components
Break 838-line page.tsx into _types.ts, _data.ts (templates),
_components/{AddRequirementForm,RequirementCard,LoadingSkeleton}.tsx,
and _hooks/useRequirementsData.ts. page.tsx is now 246 LOC (wiring
only). No behavior changes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
'use client'
|
||||
|
||||
import { RequirementStatus } from '@/lib/sdk'
|
||||
import { DisplayRequirement } from '../_types'
|
||||
|
||||
export function RequirementCard({
|
||||
requirement,
|
||||
onStatusChange,
|
||||
onDelete,
|
||||
expanded,
|
||||
onToggleDetails,
|
||||
linkedControls,
|
||||
}: {
|
||||
requirement: DisplayRequirement
|
||||
onStatusChange: (status: RequirementStatus) => void
|
||||
onDelete: () => void
|
||||
expanded: boolean
|
||||
onToggleDetails: () => void
|
||||
linkedControls: { id: string; name: string }[]
|
||||
}) {
|
||||
const priorityColors = {
|
||||
critical: 'bg-red-100 text-red-700',
|
||||
high: 'bg-orange-100 text-orange-700',
|
||||
medium: 'bg-yellow-100 text-yellow-700',
|
||||
low: 'bg-green-100 text-green-700',
|
||||
}
|
||||
|
||||
const statusColors = {
|
||||
compliant: 'bg-green-100 text-green-700 border-green-200',
|
||||
partial: 'bg-yellow-100 text-yellow-700 border-yellow-200',
|
||||
'non-compliant': 'bg-red-100 text-red-700 border-red-200',
|
||||
'not-applicable': 'bg-gray-100 text-gray-500 border-gray-200',
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`bg-white rounded-xl border-2 p-6 ${statusColors[requirement.displayStatus]}`}>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="px-2 py-1 text-xs bg-gray-100 text-gray-700 rounded font-mono">
|
||||
{requirement.code}
|
||||
</span>
|
||||
<span className={`px-2 py-1 text-xs rounded-full ${priorityColors[requirement.priority]}`}>
|
||||
{requirement.priority === 'critical' ? 'Kritisch' :
|
||||
requirement.priority === 'high' ? 'Hoch' :
|
||||
requirement.priority === 'medium' ? 'Mittel' : 'Niedrig'}
|
||||
</span>
|
||||
<span className="px-2 py-1 text-xs bg-blue-50 text-blue-600 rounded">
|
||||
{requirement.regulation}
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-gray-900">{requirement.title}</h3>
|
||||
<p className="text-sm text-gray-500 mt-1">{requirement.description}</p>
|
||||
<p className="text-xs text-gray-400 mt-2">Quelle: {requirement.source}</p>
|
||||
</div>
|
||||
<select
|
||||
value={requirement.status}
|
||||
onChange={(e) => onStatusChange(e.target.value as RequirementStatus)}
|
||||
className={`px-3 py-1 text-sm rounded-full border ${statusColors[requirement.displayStatus]}`}
|
||||
>
|
||||
<option value="NOT_STARTED">Nicht begonnen</option>
|
||||
<option value="IN_PROGRESS">In Bearbeitung</option>
|
||||
<option value="IMPLEMENTED">Implementiert</option>
|
||||
<option value="VERIFIED">Verifiziert</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 pt-4 border-t border-gray-100 flex items-center justify-between">
|
||||
<div className="flex items-center gap-4 text-sm text-gray-500">
|
||||
<span>{requirement.controlsLinked} Kontrollen</span>
|
||||
<span>{requirement.evidenceCount} Nachweise</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={onToggleDetails}
|
||||
className="text-sm text-purple-600 hover:text-purple-700 font-medium"
|
||||
>
|
||||
{expanded ? 'Details ausblenden' : 'Details anzeigen'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{expanded && (
|
||||
<div className="mt-4 pt-4 border-t border-gray-200 space-y-3">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-1">Vollstaendige Beschreibung</h4>
|
||||
<p className="text-sm text-gray-600">{requirement.description || 'Keine Beschreibung vorhanden.'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-1">Zugeordnete Kontrollen ({linkedControls.length})</h4>
|
||||
{linkedControls.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{linkedControls.map(c => (
|
||||
<span key={c.id} className="px-2 py-1 text-xs bg-green-50 text-green-700 rounded">{c.name}</span>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-gray-400">Keine Kontrollen zugeordnet</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-1">Status-Historie</h4>
|
||||
<div className="flex items-center gap-2 text-sm text-gray-500">
|
||||
<span className={`px-2 py-0.5 text-xs rounded-full ${
|
||||
requirement.displayStatus === 'compliant' ? 'bg-green-100 text-green-700' :
|
||||
requirement.displayStatus === 'partial' ? 'bg-yellow-100 text-yellow-700' :
|
||||
'bg-red-100 text-red-700'
|
||||
}`}>
|
||||
{requirement.status === 'NOT_STARTED' ? 'Nicht begonnen' :
|
||||
requirement.status === 'IN_PROGRESS' ? 'In Bearbeitung' :
|
||||
requirement.status === 'IMPLEMENTED' ? 'Implementiert' : 'Verifiziert'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onDelete}
|
||||
className="px-3 py-1 text-sm text-red-600 hover:bg-red-50 border border-red-200 rounded-lg transition-colors"
|
||||
>
|
||||
Loeschen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user