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,101 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
/// Gleiche Basis-Idee wie im React-Client (frontend/src/api/client.ts):
|
||||
/// gemeinsamer JSON-Wrapper, Token aus persistentem Speicher, ApiException
|
||||
/// mit HTTP-Status, damit aufrufender Code gezielt reagieren kann (z.B. 409
|
||||
/// bei Objekt-Sperre).
|
||||
class ApiException implements Exception {
|
||||
final int status;
|
||||
final dynamic detail;
|
||||
ApiException(this.status, this.detail);
|
||||
|
||||
@override
|
||||
String toString() => 'ApiException($status, $detail)';
|
||||
}
|
||||
|
||||
class ApiClient {
|
||||
ApiClient({required this.baseUrl});
|
||||
|
||||
final String baseUrl;
|
||||
String? _token;
|
||||
|
||||
Future<void> ladeToken() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
_token = prefs.getString('mabea_token');
|
||||
}
|
||||
|
||||
Future<void> setzeToken(String? token) async {
|
||||
_token = token;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
if (token == null) {
|
||||
await prefs.remove('mabea_token');
|
||||
} else {
|
||||
await prefs.setString('mabea_token', token);
|
||||
}
|
||||
}
|
||||
|
||||
bool get istEingeloggt => _token != null;
|
||||
|
||||
Future<String> login(String username, String password) async {
|
||||
final response = await http.post(
|
||||
Uri.parse('$baseUrl/auth/login'),
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
body: {'username': username, 'password': password},
|
||||
);
|
||||
if (response.statusCode != 200) {
|
||||
throw ApiException(response.statusCode, _tryDecode(response.body));
|
||||
}
|
||||
final data = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
final token = data['access_token'] as String;
|
||||
await setzeToken(token);
|
||||
return token;
|
||||
}
|
||||
|
||||
Future<dynamic> request(
|
||||
String path, {
|
||||
String method = 'GET',
|
||||
Object? body,
|
||||
}) async {
|
||||
final uri = Uri.parse('$baseUrl$path');
|
||||
final headers = {
|
||||
'Content-Type': 'application/json',
|
||||
if (_token != null) 'Authorization': 'Bearer $_token',
|
||||
};
|
||||
final encodedBody = body != null ? jsonEncode(body) : null;
|
||||
|
||||
late http.Response response;
|
||||
switch (method) {
|
||||
case 'POST':
|
||||
response = await http.post(uri, headers: headers, body: encodedBody);
|
||||
break;
|
||||
case 'PUT':
|
||||
response = await http.put(uri, headers: headers, body: encodedBody);
|
||||
break;
|
||||
case 'PATCH':
|
||||
response = await http.patch(uri, headers: headers, body: encodedBody);
|
||||
break;
|
||||
case 'DELETE':
|
||||
response = await http.delete(uri, headers: headers);
|
||||
break;
|
||||
default:
|
||||
response = await http.get(uri, headers: headers);
|
||||
}
|
||||
|
||||
if (response.statusCode >= 400) {
|
||||
throw ApiException(response.statusCode, _tryDecode(response.body));
|
||||
}
|
||||
if (response.body.isEmpty) return null;
|
||||
return jsonDecode(response.body);
|
||||
}
|
||||
|
||||
dynamic _tryDecode(String body) {
|
||||
try {
|
||||
return jsonDecode(body);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user