feat(fach): Duplikate case-insensitiv verhindern, Verwendung anzeigen und ersetzen/zusammenführen
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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user