fix(objekt): Fahrzeug-Zuordnung als 1:n statt n:m (fachliche Korrektur)
CI / backend-tests (push) Failing after 1m23s
CI / frontend-build (push) Successful in 29s

Nutzer-Korrektur: "Ein Objekt gehört immer nur zu genau einem Fahrzeug"
- Spalte gezogen_von_objekt_id -> fahrzeug_id (Rename-Migration 0010)
- Zuvor begonnener n:m-Ansatz (objekt_kopplung Tabelle/Endpoints/Panel)
  wieder entfernt, da fachlich falsch
- PATCH /objekte/{id} prüft Existenz + direkten Zyklus (A<->B)
- Frontend: einfache Single-Select "Fahrzeug zuordnen" statt Kopplungs-Panel

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
This commit is contained in:
2026-09-05 01:11:25 +02:00
co-authored by Claude Sonnet 5
parent ebd6a9589e
commit 38b8ce1795
8 changed files with 144 additions and 109 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ export interface Objekt {
vorlage_id: number | null;
standort_id: number;
status: string;
gezogen_von_objekt_id: number | null;
fahrzeug_id: number | null;
}
export interface Objektposition {
+49 -45
View File
@@ -33,6 +33,7 @@ export function ObjektSection({
const [name, setName] = useState("");
const [vorlageId, setVorlageId] = useState<number | "">("");
const [standortId, setStandortId] = useState<number | "">("");
const [fahrzeugId, setFahrzeugId] = useState<number | "">("");
const [wirdAngelegt, setWirdAngelegt] = useState(false);
const [ausgeklapptId, setAusgeklapptId] = useState<number | null>(null);
const [duplizierId, setDuplizierId] = useState<number | null>(null);
@@ -40,11 +41,38 @@ export function ObjektSection({
const [neuerCode, setNeuerCode] = useState("");
const [neuerName, setNeuerName] = useState("");
const [neuerStandortId, setNeuerStandortId] = useState<number | "">("");
const [gezogenVonId, setGezogenVonId] = useState<number | "">("");
const [kopplungBearbeitenId, setKopplungBearbeitenId] = useState<number | null>(null);
const [kopplungAuswahl, setKopplungAuswahl] = useState<number | "">("");
const [fahrzeugBearbeitenId, setFahrzeugBearbeitenId] = useState<number | null>(null);
const [fahrzeugAuswahl, setFahrzeugAuswahl] = useState<number | "">("");
const [suche, setSuche] = useState("");
function objektName(id: number | null): string {
if (id === null) return "";
const gefunden = objekte.find((x) => x.id === id);
return gefunden ? `${gefunden.name} (${gefunden.code})` : `#${id}`;
}
function fahrzeugBearbeiten(o: Objekt) {
if (fahrzeugBearbeitenId === o.id) {
setFahrzeugBearbeitenId(null);
return;
}
setFahrzeugBearbeitenId(o.id);
setFahrzeugAuswahl(o.fahrzeug_id ?? "");
}
async function fahrzeugSpeichern(objektId: number) {
try {
await apiRequest(`/objekte/${objektId}`, {
method: "PATCH",
body: { fahrzeug_id: fahrzeugAuswahl === "" ? null : fahrzeugAuswahl },
});
setFahrzeugBearbeitenId(null);
await onGeaendert();
} catch {
onFehler("Fahrzeug-Zuordnung konnte nicht gespeichert werden.");
}
}
async function codeVorschlagen() {
if (!praefix) {
onFehler("Präfix eingeben (z. B. RK, FZ).");
@@ -91,13 +119,13 @@ export function ObjektSection({
objekttyp_id: vorlage?.objekttyp_id,
vorlage_id: vorlageId,
standort_id: standortId,
gezogen_von_objekt_id: gezogenVonId || null,
fahrzeug_id: fahrzeugId === "" ? null : fahrzeugId,
},
});
setPraefix("");
setCode("");
setName("");
setGezogenVonId("");
setFahrzeugId("");
await onGeaendert();
} catch {
onFehler("Objekt konnte nicht angelegt werden (Code evtl. schon vergeben).");
@@ -106,30 +134,6 @@ export function ObjektSection({
}
}
function objektName(id: number | null): string {
if (id === null) return "";
const o = objekte.find((x) => x.id === id);
return o ? `${o.name} (${o.code})` : `Objekt ${id}`;
}
function kopplungBearbeiten(o: Objekt) {
setKopplungBearbeitenId(kopplungBearbeitenId === o.id ? null : o.id);
setKopplungAuswahl(o.gezogen_von_objekt_id ?? "");
}
async function kopplungSpeichern(objektId: number) {
try {
await apiRequest(`/objekte/${objektId}`, {
method: "PATCH",
body: { gezogen_von_objekt_id: kopplungAuswahl || null },
});
setKopplungBearbeitenId(null);
await onGeaendert();
} catch {
onFehler("Kopplung konnte nicht geändert werden.");
}
}
function duplizierenStarten(o: Objekt) {
setDuplizierId(duplizierId === o.id ? null : o.id);
setNeuerPraefix("");
@@ -222,16 +226,16 @@ export function ObjektSection({
<input className="input" value={name} onChange={(e) => setName(e.target.value)} />
</div>
<div className="field">
<label>Gezogen von (optional, Anhänger-Kopplung)</label>
<label>Fahrzeug (optional)</label>
<select
className="input"
value={gezogenVonId}
onChange={(e) => setGezogenVonId(Number(e.target.value) || "")}
value={fahrzeugId}
onChange={(e) => setFahrzeugId(Number(e.target.value) || "")}
>
<option value="">Kein Zugfahrzeug</option>
{objekte.map((o) => (
<option key={o.id} value={o.id}>
{o.name} ({o.code})
<option value="">Kein Fahrzeug</option>
{objekte.map((x) => (
<option key={x.id} value={x.id}>
{x.name} ({x.code})
</option>
))}
</select>
@@ -260,8 +264,8 @@ export function ObjektSection({
<div className="row-between" style={{ padding: "0.4rem 0" }}>
<span>
{o.name} <span className="text-muted">({o.code})</span>
{o.gezogen_von_objekt_id !== null && (
<span className="text-muted"> gezogen von {objektName(o.gezogen_von_objekt_id)}</span>
{o.fahrzeug_id !== null && (
<span className="text-muted"> Fahrzeug: {objektName(o.fahrzeug_id)}</span>
)}
</span>
<div className="row">
@@ -272,8 +276,8 @@ export function ObjektSection({
{ausgeklapptId === o.id ? "Positionen ausblenden" : "Ablauf/Charge/SN pflegen"}
</button>
{!nurPflegen && (
<button className="btn btn-secondary" onClick={() => kopplungBearbeiten(o)}>
{kopplungBearbeitenId === o.id ? "Schließen" : "Kopplung ändern"}
<button className="btn btn-secondary" onClick={() => fahrzeugBearbeiten(o)}>
{fahrzeugBearbeitenId === o.id ? "Schließen" : "Fahrzeug zuordnen"}
</button>
)}
{!nurPflegen && (
@@ -283,14 +287,14 @@ export function ObjektSection({
)}
</div>
</div>
{kopplungBearbeitenId === o.id && (
{fahrzeugBearbeitenId === o.id && (
<div className="row" style={{ marginTop: "0.5rem", flexWrap: "wrap" }}>
<select
className="input"
value={kopplungAuswahl}
onChange={(e) => setKopplungAuswahl(Number(e.target.value) || "")}
value={fahrzeugAuswahl}
onChange={(e) => setFahrzeugAuswahl(Number(e.target.value) || "")}
>
<option value="">Kein Zugfahrzeug</option>
<option value="">Kein Fahrzeug</option>
{objekte
.filter((x) => x.id !== o.id)
.map((x) => (
@@ -299,7 +303,7 @@ export function ObjektSection({
</option>
))}
</select>
<button className="btn btn-primary" onClick={() => kopplungSpeichern(o.id)}>
<button className="btn btn-primary" onClick={() => fahrzeugSpeichern(o.id)}>
Speichern
</button>
</div>