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
+137
View File
@@ -0,0 +1,137 @@
import { useState } from "react";
import { apiRequest } from "../../api/client";
import type { Beladungsvorlage, Material, Objekttyp } from "../../api/types";
interface PositionZeile {
materialId: number | "";
fach: string;
sollmenge: string;
}
const LEERE_ZEILE: PositionZeile = { materialId: "", fach: "", sollmenge: "1" };
interface Props {
vorlagen: Beladungsvorlage[];
objekttypen: Objekttyp[];
materialien: Material[];
onGeaendert: () => Promise<void>;
onFehler: (text: string) => void;
}
export function VorlageSection({ vorlagen, objekttypen, materialien, onGeaendert, onFehler }: Props) {
const [name, setName] = useState("");
const [objekttypId, setObjekttypId] = useState<number | "">("");
const [zeilen, setZeilen] = useState<PositionZeile[]>([LEERE_ZEILE]);
const [wirdAngelegt, setWirdAngelegt] = useState(false);
function zeileAendern(index: number, zeile: Partial<PositionZeile>) {
setZeilen((bisherige) => bisherige.map((z, i) => (i === index ? { ...z, ...zeile } : z)));
}
async function anlegen() {
if (objekttypId === "") {
onFehler("Objekttyp auswählen.");
return;
}
const positionen = zeilen
.filter((z) => z.materialId !== "")
.map((z) => ({ material_id: z.materialId, fach: z.fach || null, sollmenge: z.sollmenge }));
setWirdAngelegt(true);
try {
await apiRequest("/vorlagen", {
method: "POST",
body: { objekttyp_id: objekttypId, name, positionen },
});
setName("");
setZeilen([LEERE_ZEILE]);
await onGeaendert();
} catch {
onFehler("Vorlage konnte nicht angelegt werden.");
} finally {
setWirdAngelegt(false);
}
}
return (
<section className="section">
<h2>Vorlage anlegen</h2>
<div className="card">
{objekttypen.length === 0 ? (
<p className="text-muted">Erst einen Objekttyp anlegen.</p>
) : (
<div className="stack">
<div className="row">
<select
className="input"
value={objekttypId}
onChange={(e) => setObjekttypId(Number(e.target.value) || "")}
>
<option value="">Objekttyp wählen</option>
{objekttypen.map((o) => (
<option key={o.id} value={o.id}>
{o.name}
</option>
))}
</select>
<input className="input" value={name} onChange={(e) => setName(e.target.value)} placeholder="Name" />
</div>
<p className="text-muted" style={{ marginBottom: 0 }}>
Positionen (optional, können auch später nachgepflegt werden)
</p>
{zeilen.map((zeile, i) => (
<div className="row" key={i}>
<select
className="input"
value={zeile.materialId}
onChange={(e) => zeileAendern(i, { materialId: Number(e.target.value) || "" })}
>
<option value="">Material</option>
{materialien.map((m) => (
<option key={m.id} value={m.id}>
{m.name}
</option>
))}
</select>
<input
className="input"
style={{ width: "8rem" }}
value={zeile.fach}
onChange={(e) => zeileAendern(i, { fach: e.target.value })}
placeholder="Fach"
/>
<input
className="input"
style={{ width: "5rem" }}
type="number"
value={zeile.sollmenge}
onChange={(e) => zeileAendern(i, { sollmenge: e.target.value })}
/>
</div>
))}
<button className="btn btn-secondary" onClick={() => setZeilen((z) => [...z, LEERE_ZEILE])}>
+ Position
</button>
<button className="btn btn-primary" onClick={anlegen} disabled={!name || wirdAngelegt}>
Vorlage anlegen
</button>
</div>
)}
{vorlagen.length > 0 && (
<ul className="card-list" style={{ marginTop: "1.25rem" }}>
{vorlagen.map((v) => (
<li key={v.id} className="row-between" style={{ padding: "0.4rem 0" }}>
<span>{v.name}</span>
<span className="badge badge-neutral">v{v.version}</span>
</li>
))}
</ul>
)}
</div>
</section>
);
}