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
@@ -0,0 +1,82 @@
import 'package:flutter/material.dart';
import '../api/api_client.dart';
import 'kontroll_screen.dart';
/// Prompt 11 Screen 1: Standort/Objekt wählen. Kamera-Scan (Karte 10) ist Roadmap -
/// das Suchfeld nimmt schon jetzt einen gescannten Code entgegen, sobald ein
/// Scanner-Plugin ergänzt wird.
class ObjektListScreen extends StatefulWidget {
const ObjektListScreen({super.key, required this.apiClient});
final ApiClient apiClient;
@override
State<ObjektListScreen> createState() => _ObjektListScreenState();
}
class _ObjektListScreenState extends State<ObjektListScreen> {
List<dynamic> _objekte = [];
String _suche = '';
String? _fehler;
@override
void initState() {
super.initState();
_laden();
}
Future<void> _laden() async {
try {
final ergebnis = await widget.apiClient.request('/objekte');
setState(() => _objekte = ergebnis as List<dynamic>);
} catch (_) {
setState(() => _fehler = 'Objektliste konnte nicht geladen werden.');
}
}
@override
Widget build(BuildContext context) {
final gefiltert = _objekte.where((o) {
final name = (o['name'] as String).toLowerCase();
final code = (o['code'] as String).toLowerCase();
final suche = _suche.toLowerCase();
return name.contains(suche) || code.contains(suche);
}).toList();
return Scaffold(
appBar: AppBar(title: const Text('Objekte')),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(12),
child: TextField(
decoration: const InputDecoration(labelText: 'Suche oder Code scannen…'),
onChanged: (wert) => setState(() => _suche = wert),
),
),
if (_fehler != null) Text(_fehler!, style: const TextStyle(color: Colors.red)),
Expanded(
child: ListView.builder(
itemCount: gefiltert.length,
itemBuilder: (context, index) {
final objekt = gefiltert[index];
return ListTile(
title: Text(objekt['name'] as String),
subtitle: Text(objekt['code'] as String),
onTap: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => KontrollScreen(
apiClient: widget.apiClient,
objektId: objekt['id'] as int,
),
),
),
);
},
),
),
],
),
);
}
}