import { useEffect, useMemo, useState } from "react"; import { apiRequest } from "../../api/client"; import type { Beladungsvorlage, Material, Objektposition } from "../../api/types"; import { gruppiereNachFach, OHNE_FACH } from "../../utils/fachGruppierung"; import { GeraeteInstanzenListe } from "./GeraeteInstanzenListe"; interface Props { objektId: number; vorlageId: number | null; materialien: Material[]; onFehler: (text: string) => void; } interface ZeilenFormular { sollmengeOverride: string; ablaufdatum: string; chargennummer: string; pruefintervallMonate: string; } function leeresFormular(p: Objektposition): ZeilenFormular { return { sollmengeOverride: p.sollmenge_override ?? "", ablaufdatum: p.ablaufdatum ?? "", chargennummer: p.chargennummer ?? "", pruefintervallMonate: p.pruefintervall_monate?.toString() ?? "", }; } /** * 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. * * Nutzer-Vorgabe: Fach-Sidebar links, damit die Bestückung übersichtlich bleibt * UND damit auch Dritte (Vertretung, wenn die Kontrollperson nicht da ist) * auf einen Blick sehen, wo welches Material im Rucksack hingehört - gleiche * Fach-Zuordnung wie in der Kontroll-Erfassung (Vorlagenposition.fach). */ export function ObjektPositionenPanel({ objektId, vorlageId, materialien, onFehler }: Props) { const [positionen, setPositionen] = useState([]); const [vorlage, setVorlage] = useState(null); const [formulare, setFormulare] = useState>({}); const [speichertId, setSpeichertId] = useState(null); const [neuesMaterialId, setNeuesMaterialId] = useState(""); const [neueSollmenge, setNeueSollmenge] = useState(""); const [wirdHinzugefuegt, setWirdHinzugefuegt] = useState(false); const [aktivesFach, setAktivesFach] = useState(null); async function laden() { try { const [liste, vorlageDaten] = await Promise.all([ apiRequest(`/objekte/${objektId}/positionen`), vorlageId ? apiRequest(`/vorlagen/${vorlageId}`) : Promise.resolve(null), ]); setPositionen(liste); setVorlage(vorlageDaten); setFormulare(Object.fromEntries(liste.map((p) => [p.id, leeresFormular(p)]))); } catch { onFehler("Positionen konnten nicht geladen werden."); } } useEffect(() => { laden(); setAktivesFach(null); // eslint-disable-next-line react-hooks/exhaustive-deps }, [objektId, vorlageId]); const fachByMaterialId = useMemo(() => { const map = new Map(); for (const vp of vorlage?.positionen ?? []) { map.set(vp.material_id, vp.fach); } return map; }, [vorlage]); const fachGruppen = useMemo( () => gruppiereNachFach(positionen, (p) => fachByMaterialId.get(p.material_id) ?? null), [positionen, fachByMaterialId] ); const sichtbarePositionen = aktivesFach ? fachGruppen.find(([fach]) => fach === aktivesFach)?.[1] ?? [] : positionen; 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: { sollmenge_override: formular.sollmengeOverride === "" ? null : Number(formular.sollmengeOverride), ablaufdatum: formular.ablaufdatum || null, chargennummer: formular.chargennummer || null, pruefintervall_monate: formular.pruefintervallMonate ? Number(formular.pruefintervallMonate) : null, }, }); setPositionen((bisherige) => bisherige.map((p) => (p.id === position.id ? aktualisiert : p))); } catch { onFehler("Position konnte nicht gespeichert werden."); } finally { setSpeichertId(null); } } 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 (
setNeueSollmenge(e.target.value)} />
{positionen.length === 0 &&

Keine Positionen in diesem Objekt.

} {positionen.length > 0 && (
    {sichtbarePositionen.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)} {fachByMaterialId.get(p.material_id) ?? OHNE_FACH} {typ} {entfernt && entfernt}
    Soll (Vorlage/aktuell wirksam): {p.sollmenge_effektiv}
    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, pruefintervallMonate: e.target.value }, })) } /> )}
    {typ === "geraet_sn" && ( )}
  • ); })}
)}
); }