Files
MABEA/frontend/src/pages/admin/VorlageSection.tsx
T
patrickandClaude Sonnet 5 a2877f5672
CI / backend-tests (push) Successful in 1m0s
CI / frontend-build (push) Successful in 5m56s
Feste Fächer-Liste je Objekttyp (Vorlagen-Positionen: Dropdown + Sonstiges)
Nutzer-Vorgabe: Rucksack-Fächer sollen aus einer festen, je Objekttyp
gepflegten Liste gewählt werden können statt jedes Mal Freitext einzutippen.
Neue Tabelle fach (Migration 0004), CRUD-Endpoints /faecher, Admin-Tab
Struktur bekommt Fach-Verwaltung, Vorlagen-Editor nutzt Dropdown mit
"Sonstiges"-Freitext-Fallback.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L85hmKbvX7Cqkq47KnQhFt
2026-09-04 10:24:15 +02:00

229 lines
8.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from "react";
import { apiRequest } from "../../api/client";
import type { Beladungsvorlage, Fach, Material, Objekttyp } from "../../api/types";
import { FachAuswahl } from "./FachAuswahl";
interface PositionZeile {
materialId: number | "";
fach: string;
sollmenge: string;
}
const LEERE_ZEILE: PositionZeile = { materialId: "", fach: "", sollmenge: "1" };
interface Props {
vorlagen: Beladungsvorlage[];
objekttypen: Objekttyp[];
materialien: Material[];
faecher: Fach[];
onGeaendert: () => Promise<void>;
onFehler: (text: string) => void;
}
export function VorlageSection({ vorlagen, objekttypen, materialien, faecher, onGeaendert, onFehler }: Props) {
const [name, setName] = useState("");
const [objekttypId, setObjekttypId] = useState<number | "">("");
const [zeilen, setZeilen] = useState<PositionZeile[]>([LEERE_ZEILE]);
const [wirdAngelegt, setWirdAngelegt] = useState(false);
const [ausgeklapptId, setAusgeklapptId] = useState<number | null>(null);
const [neueVersionZeilen, setNeueVersionZeilen] = useState<PositionZeile[]>([]);
const [wirdVersioniert, setWirdVersioniert] = useState(false);
function versionBearbeiten(v: Beladungsvorlage) {
setAusgeklapptId(ausgeklapptId === v.id ? null : v.id);
setNeueVersionZeilen(
v.positionen.map((p) => ({
materialId: p.material_id,
fach: p.fach ?? "",
sollmenge: p.sollmenge,
}))
);
}
function neueVersionZeileAendern(index: number, zeile: Partial<PositionZeile>) {
setNeueVersionZeilen((bisherige) => bisherige.map((z, i) => (i === index ? { ...z, ...zeile } : z)));
}
async function neueVersionSpeichern(vorlageId: number) {
const positionen = neueVersionZeilen
.filter((z) => z.materialId !== "")
.map((z) => ({ material_id: z.materialId, fach: z.fach || null, sollmenge: z.sollmenge }));
setWirdVersioniert(true);
try {
await apiRequest(`/vorlagen/${vorlageId}/neue-version`, { method: "POST", body: { positionen } });
setAusgeklapptId(null);
await onGeaendert();
} catch {
onFehler("Neue Version konnte nicht angelegt werden.");
} finally {
setWirdVersioniert(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>
<FachAuswahl
faecher={faecher.filter((f) => f.objekttyp_id === objekttypId)}
wert={zeile.fach}
onAendern={(wert) => zeileAendern(i, { fach: wert })}
/>
<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} style={{ padding: "0.4rem 0" }}>
<div className="row-between">
<span>
{v.name} <span className="badge badge-neutral">v{v.version}</span>
</span>
<button className="btn btn-secondary" onClick={() => versionBearbeiten(v)}>
{ausgeklapptId === v.id ? "Schließen" : "Positionen / neue Version"}
</button>
</div>
{ausgeklapptId === v.id && (
<div className="stack" style={{ marginTop: "0.6rem" }}>
<p className="text-muted" style={{ marginBottom: 0 }}>
Änderungen erzeugen eine neue Version die aktuelle v{v.version} bleibt unverändert
erhalten.
</p>
{neueVersionZeilen.map((zeile, i) => (
<div className="row" key={i}>
<select
className="input"
value={zeile.materialId}
onChange={(e) =>
neueVersionZeileAendern(i, { materialId: Number(e.target.value) || "" })
}
>
<option value="">Material</option>
{materialien.map((m) => (
<option key={m.id} value={m.id}>
{m.name}
</option>
))}
</select>
<FachAuswahl
faecher={faecher.filter((f) => f.objekttyp_id === v.objekttyp_id)}
wert={zeile.fach}
onAendern={(wert) => neueVersionZeileAendern(i, { fach: wert })}
/>
<input
className="input"
style={{ width: "5rem" }}
type="number"
value={zeile.sollmenge}
onChange={(e) => neueVersionZeileAendern(i, { sollmenge: e.target.value })}
/>
</div>
))}
<button
className="btn btn-secondary"
onClick={() => setNeueVersionZeilen((z) => [...z, LEERE_ZEILE])}
>
+ Position
</button>
<button
className="btn btn-primary"
onClick={() => neueVersionSpeichern(v.id)}
disabled={wirdVersioniert}
>
Neue Version speichern
</button>
</div>
)}
</li>
))}
</ul>
)}
</div>
</section>
);
}