diff --git a/app/admin/ObjekteManager.tsx b/app/admin/ObjekteManager.tsx new file mode 100644 index 0000000..b474be4 --- /dev/null +++ b/app/admin/ObjekteManager.tsx @@ -0,0 +1,183 @@ +'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(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 ( +
+ {error && ( +

{error}

+ )} + +
+ 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" + /> + +
+ +
+ + + + + + + + + + + {initialObjekte.map((obj, idx) => ( + + + + + + + ))} + {initialObjekte.length === 0 && ( + + + + )} + +
IDNameZuletzt verwendet
{obj.ID} + {editingId === obj.ID ? ( + 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 + )} + + {obj.LastUsed ? new Date(obj.LastUsed).toLocaleDateString('de-DE') : '—'} + + {editingId === obj.ID ? ( + + + + + ) : ( + + + + + )} +
Keine Objekte vorhanden.
+
+
+ ); +} diff --git a/app/admin/actions.ts b/app/admin/actions.ts index 66a7fec..5ca135a 100644 --- a/app/admin/actions.ts +++ b/app/admin/actions.ts @@ -4,6 +4,19 @@ import { redirect } from 'next/navigation'; import { getSession } from '@/lib/session'; import { query } from '@/lib/db'; +export interface ObjektRow { + ID: number; + Name: string; + LastUsed: string | null; +} + +export async function listObjekte(): Promise { + const session = await getSession(); + if (!session || !session.role?.includes('admin')) redirect('/'); + const rows = await query('SELECT ID, Name, LastUsed FROM objekte ORDER BY Name ASC', []); + return rows as ObjektRow[]; +} + export interface BeoUser { id: number; kürzel: string | null; diff --git a/app/admin/page.tsx b/app/admin/page.tsx index 23dfaf9..e3d0215 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -1,64 +1,106 @@ import Link from 'next/link'; import { redirect } from 'next/navigation'; import { getSession } from '@/lib/session'; -import { listUsers } from './actions'; +import { listUsers, listObjekte } from './actions'; import ResetButton from './ResetButton'; +import ObjekteManager from './ObjekteManager'; -export default async function AdminPage() { +type Tab = 'benutzer' | 'objekte'; + +export default async function AdminPage({ + searchParams, +}: { + searchParams: Promise<{ tab?: string }>; +}) { const session = await getSession(); if (!session) redirect('/login'); if (session.role === null || !session.role.includes('admin')) redirect('/'); - const users = await listUsers(); + const { tab: tabParam } = await searchParams; + const activeTab: Tab = tabParam === 'objekte' ? 'objekte' : 'benutzer'; + + const [users, objekte] = await Promise.all([ + activeTab === 'benutzer' ? listUsers() : Promise.resolve([]), + activeTab === 'objekte' ? listObjekte() : Promise.resolve([]), + ]); + + const tabs: { id: Tab; label: string }[] = [ + { id: 'benutzer', label: 'Benutzerverwaltung' }, + { id: 'objekte', label: 'Objektverwaltung' }, + ]; return (
-
+

Logbuch — Sternwarte Welzheim

← Zurück
-

Benutzerverwaltung

- -
- - - - - - - - - - - - - {users.map((user, idx) => ( - - - - - - - - - ))} - -
KürzelNameVornameRollePasswort
{user.kürzel ?? '—'}{user.name}{user.vorname ?? '—'}{user.role ?? '—'} - {user.hasPw ? ( - gesetzt - ) : ( - Standard - )} - - -
+ {/* Tabs */} +
+ {tabs.map(({ id, label }) => ( + + {label} + + ))}
-

- „Zurücksetzen“ setzt das Passwort auf NULL. Der Benutzer muss sich danach mit dem Standard-Passwort anmelden und es sofort ändern. -

+ {/* Benutzerverwaltung */} + {activeTab === 'benutzer' && ( + <> +
+ + + + + + + + + + + + + {users.map((user, idx) => ( + + + + + + + + + ))} + +
KürzelNameVornameRollePasswort
{user.kürzel ?? '—'}{user.name}{user.vorname ?? '—'}{user.role ?? '—'} + {user.hasPw ? ( + gesetzt + ) : ( + Standard + )} + + +
+
+

+ „Zurücksetzen“ setzt das Passwort auf NULL. Der Benutzer muss sich danach mit dem Standard-Passwort anmelden und es sofort ändern. +

+ + )} + + {/* Objektverwaltung */} + {activeTab === 'objekte' && ( + + )}
); diff --git a/app/api/objekte/[id]/route.ts b/app/api/objekte/[id]/route.ts new file mode 100644 index 0000000..c9cd217 --- /dev/null +++ b/app/api/objekte/[id]/route.ts @@ -0,0 +1,38 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { query } from '@/lib/db'; +import { getSession } from '@/lib/session'; + +export async function PUT(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { + const session = await getSession(); + if (!session) return NextResponse.json({ error: 'Nicht angemeldet' }, { status: 401 }); + if (!session.role?.includes('admin')) return NextResponse.json({ error: 'Keine Berechtigung' }, { status: 403 }); + try { + const { id } = await params; + const numId = Number(id); + if (isNaN(numId)) return NextResponse.json({ error: 'Ungültige ID' }, { status: 400 }); + const { name } = await req.json(); + const trimmed = (name as string)?.trim(); + if (!trimmed) return NextResponse.json({ error: 'Name darf nicht leer sein' }, { status: 400 }); + await query('UPDATE objekte SET Name = ? WHERE ID = ?', [trimmed, numId]); + return NextResponse.json({ ID: numId, Name: trimmed }); + } catch (error) { + console.error('PUT /api/objekte/[id]:', error); + return NextResponse.json({ error: 'Datenbankfehler' }, { status: 500 }); + } +} + +export async function DELETE(_req: NextRequest, { params }: { params: Promise<{ id: string }> }) { + const session = await getSession(); + if (!session) return NextResponse.json({ error: 'Nicht angemeldet' }, { status: 401 }); + if (!session.role?.includes('admin')) return NextResponse.json({ error: 'Keine Berechtigung' }, { status: 403 }); + try { + const { id } = await params; + const numId = Number(id); + if (isNaN(numId)) return NextResponse.json({ error: 'Ungültige ID' }, { status: 400 }); + await query('DELETE FROM objekte WHERE ID = ?', [numId]); + return NextResponse.json({ ok: true }); + } catch (error) { + console.error('DELETE /api/objekte/[id]:', error); + return NextResponse.json({ error: 'Datenbankfehler' }, { status: 500 }); + } +} diff --git a/app/api/objekte/route.ts b/app/api/objekte/route.ts index 9ea873b..0d50fbe 100644 --- a/app/api/objekte/route.ts +++ b/app/api/objekte/route.ts @@ -1,4 +1,4 @@ -import { NextResponse } from 'next/server'; +import { NextRequest, NextResponse } from 'next/server'; import { query } from '@/lib/db'; import { getSession } from '@/lib/session'; @@ -13,3 +13,19 @@ export async function GET() { return NextResponse.json({ error: 'Datenbankfehler' }, { status: 500 }); } } + +export async function POST(req: NextRequest) { + const session = await getSession(); + if (!session) return NextResponse.json({ error: 'Nicht angemeldet' }, { status: 401 }); + if (!session.role?.includes('admin')) return NextResponse.json({ error: 'Keine Berechtigung' }, { status: 403 }); + try { + const { name } = await req.json(); + const trimmed = (name as string)?.trim(); + if (!trimmed) return NextResponse.json({ error: 'Name darf nicht leer sein' }, { status: 400 }); + const result = await query('INSERT INTO objekte (Name) VALUES (?)', [trimmed]) as { insertId: number }; + return NextResponse.json({ ID: result.insertId, Name: trimmed }, { status: 201 }); + } catch (error) { + console.error('POST /api/objekte:', error); + return NextResponse.json({ error: 'Datenbankfehler' }, { status: 500 }); + } +}