Files
breakpilot-compliance/admin-compliance/app/sdk/dsfa/[id]/page.tsx
Sharang Parnerkar ef8284dff5 refactor(admin): split dsfa/[id] and notfallplan page.tsx into colocated components
dsfa/[id]/page.tsx (1893 LOC -> 350 LOC) split into 9 components:
Section1-5Editor, SDMCoverageOverview, RAGSearchPanel, AddRiskModal,
AddMitigationModal. Page is now a thin orchestrator.

notfallplan/page.tsx (1890 LOC -> 435 LOC) split into 8 modules:
types.ts, ConfigTab, IncidentsTab, TemplatesTab, ExercisesTab, Modals,
ApiSections. All under the 500-line hard cap.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 18:51:54 +02:00

351 lines
13 KiB
TypeScript

'use client'
import React, { useState, useEffect, useCallback } from 'react'
import Link from 'next/link'
import { useParams, useRouter } from 'next/navigation'
import {
DSFA,
DSFA_SECTIONS,
DSFA_STATUS_LABELS,
DSFA_RISK_LEVEL_LABELS,
} from '@/lib/sdk/dsfa/types'
import {
getDSFA,
updateDSFASection,
updateDSFA,
} from '@/lib/sdk/dsfa/api'
import {
DSFASidebar,
ThresholdAnalysisSection,
StakeholderConsultationSection,
Art36Warning,
ReviewScheduleSection,
AIUseCaseSection,
} from '@/components/sdk/dsfa'
import {
Section1Editor,
Section2Editor,
Section3Editor,
Section4Editor,
Section5Editor,
SDMCoverageOverview,
} from './_components'
// =============================================================================
// MAIN PAGE
// =============================================================================
export default function DSFAEditorPage() {
const params = useParams()
const router = useRouter()
const dsfaId = params.id as string
const [dsfa, setDSFA] = useState<DSFA | null>(null)
const [isLoading, setIsLoading] = useState(true)
const [isSaving, setIsSaving] = useState(false)
const [activeSection, setActiveSection] = useState(0)
const [saveMessage, setSaveMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null)
// Load DSFA data
useEffect(() => {
const loadDSFA = async () => {
setIsLoading(true)
try {
const data = await getDSFA(dsfaId)
setDSFA(data)
} catch (error) {
console.error('Failed to load DSFA:', error)
} finally {
setIsLoading(false)
}
}
loadDSFA()
}, [dsfaId])
const handleSectionUpdate = useCallback(async (sectionNumber: number, data: Record<string, unknown>) => {
if (!dsfa) return
setIsSaving(true)
setSaveMessage(null)
try {
const updated = await updateDSFASection(dsfaId, sectionNumber, data)
setDSFA(updated)
setSaveMessage({ type: 'success', text: 'Abschnitt gespeichert' })
setTimeout(() => setSaveMessage(null), 3000)
} catch (error) {
console.error('Failed to update section:', error)
setSaveMessage({ type: 'error', text: 'Fehler beim Speichern' })
} finally {
setIsSaving(false)
}
}, [dsfa, dsfaId])
const handleGenericUpdate = useCallback(async (data: Record<string, unknown>) => {
if (!dsfa) return
setIsSaving(true)
setSaveMessage(null)
try {
const updated = await updateDSFA(dsfaId, data as Partial<DSFA>)
setDSFA(updated)
setSaveMessage({ type: 'success', text: 'Abschnitt gespeichert' })
setTimeout(() => setSaveMessage(null), 3000)
} catch (error) {
console.error('Failed to update DSFA:', error)
setSaveMessage({ type: 'error', text: 'Fehler beim Speichern' })
} finally {
setIsSaving(false)
}
}, [dsfa, dsfaId])
if (isLoading) {
return (
<div className="flex items-center justify-center py-12">
<svg className="animate-spin w-8 h-8 text-purple-600" 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-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
</svg>
</div>
)
}
if (!dsfa) {
return (
<div className="bg-white rounded-xl border border-gray-200 p-12 text-center">
<div className="w-16 h-16 mx-auto bg-red-100 rounded-full flex items-center justify-center mb-4">
<svg className="w-8 h-8 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</div>
<h3 className="text-lg font-semibold text-gray-900">DSFA nicht gefunden</h3>
<p className="mt-2 text-gray-500">
Die angeforderte DSFA existiert nicht oder wurde geloescht.
</p>
<Link
href="/sdk/dsfa"
className="mt-4 inline-flex items-center gap-2 px-4 py-2 text-purple-600 hover:bg-purple-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="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
Zurueck zur Uebersicht
</Link>
</div>
)
}
const sectionConfig = DSFA_SECTIONS.find(s => s.number === activeSection)
const statusColors: Record<string, string> = {
draft: 'bg-gray-100 text-gray-600',
in_review: 'bg-yellow-100 text-yellow-700',
approved: 'bg-green-100 text-green-700',
rejected: 'bg-red-100 text-red-700',
needs_update: 'bg-orange-100 text-orange-700',
}
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Link
href="/sdk/dsfa"
className="p-2 text-gray-500 hover:text-gray-700 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="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
</Link>
<div>
<div className="flex items-center gap-2">
<span className={`px-2 py-1 text-xs rounded-full ${statusColors[dsfa.status]}`}>
{DSFA_STATUS_LABELS[dsfa.status]}
</span>
<span className={`px-2 py-1 text-xs rounded-full ${
dsfa.overall_risk_level === 'low' ? 'bg-green-100 text-green-700' :
dsfa.overall_risk_level === 'medium' ? 'bg-yellow-100 text-yellow-700' :
dsfa.overall_risk_level === 'high' ? 'bg-orange-100 text-orange-700' :
'bg-red-100 text-red-700'
}`}>
Risiko: {DSFA_RISK_LEVEL_LABELS[dsfa.overall_risk_level]}
</span>
{dsfa.assessment_id && (
<span className="px-2 py-1 text-xs rounded-full bg-purple-100 text-purple-700">
UCCA-verknuepft
</span>
)}
</div>
<h1 className="text-2xl font-bold text-gray-900 mt-1">{dsfa.name}</h1>
{dsfa.description && (
<p className="text-sm text-gray-500 mt-1">{dsfa.description}</p>
)}
</div>
</div>
{/* Save Message */}
{saveMessage && (
<div className={`px-4 py-2 rounded-lg text-sm ${
saveMessage.type === 'success'
? 'bg-green-100 text-green-700'
: 'bg-red-100 text-red-700'
}`}>
{saveMessage.text}
</div>
)}
</div>
{/* Main Content: Sidebar + Content Layout */}
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
{/* Left Column - Sidebar (1/4) */}
<div className="lg:col-span-1">
<DSFASidebar
dsfa={dsfa}
activeSection={activeSection}
onSectionChange={setActiveSection}
/>
</div>
{/* Right Column - Content (3/4) */}
<div className="lg:col-span-3 space-y-6">
{/* Section Content Card */}
<div className="bg-white rounded-xl border border-gray-200">
{/* Section Header */}
{sectionConfig && (
<div className="p-4 bg-gray-50 border-b border-gray-200 rounded-t-xl">
<div className="flex items-start justify-between">
<div>
<h2 className="text-lg font-semibold text-gray-900">
{sectionConfig.number}. {sectionConfig.titleDE}
</h2>
<p className="text-sm text-gray-500 mt-1">{sectionConfig.description}</p>
</div>
<span className="px-2 py-1 bg-blue-50 text-blue-600 rounded text-xs">
{sectionConfig.gdprRef}
</span>
</div>
</div>
)}
{/* Section Content */}
<div className="p-6">
{activeSection === 0 && (
<ThresholdAnalysisSection dsfa={dsfa} onUpdate={handleGenericUpdate} isSubmitting={isSaving} />
)}
{activeSection === 1 && (
<Section1Editor dsfa={dsfa} onUpdate={(data) => handleSectionUpdate(1, data)} isSubmitting={isSaving} />
)}
{activeSection === 2 && (
<Section2Editor dsfa={dsfa} onUpdate={(data) => handleSectionUpdate(2, data)} isSubmitting={isSaving} />
)}
{activeSection === 3 && (
<Section3Editor dsfa={dsfa} onUpdate={(data) => handleSectionUpdate(3, data)} isSubmitting={isSaving} />
)}
{activeSection === 4 && (
<Section4Editor dsfa={dsfa} onUpdate={(data) => handleSectionUpdate(4, data)} isSubmitting={isSaving} />
)}
{/* SDM Coverage Overview (shown in Section 3 and 4) */}
{(activeSection === 3 || activeSection === 4) && (dsfa.risks?.length > 0 || dsfa.mitigations?.length > 0) && (
<SDMCoverageOverview dsfa={dsfa} />
)}
{activeSection === 5 && (
<StakeholderConsultationSection dsfa={dsfa} onUpdate={handleGenericUpdate} isSubmitting={isSaving} />
)}
{activeSection === 6 && (
<div className="space-y-6">
<Section5Editor dsfa={dsfa} onUpdate={(data) => handleSectionUpdate(5, data)} isSubmitting={isSaving} />
<div className="border-t border-gray-200 pt-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Art. 36 Behoerdenkonsultation</h3>
<Art36Warning dsfa={dsfa} onUpdate={handleGenericUpdate} isSubmitting={isSaving} />
</div>
</div>
)}
{activeSection === 7 && (
<ReviewScheduleSection dsfa={dsfa} onUpdate={handleGenericUpdate} isSubmitting={isSaving} />
)}
{activeSection === 8 && (
<AIUseCaseSection dsfa={dsfa} onUpdate={handleGenericUpdate} isSubmitting={isSaving} />
)}
</div>
</div>
{/* Bottom Actions Row */}
<BottomActions
activeSection={activeSection}
setActiveSection={setActiveSection}
dsfa={dsfa}
/>
</div>
</div>
</div>
)
}
function BottomActions({
activeSection,
setActiveSection,
dsfa,
}: {
activeSection: number
setActiveSection: (n: number) => void
dsfa: DSFA
}) {
return (
<div className="flex items-center justify-between bg-white rounded-xl border border-gray-200 p-4">
{/* Navigation */}
<div className="flex items-center gap-2">
{activeSection > 0 && (
<button
onClick={() => setActiveSection(activeSection - 1)}
className="flex items-center gap-2 px-4 py-2 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="M15 19l-7-7 7-7" />
</svg>
Zurueck
</button>
)}
{activeSection < 8 && (
<button
onClick={() => setActiveSection(activeSection + 1)}
className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
>
Weiter
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</button>
)}
</div>
{/* Quick Info */}
<div className="flex items-center gap-4 text-sm text-gray-500">
<span>Risiken: {(dsfa.risks || []).length}</span>
<span>Massnahmen: {(dsfa.mitigations || []).length}</span>
<span>KI-Module: {(dsfa.ai_use_case_modules || []).length}</span>
<span>Version: {dsfa.version || 1}</span>
</div>
{/* Export */}
<div className="flex items-center gap-2">
<button className="flex items-center gap-2 px-4 py-2 text-gray-700 hover:bg-gray-100 rounded-lg transition-colors">
<svg className="w-5 h-5 text-red-500" fill="currentColor" viewBox="0 0 24 24">
<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8l-6-6z"/>
<path d="M14 2v6h6M16 13H8M16 17H8M10 9H8"/>
</svg>
PDF
</button>
<button className="flex items-center gap-2 px-4 py-2 text-gray-700 hover:bg-gray-100 rounded-lg transition-colors">
<svg className="w-5 h-5 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
JSON
</button>
</div>
</div>
)
}