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>
138 lines
6.9 KiB
TypeScript
138 lines
6.9 KiB
TypeScript
'use client'
|
|
|
|
import React from 'react'
|
|
import type { ApiContact, ApiScenario, ApiExercise } from './types'
|
|
|
|
export function ApiContactsSection({ apiContacts, apiLoading, onAdd, onDelete }: {
|
|
apiContacts: ApiContact[]; apiLoading: boolean; onAdd: () => void; onDelete: (id: string) => void
|
|
}) {
|
|
return (
|
|
<div className="bg-white rounded-lg border p-5">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<div>
|
|
<h3 className="text-base font-semibold">Notfallkontakte (Datenbank)</h3>
|
|
<p className="text-sm text-gray-500">Zentral gespeicherte Kontakte fuer den Notfall</p>
|
|
</div>
|
|
<button onClick={onAdd} className="px-3 py-1.5 bg-blue-600 text-white rounded-lg text-sm font-medium hover:bg-blue-700">
|
|
+ Kontakt hinzufuegen
|
|
</button>
|
|
</div>
|
|
{apiLoading ? (
|
|
<div className="text-sm text-gray-500 py-4 text-center">Lade Kontakte...</div>
|
|
) : apiContacts.length === 0 ? (
|
|
<div className="text-sm text-gray-400 py-4 text-center">Noch keine Kontakte hinterlegt</div>
|
|
) : (
|
|
<div className="space-y-2">
|
|
{apiContacts.map(contact => (
|
|
<div key={contact.id} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
|
<div className="flex items-center gap-3">
|
|
{contact.is_primary && <span className="text-xs bg-blue-100 text-blue-700 px-2 py-0.5 rounded-full font-medium">Primaer</span>}
|
|
{contact.available_24h && <span className="text-xs bg-green-100 text-green-700 px-2 py-0.5 rounded-full font-medium">24/7</span>}
|
|
<div>
|
|
<span className="font-medium text-sm">{contact.name}</span>
|
|
{contact.role && <span className="text-gray-500 text-sm ml-2">({contact.role})</span>}
|
|
<div className="text-xs text-gray-400">{contact.email} {contact.phone && `| ${contact.phone}`}</div>
|
|
</div>
|
|
</div>
|
|
<button onClick={() => onDelete(contact.id)} className="text-xs text-red-500 hover:text-red-700 px-2 py-1 rounded hover:bg-red-50">
|
|
Loeschen
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function ApiScenariosSection({ apiScenarios, apiLoading, onAdd, onDelete }: {
|
|
apiScenarios: ApiScenario[]; apiLoading: boolean; onAdd: () => void; onDelete: (id: string) => void
|
|
}) {
|
|
return (
|
|
<div className="bg-white rounded-lg border p-5">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<div>
|
|
<h3 className="text-base font-semibold">Notfallszenarien (Datenbank)</h3>
|
|
<p className="text-sm text-gray-500">Definierte Szenarien und Reaktionsplaene</p>
|
|
</div>
|
|
<button onClick={onAdd} className="px-3 py-1.5 bg-blue-600 text-white rounded-lg text-sm font-medium hover:bg-blue-700">
|
|
+ Szenario hinzufuegen
|
|
</button>
|
|
</div>
|
|
{apiLoading ? (
|
|
<div className="text-sm text-gray-500 py-4 text-center">Lade Szenarien...</div>
|
|
) : apiScenarios.length === 0 ? (
|
|
<div className="text-sm text-gray-400 py-4 text-center">Noch keine Szenarien definiert</div>
|
|
) : (
|
|
<div className="space-y-2">
|
|
{apiScenarios.map(scenario => (
|
|
<div key={scenario.id} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
|
<div>
|
|
<span className="font-medium text-sm">{scenario.title}</span>
|
|
<div className="flex items-center gap-2 mt-1">
|
|
{scenario.category && <span className="text-xs bg-slate-100 text-slate-600 px-2 py-0.5 rounded">{scenario.category}</span>}
|
|
<span className={`text-xs px-2 py-0.5 rounded ${
|
|
scenario.severity === 'critical' ? 'bg-red-100 text-red-700' :
|
|
scenario.severity === 'high' ? 'bg-orange-100 text-orange-700' :
|
|
scenario.severity === 'medium' ? 'bg-yellow-100 text-yellow-700' :
|
|
'bg-green-100 text-green-700'
|
|
}`}>{scenario.severity}</span>
|
|
</div>
|
|
{scenario.description && <p className="text-xs text-gray-500 mt-1 truncate max-w-lg">{scenario.description}</p>}
|
|
</div>
|
|
<button onClick={() => onDelete(scenario.id)} className="text-xs text-red-500 hover:text-red-700 px-2 py-1 rounded hover:bg-red-50">
|
|
Loeschen
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function ApiExercisesSection({ apiExercises, apiLoading, onAdd, onDelete }: {
|
|
apiExercises: ApiExercise[]; apiLoading: boolean; onAdd: () => void; onDelete: (id: string) => void
|
|
}) {
|
|
return (
|
|
<div className="bg-white rounded-lg border p-5">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h3 className="text-base font-semibold">Uebungen (Datenbank)</h3>
|
|
<div className="flex items-center gap-3">
|
|
<span className="text-sm text-gray-500">{apiExercises.length} Eintraege</span>
|
|
<button onClick={onAdd} className="px-3 py-1.5 bg-blue-600 text-white text-sm rounded-lg hover:bg-blue-700 transition-colors">
|
|
+ Neue Uebung
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{apiExercises.length === 0 ? (
|
|
<div className="text-sm text-gray-400 py-2 text-center">Noch keine Uebungen in der Datenbank</div>
|
|
) : (
|
|
<div className="space-y-2">
|
|
{apiExercises.map(ex => (
|
|
<div key={ex.id} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
|
<div>
|
|
<span className="font-medium text-sm">{ex.title}</span>
|
|
<div className="flex items-center gap-2 mt-1">
|
|
<span className="text-xs bg-slate-100 text-slate-600 px-2 py-0.5 rounded">{ex.exercise_type}</span>
|
|
{ex.exercise_date && <span className="text-xs text-gray-400">{new Date(ex.exercise_date).toLocaleDateString('de-DE')}</span>}
|
|
{ex.outcome && <span className={`text-xs px-2 py-0.5 rounded ${ex.outcome === 'passed' ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'}`}>{ex.outcome}</span>}
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={() => { if (window.confirm(`Uebung "${ex.title}" loeschen?`)) onDelete(ex.id) }}
|
|
className="p-1.5 text-red-400 hover:text-red-600 hover:bg-red-50 rounded transition-colors"
|
|
title="Loeschen"
|
|
>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|