import { useEffect, useMemo, useState } from "react"; import { apiRequest, ApiError } from "../../api/client"; import type { Kontrolle, Material, Objekt, Objektposition } from "../../api/types"; import { aufErgebnisWarten, enqueue, eintraegeFuerKontrolle, entferneFehlerEintrag, synchronisiereQueue, } from "../../offline/queue"; import { useOnlineStatus } from "../../offline/useOnlineStatus"; import type { PositionZustand, Sperrmeldung } from "./types"; /** * Bündelt den kompletten Zustand/Ablauf einer Kontrolle (Prompt 11 Screens 2-6 * + Prompt 17 Offline-Härtung), damit KontrollPage.tsx nur noch die Darstellung * übernimmt. Start -> Positionen erfassen (über die lokale Queue, nie direkt * blockierend) -> Abschluss erst möglich, wenn nichts mehr unbestätigt liegt. */ export function useKontrolle(objektId: string | undefined) { const online = useOnlineStatus(); const [objekt, setObjekt] = useState(null); const [kontrolle, setKontrolle] = useState(null); const [sperre, setSperre] = useState(null); const [positionen, setPositionen] = useState([]); const [startFehler, setStartFehler] = useState(null); const [wirdAbgeschlossen, setWirdAbgeschlossen] = useState(false); const [abschlussFehler, setAbschlussFehler] = useState(null); const [nachfuellDialog, setNachfuellDialog] = useState<{ materialId: number; fehlbestandId: string } | null>( null ); useEffect(() => { if (!objektId) return; apiRequest(`/objekte/${objektId}`).then(setObjekt).catch(() => undefined); }, [objektId]); async function starteKontrolle(uebernehmen: boolean) { if (!objektId) return; setStartFehler(null); try { const neueKontrolle = await apiRequest(`/objekte/${objektId}/kontrollen`, { method: "POST", body: { uebernehmen }, }); setKontrolle(neueKontrolle); setSperre(null); } catch (error) { if (error instanceof ApiError && error.status === 409) { const detail = error.detail as { benutzer?: string; seit?: string } | null; setSperre({ benutzer: detail?.benutzer ?? null, seit: detail?.seit ?? "" }); } else { setStartFehler("Kontrolle konnte nicht gestartet werden (keine Verbindung?)."); } } } useEffect(() => { void starteKontrolle(false); // eslint-disable-next-line react-hooks/exhaustive-deps }, [objektId]); useEffect(() => { if (!kontrolle) return; Promise.all([ apiRequest(`/objekte/${kontrolle.objekt_id}/positionen`), apiRequest("/materialien"), ]).then(([positionsListe, materialListe]) => { const materialById = new Map(materialListe.map((m) => [m.id, m])); setPositionen( positionsListe .filter((p) => p.ist_status === "aktiv") .map((p) => ({ position: p, material: materialById.get(p.material_id), status: "unbestaetigt", eingabe: p.sollmenge_effektiv, ablaufdatum: p.ablaufdatum ?? "", chargennummer: p.chargennummer ?? "", seriennummer: p.seriennummer ?? "", fehlerText: null, })) ); }); }, [kontrolle]); // Reconciliation-Loop: gleicht lokalen Anzeigezustand mit der Offline-Queue ab. // Ein Eintrag, der aus der Queue verschwunden ist, wurde erfolgreich übertragen // (queue.ts entfernt Einträge ausschließlich bei Erfolg, siehe dort). useEffect(() => { if (!kontrolle) return; const interval = window.setInterval(async () => { const eintraege = await eintraegeFuerKontrolle(kontrolle.id); const byMaterial = new Map(eintraege.map((e) => [e.materialId, e])); setPositionen((bisherige) => bisherige.map((zustand) => { const inQueue = byMaterial.get(zustand.position.material_id); if (inQueue) { return { ...zustand, status: inQueue.status, fehlerText: inQueue.fehlerText }; } if (zustand.status === "wartet" || zustand.status === "wird_uebertragen") { return { ...zustand, status: "gespeichert", fehlerText: null }; } return zustand; }) ); }, 2000); return () => window.clearInterval(interval); }, [kontrolle]); useEffect(() => { if (online) void synchronisiereQueue(); }, [online]); function eingabeAendern(materialId: number, feld: keyof PositionZustand, wert: string) { setPositionen((bisherige) => bisherige.map((z) => (z.position.material_id === materialId ? { ...z, [feld]: wert } : z)) ); } async function positionSenden(materialId: number, zustand: PositionZustand) { if (!kontrolle) return; aufErgebnisWarten(kontrolle.id, materialId, (fehlbestandId, abweichung) => { if (abweichung && fehlbestandId) { setNachfuellDialog({ materialId, fehlbestandId }); } }); await enqueue(kontrolle.id, materialId, zustand.eingabe, { ablaufdatum: zustand.ablaufdatum || null, chargennummer: zustand.chargennummer || null, seriennummer: zustand.seriennummer || null, }); setPositionen((bisherige) => bisherige.map((z) => (z.position.material_id === materialId ? { ...z, status: "wartet" } : z)) ); void synchronisiereQueue(); } /** Karte 07: Sofort-Nachfüllung direkt aus dem Dialog nach einer Abweichung. */ async function jetztNachfuellen(fehlbestandId: string, menge: string) { await apiRequest(`/fehlbestaende/${fehlbestandId}/nachfuellungen`, { method: "POST", body: { menge }, }); setNachfuellDialog(null); } function nachfuellDialogSchliessen() { setNachfuellDialog(null); } async function fehlerErneutVersuchen(materialId: number, zustand: PositionZustand) { if (!kontrolle) return; await entferneFehlerEintrag(kontrolle.id, materialId); await positionSenden(materialId, zustand); } const alleUebertragen = useMemo( () => positionen.length > 0 && positionen.every((p) => p.status === "gespeichert"), [positionen] ); async function abschliessen() { if (!kontrolle) return false; setWirdAbgeschlossen(true); setAbschlussFehler(null); try { await apiRequest(`/kontrollen/${kontrolle.id}/abschliessen`, { method: "POST" }); return true; } catch (error) { if (error instanceof ApiError && error.status === 409) { setAbschlussFehler("Nicht alle Positionen wurden erfasst."); } else { setAbschlussFehler("Abschluss fehlgeschlagen (keine Verbindung?)."); } return false; } finally { setWirdAbgeschlossen(false); } } async function abbrechen(grund: string | undefined) { if (!kontrolle) return; await apiRequest(`/kontrollen/${kontrolle.id}/abbrechen`, { method: "POST", body: { grund } }); } return { online, objekt, sperre, positionen, startFehler, wirdAbgeschlossen, abschlussFehler, alleUebertragen, nachfuellDialog, starteKontrolle, eingabeAendern, positionSenden, fehlerErneutVersuchen, abschliessen, abbrechen, jetztNachfuellen, nachfuellDialogSchliessen, }; }