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>
204 lines
8.4 KiB
TypeScript
204 lines
8.4 KiB
TypeScript
'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>
|
|
)
|
|
}
|