Sprint 8: Bereich/Kategorie/Objekttyp/Vorlage-CRUD im Admin-Portal
CI / backend-tests (push) Successful in 56s
CI / frontend-build (push) Successful in 18s

Vervollständigt die Stammdaten-Anlage im Frontend (bisher nur Standort/
Material/Objekt), damit ein Pilotbetrieb nicht zwingend Excel-Import
braucht. Jede Sektion eigene Datei unter pages/admin/ (Projekt-Vorgabe:
keine großen Monolith-Dateien).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L85hmKbvX7Cqkq47KnQhFt
This commit is contained in:
2026-09-04 00:52:43 +02:00
co-authored by Claude Sonnet 5
parent 569f58b25b
commit dcc75dd2d0
6 changed files with 410 additions and 12 deletions
@@ -0,0 +1,86 @@
import { useState } from "react";
import { apiRequest } from "../../api/client";
import type { Bereich, Kategorie, Objekttyp } from "../../api/types";
interface Props {
bereiche: Bereich[];
kategorien: Kategorie[];
objekttypen: Objekttyp[];
onGeaendert: () => Promise<void>;
onFehler: (text: string) => void;
}
export function ObjekttypSection({ bereiche, kategorien, objekttypen, onGeaendert, onFehler }: Props) {
const [name, setName] = useState("");
const [bereichId, setBereichId] = useState<number | "">("");
const [kategorieId, setKategorieId] = useState<number | "">("");
const [wirdAngelegt, setWirdAngelegt] = useState(false);
async function anlegen() {
if (bereichId === "") {
onFehler("Bereich auswählen.");
return;
}
setWirdAngelegt(true);
try {
await apiRequest("/objekttypen", {
method: "POST",
body: { name, bereich_id: bereichId, kategorie_id: kategorieId || null },
});
setName("");
await onGeaendert();
} catch {
onFehler("Objekttyp konnte nicht angelegt werden.");
} finally {
setWirdAngelegt(false);
}
}
return (
<section className="section">
<h2>Objekttyp anlegen</h2>
<div className="card">
{bereiche.length === 0 ? (
<p className="text-muted">Erst einen Bereich anlegen.</p>
) : (
<div className="stack" style={{ marginBottom: objekttypen.length ? "1.25rem" : 0 }}>
<div className="row">
<select className="input" value={bereichId} onChange={(e) => setBereichId(Number(e.target.value) || "")}>
<option value="">Bereich wählen</option>
{bereiche.map((b) => (
<option key={b.id} value={b.id}>
{b.name}
</option>
))}
</select>
<select className="input" value={kategorieId} onChange={(e) => setKategorieId(Number(e.target.value) || "")}>
<option value="">Kategorie (optional)</option>
{kategorien.map((k) => (
<option key={k.id} value={k.id}>
{k.name}
</option>
))}
</select>
</div>
<div className="row">
<input className="input" value={name} onChange={(e) => setName(e.target.value)} placeholder="Name" />
<button className="btn btn-primary" onClick={anlegen} disabled={!name || wirdAngelegt}>
Anlegen
</button>
</div>
</div>
)}
{objekttypen.length > 0 && (
<ul className="card-list">
{objekttypen.map((o) => (
<li key={o.id} style={{ padding: "0.4rem 0" }}>
{o.name}
</li>
))}
</ul>
)}
</div>
</section>
);
}