CI / backend-tests (push) Successful in 54s
React/Vite-PWA (frontend/):
- Login, Objektliste (Prompt 11 Screen 1), Kontroll-Screen (Screens 2-6 kompakt:
Start/Objekt-Sperre-Übernahme, Positionserfassung, Abschluss/Abbruch)
- Offline-Härtung (Prompt 17): IndexedDB-Queue (src/offline/queue.ts) puffert
Positions-PUTs lokal, automatische Übertragung bei Reconnect + periodischem
Sync-Versuch, Statusanzeige je Position (nicht gespeichert/wird übertragen/
gespeichert/Fehler), Abschluss-Button bleibt gesperrt bis alles gespeichert
- Nutzt aus, dass PUT /kontrollen/{id}/positionen/{material_id} backend-seitig
idempotent ist (Upsert) - Queue kann beliebig oft retryen ohne Duplikate
- Service Worker via vite-plugin-pwa für App-Shell-Caching (Start im Feld ohne
Cold-Load); PWA-Icons als TODO vermerkt (noch keine echten Bilddateien)
- Client-Fehler (4xx) werden nicht automatisch wiederholt, nur Netzwerkfehler
Flutter-Android-Grundgerüst (flutter_app/), auf Nutzerwunsch parallel begonnen:
- Gleiche API als zweiter Client (Prompt 19 API-first), Login/Objektliste/
Kontroll-Screen als Dart-Äquivalent zur PWA
- Bewusst OHNE Offline-Queue in dieser ersten Fassung (siehe flutter_app/README.md)
- Plattform-Ordner (android/, ios/) nicht von Hand erzeugt - müssen auf dem
Zielsystem per `flutter create .` nachgezogen werden, sonst zu fehleranfällig
ohne Testlauf
Offen: Playwright-E2E (Testphase 4), Praktiker-Session, PWA-Icons, Flutter-
Offline-Queue - brauchen laufendes Deployment bzw. sind kein reiner Code-Schritt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L85hmKbvX7Cqkq47KnQhFt
115 lines
4.4 KiB
TypeScript
115 lines
4.4 KiB
TypeScript
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<void> {
|
|
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<QueueItem[]> {
|
|
const all = await entries<string, QueueItem>(store);
|
|
return all.map(([, value]) => value);
|
|
}
|
|
|
|
export async function eintraegeFuerKontrolle(kontrolleId: string): Promise<QueueItem[]> {
|
|
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<boolean> {
|
|
const eintraege = await eintraegeFuerKontrolle(kontrolleId);
|
|
return eintraege.length > 0;
|
|
}
|
|
|
|
async function sendeEintrag(item: QueueItem): Promise<void> {
|
|
await set(item.key, { ...item, status: "wird_uebertragen" as QueueStatus }, store);
|
|
try {
|
|
const ergebnis = await apiRequest<Kontrollposition>(
|
|
`/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<void> {
|
|
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<void> {
|
|
await del(keyFor(kontrolleId, materialId), store);
|
|
}
|