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
229 lines
8.5 KiB
TypeScript
229 lines
8.5 KiB
TypeScript
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>
|
||
);
|
||
}
|