Sprint 7: React-PWA-Frontend (Offline-Härtung) + Flutter-Android-Grundgerüst
CI / backend-tests (push) Successful in 54s
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:
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user