Karte 10: Kamera-Scan in Flutter-App (Objekt-Code, Scan-First-Einstieg)
CI / backend-tests (push) Successful in 1m1s
CI / frontend-build (push) Successful in 21s

ScannerScreen auf mobile_scanner, Treffer springt direkt ins KontrollScreen,
sonst Fallback ins bestehende Suchfeld. Kamera-Berechtigung (Android Manifest/
iOS Info.plist) im README dokumentiert, da android/ios-Ordner generiert und
nicht im Repo sind.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
This commit is contained in:
2026-09-04 17:57:18 +02:00
co-authored by Claude Sonnet 5
parent 6cf28cc8ee
commit d8d2816758
6 changed files with 138 additions and 7 deletions
+18
View File
@@ -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 +++++++++++
---
+17 -1
View File
@@ -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 `<application>`:
```xml
<uses-permission android:name="android.permission.CAMERA" />
```
Für iOS entsprechend in `ios/Runner/Info.plist`:
```xml
<key>NSCameraUsageDescription</key>
<string>Wird zum Scannen von Objekt-/Geräte-Codes benötigt.</string>
```
## 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.
@@ -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<ObjektListScreen> {
List<dynamic> _objekte = [];
String _suche = '';
String? _fehler;
final _sucheController = TextEditingController();
@override
void initState() {
@@ -25,6 +27,12 @@ class _ObjektListScreenState extends State<ObjektListScreen> {
_laden();
}
@override
void dispose() {
_sucheController.dispose();
super.dispose();
}
Future<void> _laden() async {
try {
final ergebnis = await widget.apiClient.request('/objekte');
@@ -34,6 +42,33 @@ class _ObjektListScreenState extends State<ObjektListScreen> {
}
}
Future<void> _scannen() async {
final code = await Navigator.of(context).push<String>(
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<ObjektListScreen> {
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)),
@@ -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<ScannerScreen> createState() => _ScannerScreenState();
}
class _ScannerScreenState extends State<ScannerScreen> {
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),
);
}
}
+1
View File
@@ -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: