Sprint 8: Bereich/Kategorie/Objekttyp/Vorlage-CRUD im Admin-Portal
Vervollständigt die Stammdaten-Anlage im Frontend (bisher nur Standort/ Material/Objekt), damit ein Pilotbetrieb nicht zwingend Excel-Import braucht. Jede Sektion eigene Datei unter pages/admin/ (Projekt-Vorgabe: keine großen Monolith-Dateien). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L85hmKbvX7Cqkq47KnQhFt
This commit is contained in:
@@ -69,6 +69,26 @@ export interface Beladungsvorlage {
|
|||||||
status: string;
|
status: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Bereich {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
beschreibung: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Kategorie {
|
||||||
|
id: number;
|
||||||
|
bereich_id: number;
|
||||||
|
name: string;
|
||||||
|
ueberkategorie_id: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Objekttyp {
|
||||||
|
id: number;
|
||||||
|
bereich_id: number;
|
||||||
|
kategorie_id: number | null;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Me {
|
export interface Me {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@@ -1,22 +1,28 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { apiRequest } from "../api/client";
|
import { apiRequest } from "../api/client";
|
||||||
import type { Beladungsvorlage, Material, Objekt, Standort } from "../api/types";
|
import type { Bereich, Beladungsvorlage, Kategorie, Material, Objekt, Objekttyp, Standort } from "../api/types";
|
||||||
|
import { BereichSection } from "./admin/BereichSection";
|
||||||
|
import { KategorieSection } from "./admin/KategorieSection";
|
||||||
import { MaterialSection } from "./admin/MaterialSection";
|
import { MaterialSection } from "./admin/MaterialSection";
|
||||||
import { ObjektSection } from "./admin/ObjektSection";
|
import { ObjektSection } from "./admin/ObjektSection";
|
||||||
|
import { ObjekttypSection } from "./admin/ObjekttypSection";
|
||||||
import { StandortSection } from "./admin/StandortSection";
|
import { StandortSection } from "./admin/StandortSection";
|
||||||
|
import { VorlageSection } from "./admin/VorlageSection";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Minimale Admin-Seite (Standort/Material/Objekt-Anlage), nachgezogen weil
|
* Admin-Seite (Stammdaten/Objekt-Anlage), nachgezogen weil das Frontend
|
||||||
* das Frontend bisher nur den Mitarbeiter-Kontrollflow (Sprint 7) abdeckte
|
* bisher nur den Mitarbeiter-Kontrollflow (Sprint 7) abdeckte und
|
||||||
* und Stammdaten/Objekte nur per API/Skript angelegt werden konnten. Bewusst
|
* Stammdaten/Objekte nur per API/Skript angelegt werden konnten.
|
||||||
* schlank: keine Bereich-/Kategorie-/Vorlagen-CRUD, nur was für die
|
|
||||||
* Pilotphase sofort nötig ist (Vorlagen kommen aus dem Excel-Import).
|
|
||||||
*
|
*
|
||||||
* Jeder Abschnitt (Standort/Material/Objekt) ist eine eigene Komponente unter
|
* Jeder Abschnitt ist eine eigene Komponente unter pages/admin/ - hält diese
|
||||||
* pages/admin/ - hält diese Datei als reinen Orchestrator klein.
|
* Datei als reinen Orchestrator klein (siehe Projekt-Vorgabe: keine großen
|
||||||
|
* Monolith-Dateien).
|
||||||
*/
|
*/
|
||||||
export function AdminPage() {
|
export function AdminPage() {
|
||||||
|
const [bereiche, setBereiche] = useState<Bereich[]>([]);
|
||||||
|
const [kategorien, setKategorien] = useState<Kategorie[]>([]);
|
||||||
|
const [objekttypen, setObjekttypen] = useState<Objekttyp[]>([]);
|
||||||
const [standorte, setStandorte] = useState<Standort[]>([]);
|
const [standorte, setStandorte] = useState<Standort[]>([]);
|
||||||
const [vorlagen, setVorlagen] = useState<Beladungsvorlage[]>([]);
|
const [vorlagen, setVorlagen] = useState<Beladungsvorlage[]>([]);
|
||||||
const [objekte, setObjekte] = useState<Objekt[]>([]);
|
const [objekte, setObjekte] = useState<Objekt[]>([]);
|
||||||
@@ -25,14 +31,20 @@ export function AdminPage() {
|
|||||||
const [laedt, setLaedt] = useState(true);
|
const [laedt, setLaedt] = useState(true);
|
||||||
|
|
||||||
async function ladeAlles() {
|
async function ladeAlles() {
|
||||||
const [s, v, o, m] = await Promise.all([
|
const [be, ka, ot, s, v, o, m] = await Promise.all([
|
||||||
|
apiRequest<Bereich[]>("/bereiche"),
|
||||||
|
apiRequest<Kategorie[]>("/kategorien"),
|
||||||
|
apiRequest<Objekttyp[]>("/objekttypen"),
|
||||||
apiRequest<Standort[]>("/standorte"),
|
apiRequest<Standort[]>("/standorte"),
|
||||||
apiRequest<Beladungsvorlage[]>("/vorlagen"),
|
apiRequest<Beladungsvorlage[]>("/vorlagen"),
|
||||||
apiRequest<Objekt[]>("/objekte"),
|
apiRequest<Objekt[]>("/objekte"),
|
||||||
apiRequest<Material[]>("/materialien"),
|
apiRequest<Material[]>("/materialien"),
|
||||||
]);
|
]);
|
||||||
|
setBereiche(be);
|
||||||
|
setKategorien(ka);
|
||||||
|
setObjekttypen(ot);
|
||||||
setStandorte(s);
|
setStandorte(s);
|
||||||
setVorlagen(v.filter((vorlage) => vorlage.status === "aktiv"));
|
setVorlagen(v);
|
||||||
setObjekte(o);
|
setObjekte(o);
|
||||||
setMaterialien(m);
|
setMaterialien(m);
|
||||||
}
|
}
|
||||||
@@ -56,11 +68,27 @@ export function AdminPage() {
|
|||||||
<h1>Administration</h1>
|
<h1>Administration</h1>
|
||||||
{fehler && <div className="alert alert-danger">{fehler}</div>}
|
{fehler && <div className="alert alert-danger">{fehler}</div>}
|
||||||
|
|
||||||
<StandortSection standorte={standorte} onGeaendert={ladeAlles} onFehler={setFehler} />
|
<BereichSection bereiche={bereiche} onGeaendert={ladeAlles} onFehler={setFehler} />
|
||||||
|
<KategorieSection bereiche={bereiche} kategorien={kategorien} onGeaendert={ladeAlles} onFehler={setFehler} />
|
||||||
|
<ObjekttypSection
|
||||||
|
bereiche={bereiche}
|
||||||
|
kategorien={kategorien}
|
||||||
|
objekttypen={objekttypen}
|
||||||
|
onGeaendert={ladeAlles}
|
||||||
|
onFehler={setFehler}
|
||||||
|
/>
|
||||||
<MaterialSection materialien={materialien} onGeaendert={ladeAlles} onFehler={setFehler} />
|
<MaterialSection materialien={materialien} onGeaendert={ladeAlles} onFehler={setFehler} />
|
||||||
|
<VorlageSection
|
||||||
|
vorlagen={vorlagen}
|
||||||
|
objekttypen={objekttypen}
|
||||||
|
materialien={materialien}
|
||||||
|
onGeaendert={ladeAlles}
|
||||||
|
onFehler={setFehler}
|
||||||
|
/>
|
||||||
|
<StandortSection standorte={standorte} onGeaendert={ladeAlles} onFehler={setFehler} />
|
||||||
<ObjektSection
|
<ObjektSection
|
||||||
objekte={objekte}
|
objekte={objekte}
|
||||||
vorlagen={vorlagen}
|
vorlagen={vorlagen.filter((v) => v.status === "aktiv")}
|
||||||
standorte={standorte}
|
standorte={standorte}
|
||||||
onGeaendert={ladeAlles}
|
onGeaendert={ladeAlles}
|
||||||
onFehler={setFehler}
|
onFehler={setFehler}
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import { apiRequest } from "../../api/client";
|
||||||
|
import type { Bereich } from "../../api/types";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
bereiche: Bereich[];
|
||||||
|
onGeaendert: () => Promise<void>;
|
||||||
|
onFehler: (text: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BereichSection({ bereiche, onGeaendert, onFehler }: Props) {
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
const [wirdAngelegt, setWirdAngelegt] = useState(false);
|
||||||
|
|
||||||
|
async function anlegen() {
|
||||||
|
setWirdAngelegt(true);
|
||||||
|
try {
|
||||||
|
await apiRequest("/bereiche", { method: "POST", body: { name } });
|
||||||
|
setName("");
|
||||||
|
await onGeaendert();
|
||||||
|
} catch {
|
||||||
|
onFehler("Bereich konnte nicht angelegt werden.");
|
||||||
|
} finally {
|
||||||
|
setWirdAngelegt(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="section">
|
||||||
|
<h2>Bereich anlegen</h2>
|
||||||
|
<div className="card">
|
||||||
|
<div className="row" style={{ marginBottom: bereiche.length ? "1rem" : 0 }}>
|
||||||
|
<input className="input" value={name} onChange={(e) => setName(e.target.value)} placeholder="Name" />
|
||||||
|
<button className="btn btn-primary" onClick={anlegen} disabled={!name || wirdAngelegt}>
|
||||||
|
Anlegen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{bereiche.length > 0 && (
|
||||||
|
<ul className="card-list">
|
||||||
|
{bereiche.map((b) => (
|
||||||
|
<li key={b.id} style={{ padding: "0.4rem 0" }}>
|
||||||
|
{b.name}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import { apiRequest } from "../../api/client";
|
||||||
|
import type { Bereich, Kategorie } from "../../api/types";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
bereiche: Bereich[];
|
||||||
|
kategorien: Kategorie[];
|
||||||
|
onGeaendert: () => Promise<void>;
|
||||||
|
onFehler: (text: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function KategorieSection({ bereiche, kategorien, onGeaendert, onFehler }: Props) {
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
const [bereichId, setBereichId] = useState<number | "">("");
|
||||||
|
const [wirdAngelegt, setWirdAngelegt] = useState(false);
|
||||||
|
|
||||||
|
async function anlegen() {
|
||||||
|
if (bereichId === "") {
|
||||||
|
onFehler("Bereich auswählen.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setWirdAngelegt(true);
|
||||||
|
try {
|
||||||
|
await apiRequest("/kategorien", { method: "POST", body: { name, bereich_id: bereichId } });
|
||||||
|
setName("");
|
||||||
|
await onGeaendert();
|
||||||
|
} catch {
|
||||||
|
onFehler("Kategorie konnte nicht angelegt werden.");
|
||||||
|
} finally {
|
||||||
|
setWirdAngelegt(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bereichName(id: number) {
|
||||||
|
return bereiche.find((b) => b.id === id)?.name ?? `#${id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="section">
|
||||||
|
<h2>Kategorie anlegen</h2>
|
||||||
|
<div className="card">
|
||||||
|
{bereiche.length === 0 ? (
|
||||||
|
<p className="text-muted">Erst einen Bereich anlegen.</p>
|
||||||
|
) : (
|
||||||
|
<div className="stack" style={{ marginBottom: kategorien.length ? "1.25rem" : 0 }}>
|
||||||
|
<div className="row">
|
||||||
|
<select className="input" value={bereichId} onChange={(e) => setBereichId(Number(e.target.value) || "")}>
|
||||||
|
<option value="">Bereich wählen…</option>
|
||||||
|
{bereiche.map((b) => (
|
||||||
|
<option key={b.id} value={b.id}>
|
||||||
|
{b.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<input className="input" value={name} onChange={(e) => setName(e.target.value)} placeholder="Name" />
|
||||||
|
<button className="btn btn-primary" onClick={anlegen} disabled={!name || wirdAngelegt}>
|
||||||
|
Anlegen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{kategorien.length > 0 && (
|
||||||
|
<ul className="card-list">
|
||||||
|
{kategorien.map((k) => (
|
||||||
|
<li key={k.id} className="row-between" style={{ padding: "0.4rem 0" }}>
|
||||||
|
<span>{k.name}</span>
|
||||||
|
<span className="text-muted">{bereichName(k.bereich_id)}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import { apiRequest } from "../../api/client";
|
||||||
|
import type { Bereich, Kategorie, Objekttyp } from "../../api/types";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
bereiche: Bereich[];
|
||||||
|
kategorien: Kategorie[];
|
||||||
|
objekttypen: Objekttyp[];
|
||||||
|
onGeaendert: () => Promise<void>;
|
||||||
|
onFehler: (text: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ObjekttypSection({ bereiche, kategorien, objekttypen, onGeaendert, onFehler }: Props) {
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
const [bereichId, setBereichId] = useState<number | "">("");
|
||||||
|
const [kategorieId, setKategorieId] = useState<number | "">("");
|
||||||
|
const [wirdAngelegt, setWirdAngelegt] = useState(false);
|
||||||
|
|
||||||
|
async function anlegen() {
|
||||||
|
if (bereichId === "") {
|
||||||
|
onFehler("Bereich auswählen.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setWirdAngelegt(true);
|
||||||
|
try {
|
||||||
|
await apiRequest("/objekttypen", {
|
||||||
|
method: "POST",
|
||||||
|
body: { name, bereich_id: bereichId, kategorie_id: kategorieId || null },
|
||||||
|
});
|
||||||
|
setName("");
|
||||||
|
await onGeaendert();
|
||||||
|
} catch {
|
||||||
|
onFehler("Objekttyp konnte nicht angelegt werden.");
|
||||||
|
} finally {
|
||||||
|
setWirdAngelegt(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="section">
|
||||||
|
<h2>Objekttyp anlegen</h2>
|
||||||
|
<div className="card">
|
||||||
|
{bereiche.length === 0 ? (
|
||||||
|
<p className="text-muted">Erst einen Bereich anlegen.</p>
|
||||||
|
) : (
|
||||||
|
<div className="stack" style={{ marginBottom: objekttypen.length ? "1.25rem" : 0 }}>
|
||||||
|
<div className="row">
|
||||||
|
<select className="input" value={bereichId} onChange={(e) => setBereichId(Number(e.target.value) || "")}>
|
||||||
|
<option value="">Bereich wählen…</option>
|
||||||
|
{bereiche.map((b) => (
|
||||||
|
<option key={b.id} value={b.id}>
|
||||||
|
{b.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<select className="input" value={kategorieId} onChange={(e) => setKategorieId(Number(e.target.value) || "")}>
|
||||||
|
<option value="">Kategorie (optional)…</option>
|
||||||
|
{kategorien.map((k) => (
|
||||||
|
<option key={k.id} value={k.id}>
|
||||||
|
{k.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="row">
|
||||||
|
<input className="input" value={name} onChange={(e) => setName(e.target.value)} placeholder="Name" />
|
||||||
|
<button className="btn btn-primary" onClick={anlegen} disabled={!name || wirdAngelegt}>
|
||||||
|
Anlegen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{objekttypen.length > 0 && (
|
||||||
|
<ul className="card-list">
|
||||||
|
{objekttypen.map((o) => (
|
||||||
|
<li key={o.id} style={{ padding: "0.4rem 0" }}>
|
||||||
|
{o.name}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import { apiRequest } from "../../api/client";
|
||||||
|
import type { Beladungsvorlage, Material, Objekttyp } from "../../api/types";
|
||||||
|
|
||||||
|
interface PositionZeile {
|
||||||
|
materialId: number | "";
|
||||||
|
fach: string;
|
||||||
|
sollmenge: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LEERE_ZEILE: PositionZeile = { materialId: "", fach: "", sollmenge: "1" };
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
vorlagen: Beladungsvorlage[];
|
||||||
|
objekttypen: Objekttyp[];
|
||||||
|
materialien: Material[];
|
||||||
|
onGeaendert: () => Promise<void>;
|
||||||
|
onFehler: (text: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function VorlageSection({ vorlagen, objekttypen, materialien, onGeaendert, onFehler }: Props) {
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
const [objekttypId, setObjekttypId] = useState<number | "">("");
|
||||||
|
const [zeilen, setZeilen] = useState<PositionZeile[]>([LEERE_ZEILE]);
|
||||||
|
const [wirdAngelegt, setWirdAngelegt] = useState(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>
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
style={{ width: "8rem" }}
|
||||||
|
value={zeile.fach}
|
||||||
|
onChange={(e) => zeileAendern(i, { fach: e.target.value })}
|
||||||
|
placeholder="Fach"
|
||||||
|
/>
|
||||||
|
<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} className="row-between" style={{ padding: "0.4rem 0" }}>
|
||||||
|
<span>{v.name}</span>
|
||||||
|
<span className="badge badge-neutral">v{v.version}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user