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
This commit is contained in:
@@ -98,6 +98,13 @@ export interface Objekttyp {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface Fach {
|
||||
id: number;
|
||||
objekttyp_id: number;
|
||||
name: string;
|
||||
sortierung: number;
|
||||
}
|
||||
|
||||
export interface Me {
|
||||
id: number;
|
||||
name: string;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { apiRequest } from "../api/client";
|
||||
import type { Benutzer, Bereich, Beladungsvorlage, Kategorie, Material, Objekt, Objekttyp, Standort } from "../api/types";
|
||||
import type { Benutzer, Bereich, Beladungsvorlage, Fach, Kategorie, Material, Objekt, Objekttyp, Standort } from "../api/types";
|
||||
import { BenutzerSection } from "./admin/BenutzerSection";
|
||||
import { BereichSection } from "./admin/BereichSection";
|
||||
import { FachSection } from "./admin/FachSection";
|
||||
import { KategorieSection } from "./admin/KategorieSection";
|
||||
import { MaterialSection } from "./admin/MaterialSection";
|
||||
import { ObjektSection } from "./admin/ObjektSection";
|
||||
@@ -40,12 +41,13 @@ export function AdminPage() {
|
||||
const [objekte, setObjekte] = useState<Objekt[]>([]);
|
||||
const [materialien, setMaterialien] = useState<Material[]>([]);
|
||||
const [benutzer, setBenutzer] = useState<Benutzer[]>([]);
|
||||
const [faecher, setFaecher] = useState<Fach[]>([]);
|
||||
const [fehler, setFehler] = useState<string | null>(null);
|
||||
const [laedt, setLaedt] = useState(true);
|
||||
const [tab, setTab] = useState<Tab>("objekte");
|
||||
|
||||
async function ladeAlles() {
|
||||
const [be, ka, ot, s, v, o, m, b] = await Promise.all([
|
||||
const [be, ka, ot, s, v, o, m, b, f] = await Promise.all([
|
||||
apiRequest<Bereich[]>("/bereiche"),
|
||||
apiRequest<Kategorie[]>("/kategorien"),
|
||||
apiRequest<Objekttyp[]>("/objekttypen"),
|
||||
@@ -54,6 +56,7 @@ export function AdminPage() {
|
||||
apiRequest<Objekt[]>("/objekte"),
|
||||
apiRequest<Material[]>("/materialien"),
|
||||
apiRequest<Benutzer[]>("/benutzer"),
|
||||
apiRequest<Fach[]>("/faecher"),
|
||||
]);
|
||||
setBereiche(be);
|
||||
setKategorien(ka);
|
||||
@@ -63,6 +66,7 @@ export function AdminPage() {
|
||||
setObjekte(o);
|
||||
setMaterialien(m);
|
||||
setBenutzer(b);
|
||||
setFaecher(f);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
@@ -111,6 +115,7 @@ export function AdminPage() {
|
||||
vorlagen={vorlagen}
|
||||
objekttypen={objekttypen}
|
||||
materialien={materialien}
|
||||
faecher={faecher}
|
||||
onGeaendert={ladeAlles}
|
||||
onFehler={setFehler}
|
||||
/>
|
||||
@@ -134,6 +139,12 @@ export function AdminPage() {
|
||||
onGeaendert={ladeAlles}
|
||||
onFehler={setFehler}
|
||||
/>
|
||||
<FachSection
|
||||
objekttypen={objekttypen}
|
||||
faecher={faecher}
|
||||
onGeaendert={ladeAlles}
|
||||
onFehler={setFehler}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{tab === "standorte" && (
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import type { Fach } from "../../api/types";
|
||||
|
||||
const SONSTIGES = "__sonstiges__";
|
||||
|
||||
interface Props {
|
||||
faecher: Fach[];
|
||||
wert: string;
|
||||
onAendern: (wert: string) => void;
|
||||
}
|
||||
|
||||
/** Dropdown mit fester Fächer-Liste (je Objekttyp) + "Sonstiges" für Freitext,
|
||||
* damit Vorlagen-Positionen nicht immer per Hand getippt werden müssen. */
|
||||
export function FachAuswahl({ faecher, wert, onAendern }: Props) {
|
||||
const istBekanntesFach = faecher.some((f) => f.name === wert);
|
||||
const [sonstigesAktiv, setSonstigesAktiv] = useState(wert !== "" && !istBekanntesFach);
|
||||
|
||||
if (sonstigesAktiv) {
|
||||
return (
|
||||
<input
|
||||
className="input"
|
||||
style={{ width: "8rem" }}
|
||||
value={wert}
|
||||
onChange={(e) => onAendern(e.target.value)}
|
||||
placeholder="Fach (frei)"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<select
|
||||
className="input"
|
||||
style={{ width: "8rem" }}
|
||||
value={wert}
|
||||
onChange={(e) => {
|
||||
if (e.target.value === SONSTIGES) {
|
||||
setSonstigesAktiv(true);
|
||||
onAendern("");
|
||||
} else {
|
||||
onAendern(e.target.value);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<option value="">Fach…</option>
|
||||
{faecher.map((f) => (
|
||||
<option key={f.id} value={f.name}>
|
||||
{f.name}
|
||||
</option>
|
||||
))}
|
||||
<option value={SONSTIGES}>Sonstiges…</option>
|
||||
</select>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { apiRequest } from "../../api/client";
|
||||
import type { Fach, Objekttyp } from "../../api/types";
|
||||
|
||||
interface Props {
|
||||
objekttypen: Objekttyp[];
|
||||
faecher: Fach[];
|
||||
onGeaendert: () => Promise<void>;
|
||||
onFehler: (text: string) => void;
|
||||
}
|
||||
|
||||
/** Nutzer-Vorgabe: feste Fächer-Liste je Objekttyp (z. B. Rucksack-Fächer),
|
||||
* damit bei Vorlagen-Positionen aus einer Liste gewählt werden kann statt
|
||||
* jedes Mal Freitext einzutippen (Sonstiges bleibt dort als Freitext erhalten). */
|
||||
export function FachSection({ objekttypen, faecher, onGeaendert, onFehler }: Props) {
|
||||
const [objekttypId, setObjekttypId] = useState<number | "">("");
|
||||
const [name, setName] = useState("");
|
||||
const [wirdAngelegt, setWirdAngelegt] = useState(false);
|
||||
|
||||
async function anlegen() {
|
||||
if (objekttypId === "") {
|
||||
onFehler("Objekttyp auswählen.");
|
||||
return;
|
||||
}
|
||||
setWirdAngelegt(true);
|
||||
try {
|
||||
await apiRequest("/faecher", { method: "POST", body: { objekttyp_id: objekttypId, name } });
|
||||
setName("");
|
||||
await onGeaendert();
|
||||
} catch {
|
||||
onFehler("Fach konnte nicht angelegt werden.");
|
||||
} finally {
|
||||
setWirdAngelegt(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function loeschen(id: number) {
|
||||
try {
|
||||
await apiRequest(`/faecher/${id}`, { method: "DELETE" });
|
||||
await onGeaendert();
|
||||
} catch {
|
||||
onFehler("Fach konnte nicht gelöscht werden.");
|
||||
}
|
||||
}
|
||||
|
||||
function objekttypName(id: number) {
|
||||
return objekttypen.find((o) => o.id === id)?.name ?? `#${id}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="section">
|
||||
<h2>Fächer je Objekttyp</h2>
|
||||
<div className="card">
|
||||
{objekttypen.length === 0 ? (
|
||||
<p className="text-muted">Erst einen Objekttyp anlegen.</p>
|
||||
) : (
|
||||
<div className="row" style={{ marginBottom: faecher.length ? "1rem" : 0 }}>
|
||||
<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="Fach-Name (z. B. Fach 1)"
|
||||
/>
|
||||
<button className="btn btn-primary" onClick={anlegen} disabled={!name || wirdAngelegt}>
|
||||
Anlegen
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{faecher.length > 0 && (
|
||||
<ul className="card-list">
|
||||
{faecher.map((f) => (
|
||||
<li key={f.id} className="row-between" style={{ padding: "0.4rem 0" }}>
|
||||
<span>
|
||||
{f.name} <span className="text-muted">({objekttypName(f.objekttyp_id)})</span>
|
||||
</span>
|
||||
<button className="btn btn-danger" onClick={() => loeschen(f.id)}>
|
||||
Löschen
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { apiRequest } from "../../api/client";
|
||||
import type { Beladungsvorlage, Material, Objekttyp } from "../../api/types";
|
||||
import type { Beladungsvorlage, Fach, Material, Objekttyp } from "../../api/types";
|
||||
import { FachAuswahl } from "./FachAuswahl";
|
||||
|
||||
interface PositionZeile {
|
||||
materialId: number | "";
|
||||
@@ -15,11 +16,12 @@ interface Props {
|
||||
vorlagen: Beladungsvorlage[];
|
||||
objekttypen: Objekttyp[];
|
||||
materialien: Material[];
|
||||
faecher: Fach[];
|
||||
onGeaendert: () => Promise<void>;
|
||||
onFehler: (text: string) => void;
|
||||
}
|
||||
|
||||
export function VorlageSection({ vorlagen, objekttypen, materialien, onGeaendert, onFehler }: Props) {
|
||||
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]);
|
||||
@@ -129,12 +131,10 @@ export function VorlageSection({ vorlagen, objekttypen, materialien, onGeaendert
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<input
|
||||
className="input"
|
||||
style={{ width: "8rem" }}
|
||||
value={zeile.fach}
|
||||
onChange={(e) => zeileAendern(i, { fach: e.target.value })}
|
||||
placeholder="Fach"
|
||||
<FachAuswahl
|
||||
faecher={faecher.filter((f) => f.objekttyp_id === objekttypId)}
|
||||
wert={zeile.fach}
|
||||
onAendern={(wert) => zeileAendern(i, { fach: wert })}
|
||||
/>
|
||||
<input
|
||||
className="input"
|
||||
@@ -189,12 +189,10 @@ export function VorlageSection({ vorlagen, objekttypen, materialien, onGeaendert
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<input
|
||||
className="input"
|
||||
style={{ width: "8rem" }}
|
||||
value={zeile.fach}
|
||||
onChange={(e) => neueVersionZeileAendern(i, { fach: e.target.value })}
|
||||
placeholder="Fach"
|
||||
<FachAuswahl
|
||||
faecher={faecher.filter((f) => f.objekttyp_id === v.objekttyp_id)}
|
||||
wert={zeile.fach}
|
||||
onAendern={(wert) => neueVersionZeileAendern(i, { fach: wert })}
|
||||
/>
|
||||
<input
|
||||
className="input"
|
||||
|
||||
Reference in New Issue
Block a user