diff --git a/DEVLOG.md b/DEVLOG.md index 9f13719..773c689 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -3493,3 +3493,16 @@ Keine Commits in dieser Session. - frontend/src/pages/admin/ObjektSection.tsx | 42 ++++++++++++++++++++++++++-------- --- +## 2026-09-04 22:31 – 22:31 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** frontend + +### Commits +- a2b201d Verwaltung: breiteres Layout für Admin/Material-/Leitungsverantwortliche + +### Geänderte Dateien +- DEVLOG.md | 36 ++++++++++++++++++++++++++++++++++++ +- frontend/src/pages/AdminPage.tsx | 4 ++-- +- frontend/src/styles/global.css | 7 +++++++ + +--- diff --git a/frontend/src/pages/admin/ObjektPositionenPanel.tsx b/frontend/src/pages/admin/ObjektPositionenPanel.tsx index d53683c..717e980 100644 --- a/frontend/src/pages/admin/ObjektPositionenPanel.tsx +++ b/frontend/src/pages/admin/ObjektPositionenPanel.tsx @@ -11,6 +11,7 @@ interface Props { } interface ZeilenFormular { + sollmengeOverride: string; ablaufdatum: string; chargennummer: string; pruefintervallMonate: string; @@ -18,6 +19,7 @@ interface ZeilenFormular { function leeresFormular(p: Objektposition): ZeilenFormular { return { + sollmengeOverride: p.sollmenge_override ?? "", ablaufdatum: p.ablaufdatum ?? "", chargennummer: p.chargennummer ?? "", pruefintervallMonate: p.pruefintervall_monate?.toString() ?? "", @@ -25,24 +27,34 @@ function leeresFormular(p: Objektposition): ZeilenFormular { } /** - * 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. + * Bestückung eines Objekts pflegen (Prompt 10): Sollmenge-Override je Position + * (Abweichung vom Vorlagen-Soll), Position entfernen/reaktivieren, zusätzliches + * Material hinzufügen das nicht in der Vorlage steht - plus Ablaufdatum/Charge/ + * Prüfintervall/Geräte-Instanzen für die entsprechenden Materialtypen. Backend- + * Endpunkte existierten schon (PATCH/POST .../positionen), waren im Frontend + * bisher nur für ablauf_charge/geraet_sn und nicht für Sollmenge/Status/Neuanlage + * angebunden. */ export function ObjektPositionenPanel({ objektId, materialien, onFehler }: Props) { const [positionen, setPositionen] = useState([]); const [formulare, setFormulare] = useState>({}); const [speichertId, setSpeichertId] = useState(null); + const [neuesMaterialId, setNeuesMaterialId] = useState(""); + const [neueSollmenge, setNeueSollmenge] = useState(""); + const [wirdHinzugefuegt, setWirdHinzugefuegt] = useState(false); + + async function laden() { + try { + const liste = await apiRequest(`/objekte/${objektId}/positionen`); + setPositionen(liste); + setFormulare(Object.fromEntries(liste.map((p) => [p.id, leeresFormular(p)]))); + } catch { + onFehler("Positionen konnten nicht geladen werden."); + } + } 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.")); + laden(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [objektId]); @@ -61,6 +73,7 @@ export function ObjektPositionenPanel({ objektId, materialien, onFehler }: Props const aktualisiert = await apiRequest(`/objekte/${objektId}/positionen/${position.id}`, { method: "PATCH", body: { + sollmenge_override: formular.sollmengeOverride === "" ? null : Number(formular.sollmengeOverride), ablaufdatum: formular.ablaufdatum || null, chargennummer: formular.chargennummer || null, pruefintervall_monate: formular.pruefintervallMonate ? Number(formular.pruefintervallMonate) : null, @@ -74,73 +87,157 @@ export function ObjektPositionenPanel({ objektId, materialien, onFehler }: Props } } - 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.

; + async function statusUmschalten(position: Objektposition) { + const neuerStatus = position.ist_status === "aktiv" ? "entfernt" : "aktiv"; + try { + const aktualisiert = await apiRequest(`/objekte/${objektId}/positionen/${position.id}`, { + method: "PATCH", + body: { ist_status: neuerStatus }, + }); + setPositionen((bisherige) => bisherige.map((p) => (p.id === position.id ? aktualisiert : p))); + } catch { + onFehler("Status konnte nicht geändert werden."); + } } + async function materialHinzufuegen() { + if (neuesMaterialId === "" || !neueSollmenge) return; + setWirdHinzugefuegt(true); + try { + await apiRequest(`/objekte/${objektId}/positionen`, { + method: "POST", + body: { material_id: neuesMaterialId, sollmenge_override: Number(neueSollmenge) }, + }); + setNeuesMaterialId(""); + setNeueSollmenge(""); + await laden(); + } catch { + onFehler("Material konnte nicht hinzugefügt werden (evtl. schon vorhanden)."); + } finally { + setWirdHinzugefuegt(false); + } + } + + const vorhandeneMaterialIds = new Set(positionen.map((p) => p.material_id)); + const verfuegbareMaterialien = materialien.filter((m) => !vorhandeneMaterialIds.has(m.id)); + 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" && ( - <> +
    +
    + + setNeueSollmenge(e.target.value)} + /> + +
    + + {positionen.length === 0 &&

    Keine Positionen in diesem Objekt.

    } + +
      + {positionen.map((p) => { + const typ = materialtyp(p.material_id); + const formular = formulare[p.id] ?? leeresFormular(p); + const entfernt = p.ist_status === "entfernt"; + return ( +
    • +
      + {materialName(p.material_id)} + + {typ} + {entfernt && entfernt} + +
      +
      Soll (Vorlage/aktuell wirksam): {p.sollmenge_effektiv}
      +
      +
      + - setFormulare((f) => ({ ...f, [p.id]: { ...formular, ablaufdatum: e.target.value } })) + setFormulare((f) => ({ ...f, [p.id]: { ...formular, sollmengeOverride: e.target.value } })) } /> +
      + {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, chargennummer: e.target.value } })) + setFormulare((f) => ({ + ...f, + [p.id]: { ...formular, pruefintervallMonate: e.target.value }, + })) } /> - - )} + )} + + +
      {typ === "geraet_sn" && ( - - setFormulare((f) => ({ - ...f, - [p.id]: { ...formular, pruefintervallMonate: e.target.value }, - })) - } + )} - -
    - {typ === "geraet_sn" && ( - - )} -
  • - ); - })} -
+ + ); + })} + + ); }