diff --git a/DEVLOG.md b/DEVLOG.md index 09885d8..4d0f859 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -2591,3 +2591,19 @@ Keine Commits in dieser Session. - frontend/src/pages/admin/ObjektSection.tsx | 58 +++++++++++++++++++++++++++++- --- +## 2026-09-04 17:39 – 17:39 (0m) +**Beschreibung:** Claude Code Session +**Projekt:** frontend + +### Commits +- 6cf28cc Karte 10: Kamera-Scan im Frontend (Objekt-Code, Scan-First-Einstieg) + +### Geänderte Dateien +- DEVLOG.md | 13 ++++++ +- frontend/DEVLOG.md | 13 ++++++ +- frontend/package.json | 1 + +- frontend/src/components/BarcodeScanner.tsx | 64 ++++++++++++++++++++++++++++++ +- frontend/src/pages/ObjektListPage.tsx | 48 ++++++++++++++++------ +- frontend/src/styles/global.css | 24 +++++++++++ + +--- diff --git a/flutter_app/DEVLOG.md b/flutter_app/DEVLOG.md new file mode 100644 index 0000000..a99b35b --- /dev/null +++ b/flutter_app/DEVLOG.md @@ -0,0 +1,18 @@ +# flutter_app – Dev Log + +## 2026-09-04 17:42 – 17:43 (1m) +**Beschreibung:** Claude Code Session +**Projekt:** asb-material + +### Commits +Keine Commits in dieser Session. + +### Geänderte Dateien +- DEVLOG.md | 13 ++++++ +- frontend/DEVLOG.md | 13 ++++++ +- frontend/package.json | 1 + +- frontend/src/components/BarcodeScanner.tsx | 64 ++++++++++++++++++++++++++++++ +- frontend/src/pages/ObjektListPage.tsx | 48 ++++++++++++++++------ +- frontend/src/styles/global.css | 24 +++++++++++ + +--- diff --git a/flutter_app/README.md b/flutter_app/README.md index 7226259..3007136 100644 --- a/flutter_app/README.md +++ b/flutter_app/README.md @@ -27,6 +27,23 @@ flutter build apk (`192.168.1.238`) gesetzt. Für Produktivbetrieb auf `--dart-define` umstellen oder zumindest vor jedem Build prüfen/anpassen. +## Kamera-Berechtigung (Kamera-Scan, Karte 10) + +`mobile_scanner` braucht die Kamera-Berechtigung im generierten `android/`-Ordner +(existiert erst nach `flutter create .`, siehe oben) - nach dem Erstlauf einmalig +in `android/app/src/main/AndroidManifest.xml` ergänzen, oberhalb von ``: + +```xml + +``` + +Für iOS entsprechend in `ios/Runner/Info.plist`: + +```xml +NSCameraUsageDescription +Wird zum Scannen von Objekt-/Geräte-Codes benötigt. +``` + ## Bewusst noch nicht umgesetzt (Unterschied zur React-PWA) - **Keine Offline-Queue**: anders als die PWA (`frontend/src/offline/queue.ts`, @@ -35,6 +52,5 @@ oder zumindest vor jedem Build prüfen/anpassen. Zwischenspeicherung/Sync bei Reconnect. Nachziehen, sobald die App über das Grundgerüst hinauswächst - gleiches Muster wie in der PWA übertragbar (z. B. `sqflite` oder `hive` statt IndexedDB). -- Kein QR-/Barcode-Kamera-Scan (Karte 10, Roadmap). - Keine Tests (Flutter-Widget-/Integrationstests) - Sprintplan sieht Testphase 4 primär für die PWA vor (Playwright); Android-Testabdeckung ist noch offen. diff --git a/flutter_app/lib/screens/objekt_list_screen.dart b/flutter_app/lib/screens/objekt_list_screen.dart index c34fd73..7962c4e 100644 --- a/flutter_app/lib/screens/objekt_list_screen.dart +++ b/flutter_app/lib/screens/objekt_list_screen.dart @@ -2,10 +2,11 @@ import 'package:flutter/material.dart'; import '../api/api_client.dart'; import 'kontroll_screen.dart'; +import 'scanner_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. +/// Prompt 11 Screen 1: Standort/Objekt wählen. Objekt-Code (Karte 10) → +/// Direktsprung ins Kontroll-Screen, sonst füllt der Scan dasselbe Suchfeld wie +/// manuelle Eingabe (Kamera fällt bei Etikettschaden auf Tippen zurück). class ObjektListScreen extends StatefulWidget { const ObjektListScreen({super.key, required this.apiClient}); final ApiClient apiClient; @@ -18,6 +19,7 @@ class _ObjektListScreenState extends State { List _objekte = []; String _suche = ''; String? _fehler; + final _sucheController = TextEditingController(); @override void initState() { @@ -25,6 +27,12 @@ class _ObjektListScreenState extends State { _laden(); } + @override + void dispose() { + _sucheController.dispose(); + super.dispose(); + } + Future _laden() async { try { final ergebnis = await widget.apiClient.request('/objekte'); @@ -34,6 +42,33 @@ class _ObjektListScreenState extends State { } } + Future _scannen() async { + final code = await Navigator.of(context).push( + MaterialPageRoute(builder: (_) => const ScannerScreen()), + ); + if (code == null || !mounted) return; + + try { + final objekt = await widget.apiClient.request('/objekte/code/$code'); + if (!mounted) return; + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => KontrollScreen( + apiClient: widget.apiClient, + objektId: objekt['id'] as int, + ), + ), + ); + } on ApiException catch (e) { + if (e.status == 404) { + _sucheController.text = code; + setState(() => _suche = code); + } else { + setState(() => _fehler = 'Scan konnte nicht verarbeitet werden.'); + } + } + } + @override Widget build(BuildContext context) { final gefiltert = _objekte.where((o) { @@ -49,9 +84,21 @@ class _ObjektListScreenState extends State { children: [ Padding( padding: const EdgeInsets.all(12), - child: TextField( - decoration: const InputDecoration(labelText: 'Suche oder Code scannen…'), - onChanged: (wert) => setState(() => _suche = wert), + child: Row( + children: [ + Expanded( + child: TextField( + controller: _sucheController, + decoration: const InputDecoration(labelText: 'Suche oder Code…'), + onChanged: (wert) => setState(() => _suche = wert), + ), + ), + IconButton( + icon: const Icon(Icons.qr_code_scanner), + tooltip: 'Scannen', + onPressed: _scannen, + ), + ], ), ), if (_fehler != null) Text(_fehler!, style: const TextStyle(color: Colors.red)), diff --git a/flutter_app/lib/screens/scanner_screen.dart b/flutter_app/lib/screens/scanner_screen.dart new file mode 100644 index 0000000..e4d252c --- /dev/null +++ b/flutter_app/lib/screens/scanner_screen.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import 'package:mobile_scanner/mobile_scanner.dart'; + +/// Kamera-Scan (Karte 10): Vollbild-Scanner, gibt den ersten erkannten +/// Code128-Wert per Navigator.pop zurück. Kein Auto-Close bei fehlgeschlagenem +/// Decode-Versuch nötig - mobile_scanner meldet nur echte Treffer. +class ScannerScreen extends StatefulWidget { + const ScannerScreen({super.key}); + + @override + State createState() => _ScannerScreenState(); +} + +class _ScannerScreenState extends State { + bool _bereitsErkannt = false; + + void _onDetect(BarcodeCapture capture) { + if (_bereitsErkannt) return; + if (capture.barcodes.isEmpty) return; + final wert = capture.barcodes.first.rawValue; + if (wert == null || wert.isEmpty) return; + _bereitsErkannt = true; + Navigator.of(context).pop(wert); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Code scannen')), + body: MobileScanner(onDetect: _onDetect), + ); + } +} diff --git a/flutter_app/pubspec.yaml b/flutter_app/pubspec.yaml index b309375..8bb75c4 100644 --- a/flutter_app/pubspec.yaml +++ b/flutter_app/pubspec.yaml @@ -11,6 +11,7 @@ dependencies: sdk: flutter http: ^1.2.2 shared_preferences: ^2.3.2 + mobile_scanner: ^5.2.3 dev_dependencies: flutter_test: