Each page.tsx was 750-780 LOC. Extracted React components to _components/ and custom hooks to _hooks/ next to each page.tsx. All three pages are now under 215 LOC (well within the 500 LOC hard cap). Zero behavior changes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
156 lines
6.0 KiB
TypeScript
156 lines
6.0 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
|
|
interface CreateModalProps {
|
|
onClose: () => void
|
|
onCreated: () => void
|
|
}
|
|
|
|
export function EscalationCreateModal({ onClose, onCreated }: CreateModalProps) {
|
|
const [title, setTitle] = useState('')
|
|
const [description, setDescription] = useState('')
|
|
const [priority, setPriority] = useState('medium')
|
|
const [category, setCategory] = useState('')
|
|
const [assignee, setAssignee] = useState('')
|
|
const [dueDate, setDueDate] = useState('')
|
|
const [saving, setSaving] = useState(false)
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
async function handleSave() {
|
|
if (!title.trim()) {
|
|
setError('Titel ist erforderlich.')
|
|
return
|
|
}
|
|
setSaving(true)
|
|
setError(null)
|
|
try {
|
|
const res = await fetch('/api/sdk/v1/escalations', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
title: title.trim(),
|
|
description: description.trim() || null,
|
|
priority,
|
|
category: category || null,
|
|
assignee: assignee.trim() || null,
|
|
due_date: dueDate || null,
|
|
}),
|
|
})
|
|
if (!res.ok) {
|
|
const err = await res.json()
|
|
throw new Error(err.detail || err.error || 'Fehler beim Erstellen')
|
|
}
|
|
onCreated()
|
|
onClose()
|
|
} catch (e: unknown) {
|
|
setError(e instanceof Error ? e.message : 'Unbekannter Fehler')
|
|
} finally {
|
|
setSaving(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
|
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-lg">
|
|
<div className="p-6 border-b border-gray-100">
|
|
<h2 className="text-xl font-bold text-gray-900">Neue Eskalation erstellen</h2>
|
|
</div>
|
|
<div className="p-6 space-y-4">
|
|
{error && (
|
|
<div className="bg-red-50 text-red-700 text-sm px-4 py-2 rounded-lg">{error}</div>
|
|
)}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Titel <span className="text-red-500">*</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={title}
|
|
onChange={e => setTitle(e.target.value)}
|
|
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-purple-500"
|
|
placeholder="Kurze Beschreibung der Eskalation"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Beschreibung</label>
|
|
<textarea
|
|
value={description}
|
|
onChange={e => setDescription(e.target.value)}
|
|
rows={3}
|
|
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-purple-500"
|
|
placeholder="Detaillierte Beschreibung…"
|
|
/>
|
|
</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={priority}
|
|
onChange={e => setPriority(e.target.value)}
|
|
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-purple-500"
|
|
>
|
|
<option value="low">Niedrig</option>
|
|
<option value="medium">Mittel</option>
|
|
<option value="high">Hoch</option>
|
|
<option value="critical">Kritisch</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Kategorie</label>
|
|
<select
|
|
value={category}
|
|
onChange={e => setCategory(e.target.value)}
|
|
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-purple-500"
|
|
>
|
|
<option value="">— Keine —</option>
|
|
<option value="dsgvo_breach">DSGVO-Verletzung</option>
|
|
<option value="ai_act">AI Act</option>
|
|
<option value="vendor">Vendor</option>
|
|
<option value="internal">Intern</option>
|
|
<option value="other">Sonstiges</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">Zugewiesen an</label>
|
|
<input
|
|
type="text"
|
|
value={assignee}
|
|
onChange={e => setAssignee(e.target.value)}
|
|
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-purple-500"
|
|
placeholder="Name oder Team"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Faelligkeitsdatum</label>
|
|
<input
|
|
type="date"
|
|
value={dueDate}
|
|
onChange={e => setDueDate(e.target.value)}
|
|
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-purple-500"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="p-6 border-t border-gray-100 flex items-center justify-end gap-3">
|
|
<button
|
|
onClick={onClose}
|
|
className="px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
|
|
>
|
|
Abbrechen
|
|
</button>
|
|
<button
|
|
onClick={handleSave}
|
|
disabled={saving}
|
|
className="px-4 py-2 text-sm bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors disabled:opacity-50"
|
|
>
|
|
{saving ? 'Speichern…' : 'Eskalation erstellen'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|