From a6e68b0a948927a6d48ada857505408e56dcab9b Mon Sep 17 00:00:00 2001 From: patrick Date: Fri, 4 Sep 2026 01:03:37 +0200 Subject: [PATCH] Sprint 8: Ablaufdatum/Chargennummer/Seriennummer im Admin-Portal pflegbar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nutzer-Fund: bei keinem Objekt war Chargennummer/Ablaufdatum sichtbar. Backend-Endpoint (PATCH /objekte/{id}/positionen/{position_id}) existierte bereits (Prompt 10), war im Frontend nirgends angebunden. Kontroll-Screen selbst bleibt unverändert (Vier-Kernbegriffe: Kontrolle erfasst nur Menge, Ablauf/Charge/SN gehören zur Objektposition) - neue Pflege-Ansicht im Admin-Portal pro Objekt, nur für materialtyp ablauf_charge/geraet_sn. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01L85hmKbvX7Cqkq47KnQhFt --- frontend/src/pages/AdminPage.tsx | 1 + .../src/pages/admin/ObjektPositionenPanel.tsx | 133 ++++++++++++++++++ frontend/src/pages/admin/ObjektSection.tsx | 27 +++- 3 files changed, 156 insertions(+), 5 deletions(-) create mode 100644 frontend/src/pages/admin/ObjektPositionenPanel.tsx diff --git a/frontend/src/pages/AdminPage.tsx b/frontend/src/pages/AdminPage.tsx index a855178..540b1d2 100644 --- a/frontend/src/pages/AdminPage.tsx +++ b/frontend/src/pages/AdminPage.tsx @@ -90,6 +90,7 @@ export function AdminPage() { objekte={objekte} vorlagen={vorlagen.filter((v) => v.status === "aktiv")} standorte={standorte} + materialien={materialien} onGeaendert={ladeAlles} onFehler={setFehler} /> diff --git a/frontend/src/pages/admin/ObjektPositionenPanel.tsx b/frontend/src/pages/admin/ObjektPositionenPanel.tsx new file mode 100644 index 0000000..0468659 --- /dev/null +++ b/frontend/src/pages/admin/ObjektPositionenPanel.tsx @@ -0,0 +1,133 @@ +import { useEffect, useState } from "react"; + +import { apiRequest } from "../../api/client"; +import type { Material, Objektposition } from "../../api/types"; + +interface Props { + objektId: number; + materialien: Material[]; + onFehler: (text: string) => void; +} + +interface ZeilenFormular { + ablaufdatum: string; + chargennummer: string; + seriennummer: string; +} + +function leeresFormular(p: Objektposition): ZeilenFormular { + return { + ablaufdatum: p.ablaufdatum ?? "", + chargennummer: p.chargennummer ?? "", + seriennummer: p.seriennummer ?? "", + }; +} + +/** + * Prompt 10: Ablaufdatum/Chargennummer (materialtyp 'ablauf_charge') und + * Seriennummer (materialtyp 'geraet_sn') gehören zur Objektposition, nicht + * zur Kontrolle selbst (Vier-Kernbegriffe: Kontrolle erfasst nur die Menge). + * Backend-Endpoint (PATCH .../positionen/{id}) existierte schon, war im + * Frontend bisher nirgends angebunden. + */ +export function ObjektPositionenPanel({ objektId, materialien, onFehler }: Props) { + const [positionen, setPositionen] = useState([]); + const [formulare, setFormulare] = useState>({}); + const [speichertId, setSpeichertId] = useState(null); + + useEffect(() => { + apiRequest(`/objekte/${objektId}/positionen`) + .then((liste) => { + setPositionen(liste); + setFormulare(Object.fromEntries(liste.map((p) => [p.id, leeresFormular(p)]))); + }) + .catch(() => onFehler("Positionen konnten nicht geladen werden.")); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [objektId]); + + function materialName(materialId: number) { + return materialien.find((m) => m.id === materialId)?.name ?? `Material ${materialId}`; + } + + function materialtyp(materialId: number) { + return materialien.find((m) => m.id === materialId)?.materialtyp; + } + + async function speichern(position: Objektposition) { + const formular = formulare[position.id]; + setSpeichertId(position.id); + try { + const aktualisiert = await apiRequest(`/objekte/${objektId}/positionen/${position.id}`, { + method: "PATCH", + body: { + ablaufdatum: formular.ablaufdatum || null, + chargennummer: formular.chargennummer || null, + seriennummer: formular.seriennummer || null, + }, + }); + setPositionen((bisherige) => bisherige.map((p) => (p.id === position.id ? aktualisiert : p))); + } catch { + onFehler("Position konnte nicht gespeichert werden."); + } finally { + setSpeichertId(null); + } + } + + const relevante = positionen.filter((p) => materialtyp(p.material_id) !== "standard"); + + if (relevante.length === 0) { + return

Kein Material mit Ablaufdatum/Charge oder Seriennummer in diesem Objekt.

; + } + + return ( +
    + {relevante.map((p) => { + const typ = materialtyp(p.material_id); + const formular = formulare[p.id] ?? leeresFormular(p); + return ( +
  • +
    + {materialName(p.material_id)} + {typ} +
    +
    + {typ === "ablauf_charge" && ( + <> + + setFormulare((f) => ({ ...f, [p.id]: { ...formular, ablaufdatum: e.target.value } })) + } + /> + + setFormulare((f) => ({ ...f, [p.id]: { ...formular, chargennummer: e.target.value } })) + } + /> + + )} + {typ === "geraet_sn" && ( + + setFormulare((f) => ({ ...f, [p.id]: { ...formular, seriennummer: e.target.value } })) + } + /> + )} + +
    +
  • + ); + })} +
+ ); +} diff --git a/frontend/src/pages/admin/ObjektSection.tsx b/frontend/src/pages/admin/ObjektSection.tsx index eb3b9fb..b60373a 100644 --- a/frontend/src/pages/admin/ObjektSection.tsx +++ b/frontend/src/pages/admin/ObjektSection.tsx @@ -1,22 +1,25 @@ import { useState } from "react"; import { apiRequest } from "../../api/client"; -import type { Beladungsvorlage, Objekt, Standort } from "../../api/types"; +import type { Beladungsvorlage, Material, Objekt, Standort } from "../../api/types"; +import { ObjektPositionenPanel } from "./ObjektPositionenPanel"; interface Props { objekte: Objekt[]; vorlagen: Beladungsvorlage[]; standorte: Standort[]; + materialien: Material[]; onGeaendert: () => Promise; onFehler: (text: string) => void; } -export function ObjektSection({ objekte, vorlagen, standorte, onGeaendert, onFehler }: Props) { +export function ObjektSection({ objekte, vorlagen, standorte, materialien, onGeaendert, onFehler }: Props) { const [code, setCode] = useState(""); const [name, setName] = useState(""); const [vorlageId, setVorlageId] = useState(""); const [standortId, setStandortId] = useState(""); const [wirdAngelegt, setWirdAngelegt] = useState(false); + const [ausgeklapptId, setAusgeklapptId] = useState(null); async function anlegen() { if (vorlageId === "" || standortId === "") { @@ -99,9 +102,23 @@ export function ObjektSection({ objekte, vorlagen, standorte, onGeaendert, onFeh {objekte.length > 0 && (
    {objekte.map((o) => ( -
  • - {o.name} - {o.code} +
  • +
    + + {o.name} ({o.code}) + + +
    + {ausgeklapptId === o.id && ( +
    + +
    + )}
  • ))}