controls/page.tsx 840→211 LOC — extracted StatsCards, FilterBar, ControlCard, AddControlForm, RAGPanel, LoadingSkeleton to _components/; useControlsData, useRAGSuggestions to _hooks/; shared types to _types.ts. dsr/[requestId]/page.tsx 854→172 LOC — extracted detail panels and timeline components to _components/. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
169 lines
6.3 KiB
TypeScript
169 lines
6.3 KiB
TypeScript
'use client'
|
|
|
|
import React, { useState } from 'react'
|
|
import { ImplementationStatus } from '@/lib/sdk'
|
|
import { DisplayControl } from '../_types'
|
|
|
|
export function ControlCard({
|
|
control,
|
|
onStatusChange,
|
|
onEffectivenessChange,
|
|
onLinkEvidence,
|
|
}: {
|
|
control: DisplayControl
|
|
onStatusChange: (status: ImplementationStatus) => void
|
|
onEffectivenessChange: (effectivenessPercent: number) => void
|
|
onLinkEvidence: () => void
|
|
}) {
|
|
const [showEffectivenessSlider, setShowEffectivenessSlider] = useState(false)
|
|
|
|
const typeColors = {
|
|
preventive: 'bg-blue-100 text-blue-700',
|
|
detective: 'bg-purple-100 text-purple-700',
|
|
corrective: 'bg-orange-100 text-orange-700',
|
|
}
|
|
|
|
const categoryColors = {
|
|
technical: 'bg-green-100 text-green-700',
|
|
organizational: 'bg-yellow-100 text-yellow-700',
|
|
physical: 'bg-gray-100 text-gray-700',
|
|
}
|
|
|
|
const statusColors = {
|
|
implemented: 'border-green-200 bg-green-50',
|
|
partial: 'border-yellow-200 bg-yellow-50',
|
|
planned: 'border-blue-200 bg-blue-50',
|
|
'not-implemented': 'border-red-200 bg-red-50',
|
|
}
|
|
|
|
const statusLabels = {
|
|
implemented: 'Implementiert',
|
|
partial: 'Teilweise',
|
|
planned: 'Geplant',
|
|
'not-implemented': 'Nicht implementiert',
|
|
}
|
|
|
|
return (
|
|
<div className={`bg-white rounded-xl border-2 p-6 ${statusColors[control.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">
|
|
{control.code}
|
|
</span>
|
|
<span className={`px-2 py-1 text-xs rounded-full ${typeColors[control.displayType]}`}>
|
|
{control.displayType === 'preventive' ? 'Praeventiv' :
|
|
control.displayType === 'detective' ? 'Detektiv' : 'Korrektiv'}
|
|
</span>
|
|
<span className={`px-2 py-1 text-xs rounded-full ${categoryColors[control.displayCategory]}`}>
|
|
{control.displayCategory === 'technical' ? 'Technisch' :
|
|
control.displayCategory === 'organizational' ? 'Organisatorisch' : 'Physisch'}
|
|
</span>
|
|
</div>
|
|
<h3 className="text-lg font-semibold text-gray-900">{control.name}</h3>
|
|
<p className="text-sm text-gray-500 mt-1">{control.description}</p>
|
|
</div>
|
|
<select
|
|
value={control.implementationStatus}
|
|
onChange={(e) => onStatusChange(e.target.value as ImplementationStatus)}
|
|
className={`px-3 py-1 text-sm rounded-full border ${statusColors[control.displayStatus]}`}
|
|
>
|
|
<option value="NOT_IMPLEMENTED">Nicht implementiert</option>
|
|
<option value="PARTIAL">Teilweise</option>
|
|
<option value="IMPLEMENTED">Implementiert</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div className="mt-4">
|
|
<div
|
|
className="flex items-center justify-between text-sm mb-1 cursor-pointer"
|
|
onClick={() => setShowEffectivenessSlider(!showEffectivenessSlider)}
|
|
>
|
|
<span className="text-gray-500">Wirksamkeit</span>
|
|
<span className="font-medium">{control.effectivenessPercent}%</span>
|
|
</div>
|
|
<div className="h-2 bg-gray-200 rounded-full overflow-hidden">
|
|
<div
|
|
className={`h-full rounded-full transition-all ${
|
|
control.effectivenessPercent >= 80 ? 'bg-green-500' :
|
|
control.effectivenessPercent >= 50 ? 'bg-yellow-500' : 'bg-red-500'
|
|
}`}
|
|
style={{ width: `${control.effectivenessPercent}%` }}
|
|
/>
|
|
</div>
|
|
{showEffectivenessSlider && (
|
|
<div className="mt-2">
|
|
<input
|
|
type="range"
|
|
min={0}
|
|
max={100}
|
|
value={control.effectivenessPercent}
|
|
onChange={(e) => onEffectivenessChange(Number(e.target.value))}
|
|
className="w-full"
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="mt-4 pt-4 border-t border-gray-100 flex items-center justify-between text-sm">
|
|
<div className="text-gray-500">
|
|
<span>Verantwortlich: </span>
|
|
<span className="font-medium text-gray-700">{control.owner || 'Nicht zugewiesen'}</span>
|
|
</div>
|
|
<div className="text-gray-500">
|
|
Letzte Pruefung: {control.lastReview.toLocaleDateString('de-DE')}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-3 flex items-center justify-between">
|
|
<div className="flex items-center gap-1 flex-wrap">
|
|
{control.linkedRequirements.slice(0, 3).map(req => (
|
|
<span key={req} className="px-2 py-0.5 text-xs bg-gray-100 text-gray-600 rounded">
|
|
{req}
|
|
</span>
|
|
))}
|
|
{control.linkedRequirements.length > 3 && (
|
|
<span className="px-2 py-0.5 text-xs bg-gray-100 text-gray-600 rounded">
|
|
+{control.linkedRequirements.length - 3}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<span className={`px-3 py-1 text-xs rounded-full ${
|
|
control.displayStatus === 'implemented' ? 'bg-green-100 text-green-700' :
|
|
control.displayStatus === 'partial' ? 'bg-yellow-100 text-yellow-700' :
|
|
control.displayStatus === 'planned' ? 'bg-blue-100 text-blue-700' : 'bg-red-100 text-red-700'
|
|
}`}>
|
|
{statusLabels[control.displayStatus]}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Linked Evidence */}
|
|
{control.linkedEvidence.length > 0 && (
|
|
<div className="mt-3 pt-3 border-t border-gray-100">
|
|
<span className="text-xs text-gray-500 mb-1 block">Nachweise:</span>
|
|
<div className="flex items-center gap-1 flex-wrap">
|
|
{control.linkedEvidence.map(ev => (
|
|
<span key={ev.id} className={`px-2 py-0.5 text-xs rounded ${
|
|
ev.status === 'valid' ? 'bg-green-50 text-green-700' :
|
|
ev.status === 'expired' ? 'bg-red-50 text-red-700' :
|
|
'bg-yellow-50 text-yellow-700'
|
|
}`}>
|
|
{ev.title}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="mt-3 pt-3 border-t border-gray-100">
|
|
<button
|
|
onClick={onLinkEvidence}
|
|
className="text-sm text-purple-600 hover:text-purple-700 font-medium"
|
|
>
|
|
Evidence verknuepfen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|