edb324719b
Admins können Objekte anlegen, umbenennen und löschen. Die Admin-Seite ist in zwei Tabs aufgeteilt: Benutzerverwaltung (?tab=benutzer) und Objektverwaltung (?tab=objekte), navigierbar per URL-Parameter. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
184 lines
6.7 KiB
TypeScript
184 lines
6.7 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import type { ObjektRow } from './actions';
|
|
|
|
interface Props {
|
|
initialObjekte: ObjektRow[];
|
|
}
|
|
|
|
export default function ObjekteManager({ initialObjekte }: Props) {
|
|
const router = useRouter();
|
|
const [editingId, setEditingId] = useState<number | null>(null);
|
|
const [editName, setEditName] = useState('');
|
|
const [newName, setNewName] = useState('');
|
|
const [error, setError] = useState('');
|
|
const [busy, setBusy] = useState(false);
|
|
|
|
async function handleSaveEdit(id: number) {
|
|
const trimmed = editName.trim();
|
|
if (!trimmed) { setError('Name darf nicht leer sein.'); return; }
|
|
setBusy(true);
|
|
setError('');
|
|
const res = await fetch('/api/objekte/' + id, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ name: trimmed }),
|
|
});
|
|
setBusy(false);
|
|
if (!res.ok) {
|
|
const data = await res.json();
|
|
setError(data.error ?? 'Fehler beim Speichern.');
|
|
return;
|
|
}
|
|
setEditingId(null);
|
|
router.refresh();
|
|
}
|
|
|
|
async function handleDelete(id: number, name: string) {
|
|
if (!confirm(`Objekt „${name}" wirklich löschen?`)) return;
|
|
setBusy(true);
|
|
setError('');
|
|
const res = await fetch('/api/objekte/' + id, { method: 'DELETE' });
|
|
setBusy(false);
|
|
if (!res.ok) {
|
|
const data = await res.json();
|
|
setError(data.error ?? 'Fehler beim Löschen.');
|
|
return;
|
|
}
|
|
router.refresh();
|
|
}
|
|
|
|
async function handleCreate(e: React.FormEvent) {
|
|
e.preventDefault();
|
|
const trimmed = newName.trim();
|
|
if (!trimmed) { setError('Name darf nicht leer sein.'); return; }
|
|
setBusy(true);
|
|
setError('');
|
|
const res = await fetch('/api/objekte', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ name: trimmed }),
|
|
});
|
|
setBusy(false);
|
|
if (!res.ok) {
|
|
const data = await res.json();
|
|
setError(data.error ?? 'Fehler beim Erstellen.');
|
|
return;
|
|
}
|
|
setNewName('');
|
|
router.refresh();
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
{error && (
|
|
<p className="mb-3 text-sm text-red-600 font-medium">{error}</p>
|
|
)}
|
|
|
|
<form onSubmit={handleCreate} className="flex gap-2 mb-4">
|
|
<input
|
|
type="text"
|
|
value={newName}
|
|
onChange={(e) => setNewName(e.target.value)}
|
|
placeholder="Neues Objekt…"
|
|
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:border-blue-500"
|
|
/>
|
|
<button
|
|
type="submit"
|
|
disabled={busy}
|
|
className="px-4 py-2 text-sm font-medium bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50"
|
|
>
|
|
Hinzufügen
|
|
</button>
|
|
</form>
|
|
|
|
<div className="bg-white border border-gray-300 rounded-xl shadow-sm overflow-hidden">
|
|
<table className="w-full text-sm">
|
|
<thead className="bg-gray-100 text-gray-700">
|
|
<tr>
|
|
<th className="text-left px-4 py-3 font-semibold w-16">ID</th>
|
|
<th className="text-left px-4 py-3 font-semibold">Name</th>
|
|
<th className="text-left px-4 py-3 font-semibold hidden sm:table-cell">Zuletzt verwendet</th>
|
|
<th className="px-4 py-3 w-36"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{initialObjekte.map((obj, idx) => (
|
|
<tr key={obj.ID} className={idx % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
|
|
<td className="px-4 py-2 text-gray-400 font-mono text-xs">{obj.ID}</td>
|
|
<td className="px-4 py-2">
|
|
{editingId === obj.ID ? (
|
|
<input
|
|
type="text"
|
|
value={editName}
|
|
onChange={(e) => setEditName(e.target.value)}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter') handleSaveEdit(obj.ID);
|
|
if (e.key === 'Escape') setEditingId(null);
|
|
}}
|
|
autoFocus
|
|
className="w-full px-2 py-1 border border-blue-400 rounded text-sm focus:outline-none"
|
|
/>
|
|
) : (
|
|
obj.Name
|
|
)}
|
|
</td>
|
|
<td className="px-4 py-2 text-gray-500 hidden sm:table-cell">
|
|
{obj.LastUsed ? new Date(obj.LastUsed).toLocaleDateString('de-DE') : '—'}
|
|
</td>
|
|
<td className="px-4 py-2 text-right">
|
|
{editingId === obj.ID ? (
|
|
<span className="flex justify-end gap-2">
|
|
<button
|
|
type="button"
|
|
onClick={() => handleSaveEdit(obj.ID)}
|
|
disabled={busy}
|
|
className="px-2 py-1 text-xs font-medium bg-green-100 text-green-700 border border-green-300 rounded hover:bg-green-200 disabled:opacity-50"
|
|
>
|
|
Speichern
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setEditingId(null)}
|
|
className="px-2 py-1 text-xs font-medium bg-gray-100 text-gray-600 border border-gray-300 rounded hover:bg-gray-200"
|
|
>
|
|
Abbrechen
|
|
</button>
|
|
</span>
|
|
) : (
|
|
<span className="flex justify-end gap-2">
|
|
<button
|
|
type="button"
|
|
onClick={() => { setEditingId(obj.ID); setEditName(obj.Name); setError(''); }}
|
|
disabled={busy}
|
|
className="px-2 py-1 text-xs font-medium bg-blue-100 text-blue-700 border border-blue-300 rounded hover:bg-blue-200 disabled:opacity-50"
|
|
>
|
|
Bearbeiten
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => handleDelete(obj.ID, obj.Name)}
|
|
disabled={busy}
|
|
className="px-2 py-1 text-xs font-medium bg-red-100 text-red-700 border border-red-300 rounded hover:bg-red-200 disabled:opacity-50"
|
|
>
|
|
Löschen
|
|
</button>
|
|
</span>
|
|
)}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
{initialObjekte.length === 0 && (
|
|
<tr>
|
|
<td colSpan={4} className="px-4 py-6 text-center text-gray-400 text-sm">Keine Objekte vorhanden.</td>
|
|
</tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|