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
+59
View File
@@ -0,0 +1,59 @@
import { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import { apiRequest } from "../api/client";
import type { Objekt } from "../api/types";
// Prompt 11 Screen 1: Standort/Objekt wählen. QR-/Barcode-Scan (Karte 10) ist
// Roadmap - dieser Screen bietet bewusst schon die Code-Suche als Textfeld an,
// ein späterer Kamera-Scan kann denselben Code einfach in dasselbe Feld schreiben.
export function ObjektListPage() {
const [objekte, setObjekte] = useState<Objekt[]>([]);
const [suche, setSuche] = useState("");
const [ladefehler, setLadefehler] = useState<string | null>(null);
useEffect(() => {
apiRequest<Objekt[]>("/objekte")
.then(setObjekte)
.catch(() => setLadefehler("Objektliste konnte nicht geladen werden."));
}, []);
const gefiltert = objekte.filter(
(o) =>
o.name.toLowerCase().includes(suche.toLowerCase()) ||
o.code.toLowerCase().includes(suche.toLowerCase())
);
return (
<main style={{ maxWidth: 480, margin: "0 auto", padding: "1rem" }}>
<h1>Objekte</h1>
<input
placeholder="Suche oder Code scannen…"
value={suche}
onChange={(e) => setSuche(e.target.value)}
style={{ width: "100%", fontSize: "1.2rem", padding: "0.75rem", marginBottom: "1rem" }}
/>
{ladefehler && <p style={{ color: "crimson" }}>{ladefehler}</p>}
<ul style={{ listStyle: "none", padding: 0 }}>
{gefiltert.map((objekt) => (
<li key={objekt.id} style={{ marginBottom: "0.5rem" }}>
<Link
to={`/objekte/${objekt.id}/kontrolle`}
style={{
display: "block",
padding: "1rem",
border: "1px solid #ccc",
borderRadius: 8,
textDecoration: "none",
color: "inherit",
}}
>
<strong>{objekt.name}</strong>
<div style={{ fontSize: "0.9rem", color: "#666" }}>{objekt.code}</div>
</Link>
</li>
))}
</ul>
</main>
);
}