Sprint 7: React-PWA-Frontend (Offline-Härtung) + Flutter-Android-Grundgerüst
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
This commit is contained in:
2026-09-04 00:10:26 +02:00
co-authored by Claude Sonnet 5
parent aba991bbe2
commit 564e98e5c6
28 changed files with 1624 additions and 0 deletions
+79
View File
@@ -0,0 +1,79 @@
const BASE_URL = import.meta.env.VITE_API_BASE_URL ?? "/api/v1";
let authToken: string | null = localStorage.getItem("mabea_token");
export function setAuthToken(token: string | null): void {
authToken = token;
if (token) {
localStorage.setItem("mabea_token", token);
} else {
localStorage.removeItem("mabea_token");
}
}
export function getAuthToken(): string | null {
return authToken;
}
export class ApiError extends Error {
status: number;
detail: unknown;
constructor(status: number, detail: unknown) {
super(`API-Fehler ${status}`);
this.status = status;
this.detail = detail;
}
}
interface RequestOptions {
method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
body?: unknown;
}
/**
* Generischer JSON-Client. Wirft ApiError bei Nicht-2xx-Antworten, damit
* aufrufender Code (insb. die Offline-Queue) gezielt auf HTTP-Status reagieren
* kann (z. B. 409 bei Objekt-Sperre nicht wie ein Netzwerkfehler behandeln).
*/
export async function apiRequest<T>(path: string, options: RequestOptions = {}): Promise<T> {
const headers: Record<string, string> = { "Content-Type": "application/json" };
if (authToken) {
headers["Authorization"] = `Bearer ${authToken}`;
}
const response = await fetch(`${BASE_URL}${path}`, {
method: options.method ?? "GET",
headers,
body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
});
if (!response.ok) {
let detail: unknown = null;
try {
detail = await response.json();
} catch {
// Antwort ohne JSON-Body (z. B. 204) - detail bleibt null.
}
throw new ApiError(response.status, detail);
}
if (response.status === 204) {
return undefined as T;
}
return (await response.json()) as T;
}
export async function login(username: string, password: string): Promise<string> {
const body = new URLSearchParams({ username, password });
const response = await fetch(`${BASE_URL}/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body,
});
if (!response.ok) {
throw new ApiError(response.status, await response.json().catch(() => null));
}
const data = (await response.json()) as { access_token: string };
return data.access_token;
}
+49
View File
@@ -0,0 +1,49 @@
export interface Objekt {
id: number;
code: string;
name: string;
objekttyp_id: number;
vorlage_id: number | null;
standort_id: number;
status: string;
}
export interface Objektposition {
id: string;
objekt_id: number;
material_id: number;
sollmenge_override: string | null;
sollmenge_effektiv: string;
ist_status: string;
istmenge: string;
seriennummer: string | null;
ablaufdatum: string | null;
chargennummer: string | null;
}
export interface Kontrolle {
id: string;
objekt_id: number;
benutzer_id: number;
status: "nicht_gestartet" | "in_bearbeitung" | "abgeschlossen" | "abgebrochen";
gestartet_am: string;
beendet_am: string | null;
abbruch_grund: string | null;
}
export interface Kontrollposition {
id: string;
kontrolle_id: string;
material_id: number;
sollmenge_snapshot: string;
istmenge_erfasst: string;
abweichung: boolean;
fehlbestand_id: string | null;
}
export interface Material {
id: number;
name: string;
einheit: string;
materialtyp: string;
}