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
76 lines
2.3 KiB
Dart
76 lines
2.3 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import '../api/api_client.dart';
|
|
import 'objekt_list_screen.dart';
|
|
|
|
class LoginScreen extends StatefulWidget {
|
|
const LoginScreen({super.key, required this.apiClient});
|
|
final ApiClient apiClient;
|
|
|
|
@override
|
|
State<LoginScreen> createState() => _LoginScreenState();
|
|
}
|
|
|
|
class _LoginScreenState extends State<LoginScreen> {
|
|
final _loginController = TextEditingController();
|
|
final _passwortController = TextEditingController();
|
|
String? _fehler;
|
|
bool _ladeVorgang = false;
|
|
|
|
Future<void> _anmelden() async {
|
|
setState(() {
|
|
_fehler = null;
|
|
_ladeVorgang = true;
|
|
});
|
|
try {
|
|
await widget.apiClient.login(_loginController.text, _passwortController.text);
|
|
if (!mounted) return;
|
|
Navigator.of(context).pushReplacement(
|
|
MaterialPageRoute(builder: (_) => ObjektListScreen(apiClient: widget.apiClient)),
|
|
);
|
|
} catch (_) {
|
|
setState(() => _fehler = 'Login oder Passwort falsch');
|
|
} finally {
|
|
if (mounted) setState(() => _ladeVorgang = false);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
body: Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(24),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
const Text('MABEA', style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold)),
|
|
const SizedBox(height: 24),
|
|
TextField(
|
|
controller: _loginController,
|
|
decoration: const InputDecoration(labelText: 'Login'),
|
|
),
|
|
const SizedBox(height: 12),
|
|
TextField(
|
|
controller: _passwortController,
|
|
decoration: const InputDecoration(labelText: 'Passwort'),
|
|
obscureText: true,
|
|
),
|
|
if (_fehler != null) ...[
|
|
const SizedBox(height: 12),
|
|
Text(_fehler!, style: const TextStyle(color: Colors.red)),
|
|
],
|
|
const SizedBox(height: 24),
|
|
FilledButton(
|
|
onPressed: _ladeVorgang ? null : _anmelden,
|
|
child: Text(_ladeVorgang ? 'Wird geprüft…' : 'Anmelden'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|