refactor(admin): split obligations page.tsx into colocated components
Extract ObligationModal, ObligationDetail, ObligationCard, ObligationsHeader, StatsGrid, FilterBar and InfoBanners into _components/, plus _types.ts for shared types/constants. page.tsx drops from 987 to 325 LOC, below the 300 soft target region and well under the 500 hard cap. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState } from 'react'
|
||||
import { EMPTY_FORM, type ObligationFormData } from '../_types'
|
||||
|
||||
export default function ObligationModal({
|
||||
initial,
|
||||
onClose,
|
||||
onSave,
|
||||
}: {
|
||||
initial?: Partial<ObligationFormData>
|
||||
onClose: () => void
|
||||
onSave: (data: ObligationFormData) => Promise<void>
|
||||
}) {
|
||||
const [form, setForm] = useState<ObligationFormData>({ ...EMPTY_FORM, ...initial })
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const update = (field: keyof ObligationFormData, value: string) =>
|
||||
setForm(prev => ({ ...prev, [field]: value }))
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!form.title.trim()) { setError('Titel ist erforderlich'); return }
|
||||
setSaving(true)
|
||||
setError(null)
|
||||
try {
|
||||
await onSave(form)
|
||||
onClose()
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : 'Fehler beim Speichern')
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/40 z-50 flex items-center justify-center p-4" onClick={e => e.target === e.currentTarget && onClose()}>
|
||||
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-xl max-h-[90vh] overflow-y-auto">
|
||||
<div className="flex items-center justify-between p-6 border-b">
|
||||
<h2 className="text-lg font-semibold text-gray-900">
|
||||
{initial?.title ? 'Pflicht bearbeiten' : 'Neue Pflicht erstellen'}
|
||||
</h2>
|
||||
<button onClick={onClose} className="text-gray-400 hover:text-gray-600 text-xl">✕</button>
|
||||
</div>
|
||||
<div className="p-6 space-y-4">
|
||||
{error && <div className="text-red-600 text-sm bg-red-50 rounded-lg p-3">{error}</div>}
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Titel *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={form.title}
|
||||
onChange={e => update('title', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500"
|
||||
placeholder="z.B. Datenschutz-Folgenabschaetzung durchfuehren"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Beschreibung</label>
|
||||
<textarea
|
||||
value={form.description}
|
||||
onChange={e => update('description', e.target.value)}
|
||||
rows={3}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500"
|
||||
placeholder="Detaillierte Beschreibung der Anforderung..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Rechtsquelle</label>
|
||||
<select
|
||||
value={form.source}
|
||||
onChange={e => update('source', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm"
|
||||
>
|
||||
{['DSGVO', 'AI Act', 'NIS2', 'BDSG', 'TTDSG', 'DSA', 'Data Act', 'DORA', 'EU-Maschinen', 'Sonstig'].map(s => (
|
||||
<option key={s}>{s}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Artikel/Paragraph</label>
|
||||
<input
|
||||
type="text"
|
||||
value={form.source_article}
|
||||
onChange={e => update('source_article', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm"
|
||||
placeholder="z.B. Art. 35"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Prioritaet</label>
|
||||
<select
|
||||
value={form.priority}
|
||||
onChange={e => update('priority', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm"
|
||||
>
|
||||
<option value="critical">Kritisch</option>
|
||||
<option value="high">Hoch</option>
|
||||
<option value="medium">Mittel</option>
|
||||
<option value="low">Niedrig</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Status</label>
|
||||
<select
|
||||
value={form.status}
|
||||
onChange={e => update('status', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm"
|
||||
>
|
||||
<option value="pending">Ausstehend</option>
|
||||
<option value="in-progress">In Bearbeitung</option>
|
||||
<option value="completed">Abgeschlossen</option>
|
||||
<option value="overdue">Ueberfaellig</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Faellig bis</label>
|
||||
<input
|
||||
type="date"
|
||||
value={form.deadline}
|
||||
onChange={e => update('deadline', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Verantwortlich</label>
|
||||
<input
|
||||
type="text"
|
||||
value={form.responsible}
|
||||
onChange={e => update('responsible', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm"
|
||||
placeholder="z.B. DSB, IT, Compliance"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Betroffene Systeme</label>
|
||||
<input
|
||||
type="text"
|
||||
value={form.linked_systems}
|
||||
onChange={e => update('linked_systems', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm"
|
||||
placeholder="Kommagetrennt: CRM, ERP, Website"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Notizen</label>
|
||||
<textarea
|
||||
value={form.notes}
|
||||
onChange={e => update('notes', e.target.value)}
|
||||
rows={2}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm"
|
||||
placeholder="Interne Hinweise..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end gap-3 p-6 border-t">
|
||||
<button onClick={onClose} className="px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-lg">Abbrechen</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className="px-5 py-2 text-sm bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50"
|
||||
>
|
||||
{saving ? 'Speichern...' : 'Speichern'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user