Karte 10: Kamera-Scan in Flutter-App (Objekt-Code, Scan-First-Einstieg)
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:
@@ -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 +++++++++++
|
||||
|
||||
---
|
||||
|
||||
@@ -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
@@ -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,11 +84,23 @@ class _ObjektListScreenState extends State<ObjektListScreen> {
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
decoration: const InputDecoration(labelText: 'Suche oder Code scannen…'),
|
||||
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)),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user