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>
This commit is contained in:
203
admin-compliance/app/sdk/notfallplan/_components/ConfigTab.tsx
Normal file
203
admin-compliance/app/sdk/notfallplan/_components/ConfigTab.tsx
Normal file
@@ -0,0 +1,203 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import type { NotfallplanConfig } from './types'
|
||||
|
||||
export function ConfigTab({
|
||||
config,
|
||||
setConfig,
|
||||
}: {
|
||||
config: NotfallplanConfig
|
||||
setConfig: React.Dispatch<React.SetStateAction<NotfallplanConfig>>
|
||||
}) {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* Meldewege */}
|
||||
<section className="bg-white rounded-lg border p-6">
|
||||
<h3 className="text-lg font-semibold mb-4">Meldewege (intern → Aufsichtsbehoerde)</h3>
|
||||
<p className="text-sm text-gray-500 mb-4">
|
||||
Definieren Sie die interne Eskalationskette bei einer Datenpanne.
|
||||
</p>
|
||||
<div className="space-y-3">
|
||||
{config.meldewege.map((step, idx) => (
|
||||
<div key={step.id} className="flex items-center gap-3 p-3 bg-gray-50 rounded-lg">
|
||||
<span className="flex-shrink-0 w-8 h-8 rounded-full bg-blue-100 text-blue-800 flex items-center justify-center text-sm font-bold">
|
||||
{step.order}
|
||||
</span>
|
||||
<div className="flex-1 grid grid-cols-3 gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={step.role}
|
||||
onChange={e => {
|
||||
const updated = [...config.meldewege]
|
||||
updated[idx] = { ...updated[idx], role: e.target.value }
|
||||
setConfig(prev => ({ ...prev, meldewege: updated }))
|
||||
}}
|
||||
placeholder="Rolle"
|
||||
className="text-sm border rounded px-2 py-1"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={step.name}
|
||||
onChange={e => {
|
||||
const updated = [...config.meldewege]
|
||||
updated[idx] = { ...updated[idx], name: e.target.value }
|
||||
setConfig(prev => ({ ...prev, meldewege: updated }))
|
||||
}}
|
||||
placeholder="Name"
|
||||
className="text-sm border rounded px-2 py-1"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={step.action}
|
||||
onChange={e => {
|
||||
const updated = [...config.meldewege]
|
||||
updated[idx] = { ...updated[idx], action: e.target.value }
|
||||
setConfig(prev => ({ ...prev, meldewege: updated }))
|
||||
}}
|
||||
placeholder="Aktion"
|
||||
className="text-sm border rounded px-2 py-1"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-shrink-0 text-xs text-gray-500">
|
||||
max. {step.maxHours}h
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Zustaendigkeiten */}
|
||||
<section className="bg-white rounded-lg border p-6">
|
||||
<h3 className="text-lg font-semibold mb-4">Zustaendigkeiten</h3>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b">
|
||||
<th className="text-left py-2 pr-4">Rolle</th>
|
||||
<th className="text-left py-2 pr-4">Name</th>
|
||||
<th className="text-left py-2 pr-4">E-Mail</th>
|
||||
<th className="text-left py-2">Telefon</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{config.zustaendigkeiten.map((z, idx) => (
|
||||
<tr key={z.id} className="border-b last:border-0">
|
||||
<td className="py-2 pr-4 font-medium">{z.role}</td>
|
||||
<td className="py-2 pr-4">
|
||||
<input
|
||||
type="text"
|
||||
value={z.name}
|
||||
onChange={e => {
|
||||
const updated = [...config.zustaendigkeiten]
|
||||
updated[idx] = { ...updated[idx], name: e.target.value }
|
||||
setConfig(prev => ({ ...prev, zustaendigkeiten: updated }))
|
||||
}}
|
||||
placeholder="Name eingeben"
|
||||
className="w-full text-sm border rounded px-2 py-1"
|
||||
/>
|
||||
</td>
|
||||
<td className="py-2 pr-4">
|
||||
<input
|
||||
type="email"
|
||||
value={z.email}
|
||||
onChange={e => {
|
||||
const updated = [...config.zustaendigkeiten]
|
||||
updated[idx] = { ...updated[idx], email: e.target.value }
|
||||
setConfig(prev => ({ ...prev, zustaendigkeiten: updated }))
|
||||
}}
|
||||
placeholder="email@example.com"
|
||||
className="w-full text-sm border rounded px-2 py-1"
|
||||
/>
|
||||
</td>
|
||||
<td className="py-2">
|
||||
<input
|
||||
type="tel"
|
||||
value={z.phone}
|
||||
onChange={e => {
|
||||
const updated = [...config.zustaendigkeiten]
|
||||
updated[idx] = { ...updated[idx], phone: e.target.value }
|
||||
setConfig(prev => ({ ...prev, zustaendigkeiten: updated }))
|
||||
}}
|
||||
placeholder="+49..."
|
||||
className="w-full text-sm border rounded px-2 py-1"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Aufsichtsbehoerde */}
|
||||
<section className="bg-white rounded-lg border p-6">
|
||||
<h3 className="text-lg font-semibold mb-4">Zustaendige Aufsichtsbehoerde</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{(['name', 'state', 'email', 'phone'] as const).map(field => (
|
||||
<div key={field}>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
{field === 'name' ? 'Name' : field === 'state' ? 'Bundesland' : field === 'email' ? 'E-Mail' : 'Telefon'}
|
||||
</label>
|
||||
<input
|
||||
type={field === 'email' ? 'email' : field === 'phone' ? 'tel' : 'text'}
|
||||
value={config.aufsichtsbehoerde[field]}
|
||||
onChange={e => setConfig(prev => ({
|
||||
...prev,
|
||||
aufsichtsbehoerde: { ...prev.aufsichtsbehoerde, [field]: e.target.value },
|
||||
}))}
|
||||
placeholder={field === 'name' ? 'z.B. LfD Niedersachsen' :
|
||||
field === 'state' ? 'z.B. Niedersachsen' :
|
||||
field === 'email' ? 'poststelle@lfd.niedersachsen.de' : '+49...'}
|
||||
className="w-full text-sm border rounded px-3 py-2"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Eskalationsstufen */}
|
||||
<section className="bg-white rounded-lg border p-6">
|
||||
<h3 className="text-lg font-semibold mb-4">Eskalationsstufen</h3>
|
||||
<div className="space-y-4">
|
||||
{config.eskalationsstufen.map((stufe) => (
|
||||
<div key={stufe.id} className="p-4 bg-gray-50 rounded-lg">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<span className={`px-2 py-1 rounded text-xs font-bold ${
|
||||
stufe.level === 1 ? 'bg-yellow-100 text-yellow-800' :
|
||||
stufe.level === 2 ? 'bg-orange-100 text-orange-800' :
|
||||
'bg-red-100 text-red-800'
|
||||
}`}>
|
||||
Stufe {stufe.level}
|
||||
</span>
|
||||
<span className="font-medium">{stufe.label}</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 mb-2">Ausloeser: {stufe.triggerCondition}</p>
|
||||
<ul className="list-disc list-inside text-sm text-gray-700">
|
||||
{stufe.actions.map((action, i) => (
|
||||
<li key={i}>{action}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Sofortmassnahmen-Checkliste */}
|
||||
<section className="bg-white rounded-lg border p-6">
|
||||
<h3 className="text-lg font-semibold mb-4">Sofortmassnahmen-Checkliste</h3>
|
||||
<p className="text-sm text-gray-500 mb-4">
|
||||
Diese Massnahmen sind sofort bei Entdeckung einer Datenpanne durchzufuehren.
|
||||
</p>
|
||||
<ul className="space-y-2">
|
||||
{config.sofortmassnahmen.map((m, idx) => (
|
||||
<li key={idx} className="flex items-start gap-3 p-2">
|
||||
<span className="flex-shrink-0 w-6 h-6 rounded border-2 border-gray-300 mt-0.5" />
|
||||
<span className="text-sm">{m}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user