feat(fach): Duplikate case-insensitiv verhindern, Verwendung anzeigen und ersetzen/zusammenführen
CI / backend-tests (push) Failing after 2m7s
CI / frontend-build (push) Successful in 18s

Anlegen prüft jetzt case-insensitiv auf Duplikate (409 statt "Fach A"
neben "fach a"). Neuer Endpunkt GET /faecher/{id}/objekte zeigt, welche
Objekte ein Fach über ihre Vorlage tatsächlich nutzen. POST
/faecher/{id}/ersetzen benennt um und zieht alle Vorlagenpositionen
mit; landet der neue Name auf einem bereits bestehenden Fach, werden
beide zusammengeführt. Frontend: Fach anklicken öffnet Verwendung +
Ersetzen-Eingabe direkt in der Fächer-Verwaltung.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
This commit is contained in:
2026-09-07 00:18:04 +02:00
co-authored by Claude Sonnet 5
parent 8e89755385
commit 5f568c365c
5 changed files with 283 additions and 14 deletions
+82 -10
View File
@@ -1,7 +1,7 @@
import { useState } from "react";
import { apiRequest } from "../../api/client";
import type { Fach, Objekttyp } from "../../api/types";
import type { Fach, Objekt, Objekttyp } from "../../api/types";
interface Props {
objekttypen: Objekttyp[];
@@ -18,6 +18,11 @@ export function FachSection({ objekttypen, faecher, onGeaendert, onFehler }: Pro
const [name, setName] = useState("");
const [wirdAngelegt, setWirdAngelegt] = useState(false);
const [offenesFachId, setOffenesFachId] = useState<number | null>(null);
const [verwendungObjekte, setVerwendungObjekte] = useState<Objekt[]>([]);
const [ladeVerwendung, setLadeVerwendung] = useState(false);
const [ersetzenName, setErsetzenName] = useState("");
async function anlegen() {
if (objekttypId === "") {
onFehler("Objekttyp auswählen.");
@@ -29,7 +34,7 @@ export function FachSection({ objekttypen, faecher, onGeaendert, onFehler }: Pro
setName("");
await onGeaendert();
} catch {
onFehler("Fach konnte nicht angelegt werden.");
onFehler("Fach konnte nicht angelegt werden (evtl. existiert es schon, Groß-/Kleinschreibung egal).");
} finally {
setWirdAngelegt(false);
}
@@ -44,6 +49,40 @@ export function FachSection({ objekttypen, faecher, onGeaendert, onFehler }: Pro
}
}
async function fachOeffnen(fach: Fach) {
if (offenesFachId === fach.id) {
setOffenesFachId(null);
return;
}
setOffenesFachId(fach.id);
setErsetzenName(fach.name);
setLadeVerwendung(true);
try {
setVerwendungObjekte(await apiRequest<Objekt[]>(`/faecher/${fach.id}/objekte`));
} catch {
onFehler("Verwendung konnte nicht geladen werden.");
} finally {
setLadeVerwendung(false);
}
}
async function ersetzen(fach: Fach) {
if (!ersetzenName.trim()) {
onFehler("Neuen Namen angeben.");
return;
}
try {
await apiRequest(`/faecher/${fach.id}/ersetzen`, {
method: "POST",
body: { neuer_name: ersetzenName.trim() },
});
setOffenesFachId(null);
await onGeaendert();
} catch {
onFehler("Fach konnte nicht ersetzt werden.");
}
}
function objekttypName(id: number) {
return objekttypen.find((o) => o.id === id)?.name ?? `#${id}`;
}
@@ -84,14 +123,47 @@ export function FachSection({ objekttypen, faecher, onGeaendert, onFehler }: Pro
{angezeigteFaecher.length > 0 && (
<ul className="card-list">
{angezeigteFaecher.map((f) => (
<li key={f.id} className="row-between" style={{ padding: "0.4rem 0" }}>
<span>
{f.name}
{objekttypId === "" && <span className="text-muted"> ({objekttypName(f.objekttyp_id)})</span>}
</span>
<button className="btn btn-danger" onClick={() => loeschen(f.id)}>
Löschen
</button>
<li key={f.id} style={{ padding: "0.4rem 0" }}>
<div className="row-between">
<button
onClick={() => fachOeffnen(f)}
style={{ background: "none", border: "none", padding: 0, cursor: "pointer", textAlign: "left" }}
>
<span style={{ textDecoration: "underline" }}>{f.name}</span>
{objekttypId === "" && <span className="text-muted"> ({objekttypName(f.objekttyp_id)})</span>}
</button>
<button className="btn btn-danger" onClick={() => loeschen(f.id)}>
Löschen
</button>
</div>
{offenesFachId === f.id && (
<div className="card" style={{ marginTop: "0.5rem", background: "var(--color-surface-sunken)" }}>
{ladeVerwendung && <p className="text-muted">Lade</p>}
{!ladeVerwendung && verwendungObjekte.length === 0 && (
<p className="text-muted">Kein Objekt verwendet dieses Fach.</p>
)}
{!ladeVerwendung && verwendungObjekte.length > 0 && (
<ul>
{verwendungObjekte.map((o) => (
<li key={o.id}>
{o.name} ({o.code})
</li>
))}
</ul>
)}
<div className="row" style={{ marginTop: "0.5rem" }}>
<input
className="input"
value={ersetzenName}
onChange={(e) => setErsetzenName(e.target.value)}
placeholder="Neuer Name (bestehender Fach-Name = Zusammenführen)"
/>
<button className="btn btn-secondary" onClick={() => ersetzen(f)}>
Ersetzen
</button>
</div>
</div>
)}
</li>
))}
</ul>