import { createStore, del, entries, set } from "idb-keyval"; import { apiRequest, ApiError } from "../api/client"; import type { Kontrollposition } from "../api/types"; // Prompt 17: Eingaben werden lokal zwischengespeichert, sobald eingegeben - nicht // erst bei Übertragung an den Server verworfen, wenn die Verbindung kurz fehlt. // IndexedDB statt reinem Service-Worker-Cache, weil hier tatsächliche Nutzdaten // (Ist-Mengen) über einen Neustart/Tab-Wechsel hinweg erhalten bleiben müssen. const store = createStore("mabea-offline-queue", "kontrollpositionen"); export type QueueStatus = "wartet" | "wird_uebertragen" | "fehler"; export interface QueueItem { key: string; // `${kontrolleId}:${materialId}` kontrolleId: string; materialId: number; istmenge: string; status: QueueStatus; fehlerText: string | null; fehlbestandId: string | null; // wird nach erfolgreicher Übertragung befüllt } function keyFor(kontrolleId: string, materialId: number): string { return `${kontrolleId}:${materialId}`; } export async function enqueue(kontrolleId: string, materialId: number, istmenge: string): Promise { const key = keyFor(kontrolleId, materialId); const item: QueueItem = { key, kontrolleId, materialId, istmenge, status: "wartet", fehlerText: null, fehlbestandId: null, }; await set(key, item, store); } export async function alleEintraege(): Promise { const all = await entries(store); return all.map(([, value]) => value); } export async function eintraegeFuerKontrolle(kontrolleId: string): Promise { const all = await alleEintraege(); return all.filter((item) => item.kontrolleId === kontrolleId); } /** Prompt 16: Abschluss darf erst möglich sein, wenn nichts mehr unbestätigt in * der Warteschlange liegt (weder "wartet" noch "wird_uebertragen"). Ein * "fehler"-Eintrag blockiert ebenfalls - der Mitarbeiter muss ihn sehen und * bewusst erneut versuchen, kein stiller Datenverlust. */ export async function hatUnbestaetigtePositionen(kontrolleId: string): Promise { const eintraege = await eintraegeFuerKontrolle(kontrolleId); return eintraege.length > 0; } async function sendeEintrag(item: QueueItem): Promise { await set(item.key, { ...item, status: "wird_uebertragen" as QueueStatus }, store); try { const ergebnis = await apiRequest( `/kontrollen/${item.kontrolleId}/positionen/${item.materialId}`, { method: "PUT", body: { istmenge: item.istmenge } } ); // Erfolgreich übertragen (idempotent - ein erneutes Senden desselben Werts // durch die Queue vor einem Absturz führt zu keinem doppelten Fehlbestand, // siehe Backend app/services/kontrolle.py erfasse_position) -> aus Queue entfernen. void ergebnis; await del(item.key, store); } catch (error) { if (error instanceof ApiError && error.status >= 400 && error.status < 500) { // Client-Fehler (z. B. 403 Objekt-Sperre an anderen Benutzer übergegangen, // 404 Kontrolle nicht mehr aktiv) - erneutes Senden würde nichts ändern, // aber der Eintrag bleibt sichtbar in der Queue, bis der Mitarbeiter reagiert. await set( item.key, { ...item, status: "fehler" as QueueStatus, fehlerText: `Server hat abgelehnt (${error.status}) - bitte Kontrolle neu prüfen.`, }, store ); } else { // Netzwerkfehler - bleibt "wartet", nächster Sync-Versuch übernimmt wieder. await set(item.key, { ...item, status: "wartet" as QueueStatus }, store); } throw error; } } /** Wird bei Reconnect (online-Event) und periodisch aufgerufen. Sendet alle * wartenden Einträge, bricht bei Netzwerkfehlern early ab (weitere Versuche * würden ohnehin scheitern, solange keine Verbindung besteht). */ export async function synchronisiereQueue(): Promise { const eintraege = await alleEintraege(); for (const item of eintraege) { if (item.status === "fehler") continue; // braucht bewusste Nutzer-Aktion, kein Auto-Retry try { await sendeEintrag(item); } catch (error) { if (!(error instanceof ApiError)) { return; // vermutlich offline - restliche Queue in Ruhe lassen, nicht durchhämmern } } } } export async function entferneFehlerEintrag(kontrolleId: string, materialId: number): Promise { await del(keyFor(kontrolleId, materialId), store); }