Files
breakpilot-compliance/admin-compliance/app/sdk/requirements/page.tsx
Sharang Parnerkar 1c1af4e38d 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>
2026-04-15 08:22:24 +02:00

247 lines
11 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import { useState } from 'react'
import { StepHeader, STEP_EXPLANATIONS } from '@/components/sdk/StepHeader'
import { DisplayRequirement, mapCriticalityToPriority, mapStatusToDisplayStatus, AddRequirementData } from './_types'
import { requirementTemplates } from './_data'
import { AddRequirementForm } from './_components/AddRequirementForm'
import { RequirementCard } from './_components/RequirementCard'
import { LoadingSkeleton } from './_components/LoadingSkeleton'
import { useRequirementsData } from './_hooks/useRequirementsData'
export default function RequirementsPage() {
const {
state,
loading,
error,
setError,
ragExtracting,
ragResult,
setRagResult,
extractFromRAG,
handleStatusChange,
handleDeleteRequirement,
handleAddRequirement,
} = useRequirementsData()
const [filter, setFilter] = useState<string>('all')
const [searchQuery, setSearchQuery] = useState('')
const [showAddForm, setShowAddForm] = useState(false)
const [expandedId, setExpandedId] = useState<string | null>(null)
const onSubmitAdd = async (data: AddRequirementData) => {
const ok = await handleAddRequirement(data)
if (ok) setShowAddForm(false)
}
// Convert SDK requirements to display requirements
const displayRequirements: DisplayRequirement[] = state.requirements.map(req => {
const template = requirementTemplates.find(t => t.id === req.id)
const linkedControls = state.controls.filter(c => c.evidence.includes(req.id))
const linkedEvidence = state.evidence.filter(e => e.controlId && linkedControls.some(c => c.id === e.controlId))
return {
...req,
code: template?.code || req.id,
source: template?.source || `${req.regulation} ${req.article}`,
category: template?.category || req.regulation,
priority: mapCriticalityToPriority(req.criticality),
displayStatus: mapStatusToDisplayStatus(req.status),
controlsLinked: linkedControls.length,
evidenceCount: linkedEvidence.length,
}
})
const filteredRequirements = displayRequirements.filter(req => {
const matchesFilter = filter === 'all' ||
req.displayStatus === filter ||
req.priority === filter
const matchesSearch = searchQuery === '' ||
req.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
req.code.toLowerCase().includes(searchQuery.toLowerCase())
return matchesFilter && matchesSearch
})
const compliantCount = displayRequirements.filter(r => r.displayStatus === 'compliant').length
const partialCount = displayRequirements.filter(r => r.displayStatus === 'partial').length
const nonCompliantCount = displayRequirements.filter(r => r.displayStatus === 'non-compliant').length
const stepInfo = STEP_EXPLANATIONS['requirements']
return (
<div className="space-y-6">
{/* Step Header */}
<StepHeader
stepId="requirements"
title={stepInfo.title}
description={stepInfo.description}
explanation={stepInfo.explanation}
tips={stepInfo.tips}
>
<div className="flex items-center gap-2">
<button
onClick={extractFromRAG}
disabled={ragExtracting}
className="flex items-center gap-2 px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 disabled:opacity-60 transition-colors"
>
{ragExtracting ? (
<svg className="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8H4z" />
</svg>
) : (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.347.347a3.5 3.5 0 01-4.95 0l-.347-.347z" />
</svg>
)}
Aus RAG extrahieren
</button>
<button
onClick={() => setShowAddForm(true)}
className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 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="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Anforderung hinzufuegen
</button>
</div>
</StepHeader>
{/* RAG Extraction Result Banner */}
{ragResult && (
<div className={`flex items-center justify-between p-3 rounded-lg border ${ragResult.created > 0 ? 'bg-green-50 border-green-200' : 'bg-blue-50 border-blue-200'}`}>
<span className="text-sm">
{ragResult.created > 0 ? '✅' : ''} {ragResult.message}
</span>
<button onClick={() => setRagResult(null)} className="text-gray-400 hover:text-gray-600 ml-4">&times;</button>
</div>
)}
{/* Add Form */}
{showAddForm && (
<AddRequirementForm
onSubmit={onSubmitAdd}
onCancel={() => setShowAddForm(false)}
/>
)}
{/* Error Banner */}
{error && (
<div className="p-4 bg-red-50 border border-red-200 rounded-lg text-red-700 flex items-center justify-between">
<span>{error}</span>
<button onClick={() => setError(null)} className="text-red-500 hover:text-red-700">&times;</button>
</div>
)}
{/* Module Alert */}
{state.modules.length === 0 && !loading && (
<div className="bg-amber-50 border border-amber-200 rounded-xl p-4">
<div className="flex items-start gap-3">
<svg className="w-5 h-5 text-amber-600 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<div>
<h4 className="font-medium text-amber-800">Keine Module aktiviert</h4>
<p className="text-sm text-amber-700 mt-1">
Bitte aktivieren Sie zuerst Compliance-Module, um die zugehoerigen Anforderungen zu laden.
</p>
</div>
</div>
</div>
)}
{/* Stats */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="text-sm text-gray-500">Gesamt</div>
<div className="text-3xl font-bold text-gray-900">{displayRequirements.length}</div>
</div>
<div className="bg-white rounded-xl border border-green-200 p-6">
<div className="text-sm text-green-600">Konform</div>
<div className="text-3xl font-bold text-green-600">{compliantCount}</div>
</div>
<div className="bg-white rounded-xl border border-yellow-200 p-6">
<div className="text-sm text-yellow-600">In Bearbeitung</div>
<div className="text-3xl font-bold text-yellow-600">{partialCount}</div>
</div>
<div className="bg-white rounded-xl border border-red-200 p-6">
<div className="text-sm text-red-600">Offen</div>
<div className="text-3xl font-bold text-red-600">{nonCompliantCount}</div>
</div>
</div>
{/* Search and Filter */}
<div className="flex items-center gap-4">
<div className="flex-1 relative">
<svg className="w-5 h-5 text-gray-400 absolute left-3 top-1/2 -translate-y-1/2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Anforderungen durchsuchen..."
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
/>
</div>
<div className="flex items-center gap-2">
{['all', 'compliant', 'partial', 'non-compliant', 'critical'].map(f => (
<button
key={f}
onClick={() => setFilter(f)}
className={`px-3 py-1 text-sm rounded-full transition-colors ${
filter === f
? 'bg-purple-600 text-white'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
}`}
>
{f === 'all' ? 'Alle' :
f === 'compliant' ? 'Konform' :
f === 'partial' ? 'Teilweise' :
f === 'non-compliant' ? 'Offen' : 'Kritisch'}
</button>
))}
</div>
</div>
{/* Loading State */}
{loading && <LoadingSkeleton />}
{/* Requirements List */}
{!loading && (
<div className="space-y-4">
{filteredRequirements.map(requirement => {
const linkedControls = state.controls
.filter(c => c.evidence.includes(requirement.id))
.map(c => ({ id: c.id, name: c.name }))
return (
<RequirementCard
key={requirement.id}
requirement={requirement}
onStatusChange={(status) => handleStatusChange(requirement.id, status)}
onDelete={() => handleDeleteRequirement(requirement.id)}
expanded={expandedId === requirement.id}
onToggleDetails={() => setExpandedId(expandedId === requirement.id ? null : requirement.id)}
linkedControls={linkedControls}
/>
)
})}
</div>
)}
{!loading && filteredRequirements.length === 0 && state.modules.length > 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-12 text-center">
<div className="w-16 h-16 mx-auto bg-gray-100 rounded-full flex items-center justify-center mb-4">
<svg className="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
</div>
<h3 className="text-lg font-semibold text-gray-900">Keine Anforderungen gefunden</h3>
<p className="mt-2 text-gray-500">Passen Sie die Suche oder den Filter an.</p>
</div>
)}
</div>
)
}