Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L85hmKbvX7Cqkq47KnQhFt
209 lines
7.1 KiB
TypeScript
209 lines
7.1 KiB
TypeScript
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<Objekt | null>(null);
|
|
const [kontrolle, setKontrolle] = useState<Kontrolle | null>(null);
|
|
const [sperre, setSperre] = useState<Sperrmeldung | null>(null);
|
|
const [positionen, setPositionen] = useState<PositionZustand[]>([]);
|
|
const [startFehler, setStartFehler] = useState<string | null>(null);
|
|
const [wirdAbgeschlossen, setWirdAbgeschlossen] = useState(false);
|
|
const [abschlussFehler, setAbschlussFehler] = useState<string | null>(null);
|
|
const [nachfuellDialog, setNachfuellDialog] = useState<{ materialId: number; fehlbestandId: string } | null>(
|
|
null
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (!objektId) return;
|
|
apiRequest<Objekt>(`/objekte/${objektId}`).then(setObjekt).catch(() => undefined);
|
|
}, [objektId]);
|
|
|
|
async function starteKontrolle(uebernehmen: boolean) {
|
|
if (!objektId) return;
|
|
setStartFehler(null);
|
|
try {
|
|
const neueKontrolle = await apiRequest<Kontrolle>(`/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<Objektposition[]>(`/objekte/${kontrolle.objekt_id}/positionen`),
|
|
apiRequest<Material[]>("/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,
|
|
};
|
|
}
|