feat(mobile): MOBILE-003 Scope-Erweiterung - drei weitere Offline-Queues
Nutzer-Entscheidung revidiert bisherige Scope-Begrenzung (2026-09-10) auf Kontrolle/Mangel. Neu: Gerätestatus, Wartungsauftrag, Lagerbewegung laufen jetzt über eigene Offline-Queues statt bei Verbindungsverlust sofort zu scheitern. Weiterhin kein genereller Endpunkt-Layer (bleibt abgelehnt) - fünf eigenständige Queues teilen sich nur die Sync-Mechanik (offline/draftQueue.ts). Lagerbuchungen bewusst anders: Bestandskonflikte (4xx) werden weiterhin sofort angezeigt statt erst beim Sync-Versuch, nur echte Netzwerkfehler landen in der Queue. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VDMvneg6wKfXdXvTYNd3dN
This commit is contained in:
1 parent
f4e8a83097
commit
c440ccd05e
10 files changed
+358
-35
No files matched your search
@@ -95,10 +95,29 @@ Details zu MOBILE-001 … MOBILE-005 (vollständig).
|
||||
- **Kein neuer Unit-Test** — folgt demselben (bestehenden) Präzedenzfall:
|
||||
`offline/queue.ts` (Kontrolle) hat ebenfalls keinen automatisierten Test,
|
||||
IndexedDB-Interaktion ist hier bewusst nicht isoliert getestet.
|
||||
- **Bewusst NICHT abgedeckt:** alle anderen Schreib-Aktionen (Geräte-
|
||||
Status ändern, Wartungsauftrag anlegen, Lagerbuchungen, …) bleiben ohne
|
||||
Offline-Queue — schlagen bei Verbindungsverlust weiterhin sofort fehl,
|
||||
wie vorher. Kein generischer Offline-Layer, siehe Scope-Entscheidung oben.
|
||||
- **Update 2026-09-13 (Nutzer-Entscheidung: bisherige Scope-Begrenzung
|
||||
bewusst revidiert):** Gerätestatus-Änderung, Wartungsauftrag-Anlage und
|
||||
Lagerbuchungen (Ein-/Auslagern/Umlagern) laufen jetzt ebenfalls über
|
||||
Offline-Queues — weiterhin KEIN genereller "beliebiger Endpunkt"-Layer
|
||||
(das bleibt abgelehnt), sondern fünf eigenständige, feature-spezifische
|
||||
Queues, die sich eine gemeinsame Sync-Mechanik teilen
|
||||
(`frontend/src/offline/draftQueue.ts`, neu: `erzeugeDraftQueue()`
|
||||
kapselt enqueue/sendeEintrag/synchronisiere, jede Queue bringt nur
|
||||
eigenen Item-Typ + `sende()`-Funktion mit). Neue Dateien:
|
||||
`geraetStatusQueue.ts`, `wartungQueue.ts`, `lagerbewegungQueue.ts`.
|
||||
**Bewusster Unterschied bei Lagerbuchungen:** anders als die anderen vier
|
||||
Queues (immer sofort in die Queue, dann Sync-Versuch) prüft
|
||||
`LagerSection.tsx` zuerst online, ob die Buchung an echten
|
||||
Geschäftsregeln scheitert (z.B. "nicht genug Bestand", ein `ApiError`,
|
||||
4xx) — nur ein echter Netzwerkfehler (kein `ApiError`) landet in der
|
||||
Queue. Ein Bestandskonflikt soll sofort sichtbar sein, nicht erst beim
|
||||
späteren Sync-Versuch unbemerkt fehlschlagen. `useOnlineStatus.ts`
|
||||
synchronisiert jetzt alle fünf Queues bei Reconnect, `SyncStatus.tsx`
|
||||
zählt wartende Einträge aus allen zusammen (zwei Feldnamen-Generationen:
|
||||
ältere Queues `status`, neue `queueStatus`, bewusst nicht vereinheitlicht
|
||||
nur für diese eine Zählung). **Kein neuer Unit-Test** — folgt demselben
|
||||
Präzedenzfall wie die ursprüngliche Kontroll-/Mangel-Queue (IndexedDB-
|
||||
Interaktion bleibt bewusst nicht isoliert getestet, siehe oben).
|
||||
- **Referenz (Open-Source-Vergleich 2026-09-09):** KP Front (`feuerwehr-oberwil/kp-front`,
|
||||
identischer Stack FastAPI/PostgreSQL/React-Vite-PWA, AGPL-3.0-or-later — nur Muster,
|
||||
keine Codeübernahme) löst das nicht als globale Offline-Queue, sondern als
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useEffect, useState } from "react";
|
||||
import { apiRequest } from "../api/client";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import type { ExterneStelle, FaelligeWartung, Wartungsauftrag, WartungsauftragTeil } from "../api/types";
|
||||
import * as wartungQueue from "../offline/wartungQueue";
|
||||
import { DokumentePanel } from "./DokumentePanel";
|
||||
|
||||
interface Props {
|
||||
@@ -75,21 +76,20 @@ export function WartungSection({ objektId, onFehler }: Props) {
|
||||
onFehler("Wartungsart angeben.");
|
||||
return;
|
||||
}
|
||||
// MOBILE-003-Erweiterung: über die Offline-Queue statt blockierend.
|
||||
try {
|
||||
await apiRequest("/wartungsauftraege", {
|
||||
method: "POST",
|
||||
body: {
|
||||
objekt_id: objektId,
|
||||
wartungsart: neuArt,
|
||||
werkstatt_intern: !neuExtern,
|
||||
werkstatt_name: neuExtern && neuDienstleisterId === "" ? neuWerkstattName || null : null,
|
||||
dienstleister_id: neuExtern && neuDienstleisterId !== "" ? neuDienstleisterId : null,
|
||||
},
|
||||
await wartungQueue.enqueue({
|
||||
objektId,
|
||||
wartungsart: neuArt,
|
||||
werkstattIntern: !neuExtern,
|
||||
werkstattName: neuExtern && neuDienstleisterId === "" ? neuWerkstattName || null : null,
|
||||
dienstleisterId: neuExtern && neuDienstleisterId !== "" ? neuDienstleisterId : null,
|
||||
});
|
||||
setNeuArt("");
|
||||
setNeuExtern(false);
|
||||
setNeuWerkstattName("");
|
||||
setNeuDienstleisterId("");
|
||||
await wartungQueue.synchronisiereWartungQueue();
|
||||
await laden();
|
||||
} catch {
|
||||
onFehler("Wartungsauftrag konnte nicht angelegt werden.");
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { alleEintraege } from "../../offline/queue";
|
||||
import { alleEintraege as alleGeraetStatusEintraege } from "../../offline/geraetStatusQueue";
|
||||
import { alleEintraege as alleLagerbewegungEintraege } from "../../offline/lagerbewegungQueue";
|
||||
import { alleEintraege as alleMangelEintraege } from "../../offline/mangelQueue";
|
||||
import { alleEintraege } from "../../offline/queue";
|
||||
import { useOnlineStatus } from "../../offline/useOnlineStatus";
|
||||
import { alleEintraege as alleWartungEintraege } from "../../offline/wartungQueue";
|
||||
|
||||
/**
|
||||
* Online-/Offline-/Sync-Status war bisher nirgends sichtbar
|
||||
* (useOnlineStatus/Queue existierten nur intern für den Kontroll-Workflow) -
|
||||
* echte Lücke laut Redesign-Vorgabe, kein reiner Umbau. MOBILE-003: zählt
|
||||
* jetzt beide Offline-Queues (Kontrolle + Mangelmeldung) zusammen.
|
||||
* jetzt alle fünf Offline-Queues zusammen (Kontrolle, Mangelmeldung,
|
||||
* Gerätestatus, Wartungsauftrag, Lagerbewegung - Scope 2026-09-13
|
||||
* erweitert). Die neueren vier Queues nutzen `queueStatus` (draftQueue.ts),
|
||||
* die ursprüngliche Kontroll-Queue noch `status` (queue.ts) - beide
|
||||
* Feldnamen hier separat behandelt statt eine gemeinsame Normalisierung
|
||||
* einzuführen, die nur für diese eine Zählung gebraucht würde.
|
||||
*/
|
||||
export function SyncStatus() {
|
||||
const online = useOnlineStatus();
|
||||
@@ -17,8 +25,16 @@ export function SyncStatus() {
|
||||
useEffect(() => {
|
||||
let aktiv = true;
|
||||
async function pruefen() {
|
||||
const [kontrolle, mangel] = await Promise.all([alleEintraege(), alleMangelEintraege()]);
|
||||
const gesamt = [...kontrolle, ...mangel].filter((e) => e.status !== "fehler").length;
|
||||
const [kontrolle, mangel, geraetStatus, wartung, lagerbewegung] = await Promise.all([
|
||||
alleEintraege(),
|
||||
alleMangelEintraege(),
|
||||
alleGeraetStatusEintraege(),
|
||||
alleWartungEintraege(),
|
||||
alleLagerbewegungEintraege(),
|
||||
]);
|
||||
const gesamt =
|
||||
[...kontrolle, ...mangel].filter((e) => e.status !== "fehler").length +
|
||||
[...geraetStatus, ...wartung, ...lagerbewegung].filter((e) => e.queueStatus !== "fehler").length;
|
||||
if (aktiv) setWartend(gesamt);
|
||||
}
|
||||
void pruefen();
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { createStore, del, entries, set } from "idb-keyval";
|
||||
|
||||
import { ApiError } from "../api/client";
|
||||
|
||||
// Geteilte Sync-Mechanik für die per-Feature-Queues (Kontrolle/Mangel/
|
||||
// Gerätestatus/Wartungsauftrag/Lagerbuchung) - bewusst KEIN genereller
|
||||
// "beliebiger Endpunkt"-Layer (das war die 2026-09-10 abgelehnte
|
||||
// Alternative), sondern nur die Wiederholung aus enqueue/sendeEintrag/
|
||||
// synchronisiere zwischen den bereits bestehenden Queues (queue.ts,
|
||||
// mangelQueue.ts) an einer Stelle statt fünffach kopiert. Jede Queue bleibt
|
||||
// eine eigene Datei mit eigenem Item-Typ und eigener sende()-Funktion.
|
||||
export type QueueStatus = "wartet" | "wird_uebertragen" | "fehler";
|
||||
|
||||
export interface DraftItem {
|
||||
key: string;
|
||||
queueStatus: QueueStatus;
|
||||
fehlerText: string | null;
|
||||
}
|
||||
|
||||
export function erzeugeDraftQueue<T extends DraftItem>(storeName: string, sende: (item: T) => Promise<void>) {
|
||||
const store = createStore("mabea-offline-queue", storeName);
|
||||
let synchronisierungLaeuft = false;
|
||||
|
||||
async function alleEintraege(): Promise<T[]> {
|
||||
const all = await entries<string, T>(store);
|
||||
return all.map(([, wert]) => wert);
|
||||
}
|
||||
|
||||
async function speichere(item: T): Promise<void> {
|
||||
await set(item.key, item, store);
|
||||
}
|
||||
|
||||
async function entferneFehlerEintrag(key: string): Promise<void> {
|
||||
await del(key, store);
|
||||
}
|
||||
|
||||
async function sendeEintrag(item: T): Promise<void> {
|
||||
await speichere({ ...item, queueStatus: "wird_uebertragen" });
|
||||
try {
|
||||
await sende(item);
|
||||
await del(item.key, store);
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError && error.status >= 400 && error.status < 500) {
|
||||
await speichere({
|
||||
...item,
|
||||
queueStatus: "fehler",
|
||||
fehlerText: `Server hat abgelehnt (${error.status}) - bitte erneut versuchen.`,
|
||||
});
|
||||
} else {
|
||||
await speichere({ ...item, queueStatus: "wartet", fehlerText: null });
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function synchronisiere(): Promise<void> {
|
||||
if (synchronisierungLaeuft) return;
|
||||
synchronisierungLaeuft = true;
|
||||
try {
|
||||
const eintraege = await alleEintraege();
|
||||
for (const item of eintraege) {
|
||||
if (item.queueStatus === "fehler") continue; // braucht bewusste Nutzer-Aktion
|
||||
try {
|
||||
await sendeEintrag(item);
|
||||
} catch (error) {
|
||||
if (!(error instanceof ApiError)) return; // vermutlich offline - restliche Queue in Ruhe lassen
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
synchronisierungLaeuft = false;
|
||||
}
|
||||
}
|
||||
|
||||
return { store, alleEintraege, speichere, entferneFehlerEintrag, synchronisiere };
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { apiRequest } from "../api/client";
|
||||
import type { GeraetInstanz } from "../api/types";
|
||||
import { erzeugeDraftQueue, type DraftItem } from "./draftQueue";
|
||||
|
||||
// MOBILE-003-Erweiterung (2026-09-13, Nutzer-Entscheidung: bisherige
|
||||
// Scope-Begrenzung auf Kontrolle/Mangel revidiert): Gerätestatus-Änderung
|
||||
// (z.B. Gerät als defekt melden) lief bisher blockierend gegen die API -
|
||||
// dieselbe Draft-first-Idee wie Kontrolle/Mangel, hier für
|
||||
// PATCH /geraete/{id}.
|
||||
export interface GeraetStatusQueueItem extends DraftItem {
|
||||
geraetId: string;
|
||||
neuerStatus: GeraetInstanz["status"];
|
||||
pruefdatum: string | null;
|
||||
bemerkung: string | null;
|
||||
erzeugeFehlbestand: boolean;
|
||||
}
|
||||
|
||||
const queue = erzeugeDraftQueue<GeraetStatusQueueItem>("geraetStatus", async (item) => {
|
||||
await apiRequest(`/geraete/${item.geraetId}`, {
|
||||
method: "PATCH",
|
||||
body: {
|
||||
status: item.neuerStatus,
|
||||
pruefdatum: item.pruefdatum,
|
||||
bemerkung: item.bemerkung,
|
||||
erzeuge_fehlbestand: item.erzeugeFehlbestand,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
export async function enqueue(eingabe: {
|
||||
geraetId: string;
|
||||
neuerStatus: GeraetInstanz["status"];
|
||||
pruefdatum: string | null;
|
||||
bemerkung: string | null;
|
||||
erzeugeFehlbestand: boolean;
|
||||
}): Promise<void> {
|
||||
const key = crypto.randomUUID();
|
||||
await queue.speichere({
|
||||
key,
|
||||
geraetId: eingabe.geraetId,
|
||||
neuerStatus: eingabe.neuerStatus,
|
||||
pruefdatum: eingabe.pruefdatum,
|
||||
bemerkung: eingabe.bemerkung,
|
||||
erzeugeFehlbestand: eingabe.erzeugeFehlbestand,
|
||||
queueStatus: "wartet",
|
||||
fehlerText: null,
|
||||
});
|
||||
}
|
||||
|
||||
export const alleEintraege = queue.alleEintraege;
|
||||
export const entferneFehlerEintrag = queue.entferneFehlerEintrag;
|
||||
export const synchronisiereGeraetStatusQueue = queue.synchronisiere;
|
||||
@@ -0,0 +1,62 @@
|
||||
import { apiRequest } from "../api/client";
|
||||
import { erzeugeDraftQueue, type DraftItem } from "./draftQueue";
|
||||
|
||||
// MOBILE-003-Erweiterung (2026-09-13): Ein-/Auslagerung/Umlagerung liefen
|
||||
// bisher blockierend gegen die jeweiligen POST-Endpunkte. 409 (z.B. "nicht
|
||||
// genug Bestand") landet über den 4xx-Zweig der Draft-Queue korrekt im
|
||||
// "fehler"-Status statt endlos automatisch erneut versucht zu werden.
|
||||
type LagerbewegungAktion = "einlagern" | "auslagern" | "umlagern";
|
||||
|
||||
export interface LagerbewegungQueueItem extends DraftItem {
|
||||
aktion: LagerbewegungAktion;
|
||||
lagerplatzId: number;
|
||||
materialId: number;
|
||||
menge: string;
|
||||
grund: string | null;
|
||||
nachLagerplatzId: number | null; // nur bei "umlagern" gesetzt
|
||||
}
|
||||
|
||||
const queue = erzeugeDraftQueue<LagerbewegungQueueItem>("lagerbewegung", async (item) => {
|
||||
if (item.aktion === "umlagern") {
|
||||
await apiRequest(`/lagerplaetze/${item.lagerplatzId}/umlagern`, {
|
||||
method: "POST",
|
||||
body: {
|
||||
nach_lagerplatz_id: item.nachLagerplatzId,
|
||||
material_id: item.materialId,
|
||||
menge: item.menge,
|
||||
grund: item.grund,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
await apiRequest(`/lagerplaetze/${item.lagerplatzId}/${item.aktion}`, {
|
||||
method: "POST",
|
||||
body: { material_id: item.materialId, menge: item.menge, grund: item.grund },
|
||||
});
|
||||
});
|
||||
|
||||
export async function enqueue(eingabe: {
|
||||
aktion: LagerbewegungAktion;
|
||||
lagerplatzId: number;
|
||||
materialId: number;
|
||||
menge: string;
|
||||
grund?: string | null;
|
||||
nachLagerplatzId?: number | null;
|
||||
}): Promise<void> {
|
||||
const key = crypto.randomUUID();
|
||||
await queue.speichere({
|
||||
key,
|
||||
aktion: eingabe.aktion,
|
||||
lagerplatzId: eingabe.lagerplatzId,
|
||||
materialId: eingabe.materialId,
|
||||
menge: eingabe.menge,
|
||||
grund: eingabe.grund ?? null,
|
||||
nachLagerplatzId: eingabe.nachLagerplatzId ?? null,
|
||||
queueStatus: "wartet",
|
||||
fehlerText: null,
|
||||
});
|
||||
}
|
||||
|
||||
export const alleEintraege = queue.alleEintraege;
|
||||
export const entferneFehlerEintrag = queue.entferneFehlerEintrag;
|
||||
export const synchronisiereLagerbewegungQueue = queue.synchronisiere;
|
||||
@@ -1,14 +1,25 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { synchronisiereQueue } from "./queue";
|
||||
import { synchronisiereGeraetStatusQueue } from "./geraetStatusQueue";
|
||||
import { synchronisiereLagerbewegungQueue } from "./lagerbewegungQueue";
|
||||
import { synchronisiereMangelQueue } from "./mangelQueue";
|
||||
import { synchronisiereQueue } from "./queue";
|
||||
import { synchronisiereWartungQueue } from "./wartungQueue";
|
||||
|
||||
// MOBILE-003: zentraler Reconnect-Trigger für ALLE Offline-Queues der App
|
||||
// (Kontroll-Erfassung + Mangelmeldung) - läuft global über SyncStatus in
|
||||
// AppShell, nicht nur auf der jeweiligen Seite, damit auch ein Seitenwechsel
|
||||
// während des Wartens auf Verbindung die Übertragung nicht verhindert.
|
||||
// (Kontroll-Erfassung, Mangelmeldung, Gerätestatus, Wartungsauftrag,
|
||||
// Lagerbewegung - Scope 2026-09-13 erweitert) - läuft global über SyncStatus
|
||||
// in AppShell, nicht nur auf der jeweiligen Seite, damit auch ein
|
||||
// Seitenwechsel während des Wartens auf Verbindung die Übertragung nicht
|
||||
// verhindert.
|
||||
async function synchronisiereAlle(): Promise<void> {
|
||||
await Promise.all([synchronisiereQueue(), synchronisiereMangelQueue()]);
|
||||
await Promise.all([
|
||||
synchronisiereQueue(),
|
||||
synchronisiereMangelQueue(),
|
||||
synchronisiereGeraetStatusQueue(),
|
||||
synchronisiereWartungQueue(),
|
||||
synchronisiereLagerbewegungQueue(),
|
||||
]);
|
||||
}
|
||||
|
||||
/** Prompt 17: bei wiederhergestellter Verbindung automatische Übertragung der
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { apiRequest } from "../api/client";
|
||||
import { erzeugeDraftQueue, type DraftItem } from "./draftQueue";
|
||||
|
||||
// MOBILE-003-Erweiterung (2026-09-13): Wartungsauftrag anlegen lief bisher
|
||||
// blockierend gegen POST /wartungsauftraege.
|
||||
export interface WartungQueueItem extends DraftItem {
|
||||
objektId: number;
|
||||
wartungsart: string;
|
||||
werkstattIntern: boolean;
|
||||
werkstattName: string | null;
|
||||
dienstleisterId: number | null;
|
||||
}
|
||||
|
||||
const queue = erzeugeDraftQueue<WartungQueueItem>("wartung", async (item) => {
|
||||
await apiRequest("/wartungsauftraege", {
|
||||
method: "POST",
|
||||
body: {
|
||||
objekt_id: item.objektId,
|
||||
wartungsart: item.wartungsart,
|
||||
werkstatt_intern: item.werkstattIntern,
|
||||
werkstatt_name: item.werkstattName,
|
||||
dienstleister_id: item.dienstleisterId,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
export async function enqueue(eingabe: {
|
||||
objektId: number;
|
||||
wartungsart: string;
|
||||
werkstattIntern: boolean;
|
||||
werkstattName: string | null;
|
||||
dienstleisterId: number | null;
|
||||
}): Promise<void> {
|
||||
const key = crypto.randomUUID();
|
||||
await queue.speichere({
|
||||
key,
|
||||
objektId: eingabe.objektId,
|
||||
wartungsart: eingabe.wartungsart,
|
||||
werkstattIntern: eingabe.werkstattIntern,
|
||||
werkstattName: eingabe.werkstattName,
|
||||
dienstleisterId: eingabe.dienstleisterId,
|
||||
queueStatus: "wartet",
|
||||
fehlerText: null,
|
||||
});
|
||||
}
|
||||
|
||||
export const alleEintraege = queue.alleEintraege;
|
||||
export const entferneFehlerEintrag = queue.entferneFehlerEintrag;
|
||||
export const synchronisiereWartungQueue = queue.synchronisiere;
|
||||
@@ -4,6 +4,7 @@ import { apiRequest } from "../../api/client";
|
||||
import type { GeraetInstanz, Pruefung, PruefungsAmpel } from "../../api/types";
|
||||
import { CustodyDialog } from "../../components/CustodyDialog";
|
||||
import { FristBadge } from "../../components/status/FristBadge";
|
||||
import * as geraetStatusQueue from "../../offline/geraetStatusQueue";
|
||||
|
||||
const ERGEBNIS_LABEL: Record<Pruefung["ergebnis"], string> = {
|
||||
bestanden: "Bestanden",
|
||||
@@ -119,17 +120,20 @@ export function GeraeteInstanzenListe({ objektpositionId, pruefintervallMonate,
|
||||
}
|
||||
|
||||
async function speichernStatus(instanz: GeraetInstanz) {
|
||||
// MOBILE-003-Erweiterung: läuft über die Offline-Queue statt blockierend
|
||||
// gegen die API - bei bestehender Verbindung praktisch sofort (Sync-
|
||||
// Versuch läuft direkt im Anschluss), bei Verbindungsverlust bleibt die
|
||||
// Änderung erhalten statt einfach zu scheitern.
|
||||
try {
|
||||
await apiRequest(`/geraete/${instanz.id}`, {
|
||||
method: "PATCH",
|
||||
body: {
|
||||
status: statusAuswahl,
|
||||
pruefdatum: pruefdatum || null,
|
||||
bemerkung: bemerkung || null,
|
||||
erzeuge_fehlbestand: statusAuswahl !== "einsatzbereit" && erzeugeFehlbestand,
|
||||
},
|
||||
await geraetStatusQueue.enqueue({
|
||||
geraetId: instanz.id,
|
||||
neuerStatus: statusAuswahl,
|
||||
pruefdatum: pruefdatum || null,
|
||||
bemerkung: bemerkung || null,
|
||||
erzeugeFehlbestand: statusAuswahl !== "einsatzbereit" && erzeugeFehlbestand,
|
||||
});
|
||||
setBearbeiteId(null);
|
||||
await geraetStatusQueue.synchronisiereGeraetStatusQueue();
|
||||
await laden();
|
||||
} catch {
|
||||
onFehler("Status konnte nicht geändert werden.");
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { apiRequest } from "../../api/client";
|
||||
import { apiRequest, ApiError } from "../../api/client";
|
||||
import type { Bestand, Lagerort, LagerortTyp, Lagerplatz, Material, Materialbewegung } from "../../api/types";
|
||||
import * as lagerbewegungQueue from "../../offline/lagerbewegungQueue";
|
||||
|
||||
interface Props {
|
||||
materialien: Material[];
|
||||
@@ -126,6 +127,12 @@ export function LagerSection({ materialien, onFehler }: Props) {
|
||||
}
|
||||
}
|
||||
|
||||
// MOBILE-003-Erweiterung: anders als Gerätestatus/Wartungsauftrag NICHT
|
||||
// "immer zuerst in die Queue" - eine Buchung hat echte Geschäftskonflikte
|
||||
// (Bestand reicht nicht), die der Nutzer SOFORT sehen soll, wenn eine
|
||||
// Verbindung besteht, statt sie erst beim späteren Sync-Versuch zu
|
||||
// erfahren. Nur ein echter Netzwerkfehler (kein ApiError) landet in der
|
||||
// Offline-Queue, ein 4xx-Konflikt wird weiterhin direkt angezeigt.
|
||||
async function buchen(richtung: "einlagern" | "auslagern") {
|
||||
if (ausgewaehlterPlatz === "" || buchungMaterial === "" || !buchungMenge) {
|
||||
onFehler("Material und Menge angeben.");
|
||||
@@ -139,8 +146,21 @@ export function LagerSection({ materialien, onFehler }: Props) {
|
||||
setBuchungMenge("");
|
||||
setBuchungGrund("");
|
||||
await ladeBestandUndBewegungen(ausgewaehlterPlatz);
|
||||
} catch {
|
||||
onFehler(richtung === "auslagern" ? "Nicht genug Bestand am Lagerplatz." : "Buchung fehlgeschlagen.");
|
||||
} catch (fehler) {
|
||||
if (fehler instanceof ApiError) {
|
||||
onFehler(richtung === "auslagern" ? "Nicht genug Bestand am Lagerplatz." : "Buchung fehlgeschlagen.");
|
||||
return;
|
||||
}
|
||||
await lagerbewegungQueue.enqueue({
|
||||
aktion: richtung,
|
||||
lagerplatzId: ausgewaehlterPlatz,
|
||||
materialId: buchungMaterial,
|
||||
menge: buchungMenge,
|
||||
grund: buchungGrund || null,
|
||||
});
|
||||
setBuchungMenge("");
|
||||
setBuchungGrund("");
|
||||
onFehler("Keine Verbindung - Buchung wird automatisch übertragen, sobald wieder online.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,8 +183,23 @@ export function LagerSection({ materialien, onFehler }: Props) {
|
||||
setBuchungGrund("");
|
||||
setUmlagernZiel("");
|
||||
await ladeBestandUndBewegungen(ausgewaehlterPlatz);
|
||||
} catch {
|
||||
onFehler("Umlagerung fehlgeschlagen (nicht genug Bestand oder Ziel = Quelle).");
|
||||
} catch (fehler) {
|
||||
if (fehler instanceof ApiError) {
|
||||
onFehler("Umlagerung fehlgeschlagen (nicht genug Bestand oder Ziel = Quelle).");
|
||||
return;
|
||||
}
|
||||
await lagerbewegungQueue.enqueue({
|
||||
aktion: "umlagern",
|
||||
lagerplatzId: ausgewaehlterPlatz,
|
||||
nachLagerplatzId: umlagernZiel,
|
||||
materialId: buchungMaterial,
|
||||
menge: buchungMenge,
|
||||
grund: buchungGrund || null,
|
||||
});
|
||||
setBuchungMenge("");
|
||||
setBuchungGrund("");
|
||||
setUmlagernZiel("");
|
||||
onFehler("Keine Verbindung - Umlagerung wird automatisch übertragen, sobald wieder online.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user