Files
MABEA/flutter_app/lib/screens/objekt_list_screen.dart
T
patrickandClaude Sonnet 5 d8d2816758
CI / backend-tests (push) Successful in 1m1s
CI / frontend-build (push) Successful in 21s
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
2026-09-04 17:57:18 +02:00

130 lines
3.9 KiB
Dart

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. 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;
@override
State<ObjektListScreen> createState() => _ObjektListScreenState();
}
class _ObjektListScreenState extends State<ObjektListScreen> {
List<dynamic> _objekte = [];
String _suche = '';
String? _fehler;
final _sucheController = TextEditingController();
@override
void initState() {
super.initState();
_laden();
}
@override
void dispose() {
_sucheController.dispose();
super.dispose();
}
Future<void> _laden() async {
try {
final ergebnis = await widget.apiClient.request('/objekte');
setState(() => _objekte = ergebnis as List<dynamic>);
} catch (_) {
setState(() => _fehler = 'Objektliste konnte nicht geladen werden.');
}
}
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) {
final name = (o['name'] as String).toLowerCase();
final code = (o['code'] as String).toLowerCase();
final suche = _suche.toLowerCase();
return name.contains(suche) || code.contains(suche);
}).toList();
return Scaffold(
appBar: AppBar(title: const Text('Objekte')),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(12),
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)),
Expanded(
child: ListView.builder(
itemCount: gefiltert.length,
itemBuilder: (context, index) {
final objekt = gefiltert[index];
return ListTile(
title: Text(objekt['name'] as String),
subtitle: Text(objekt['code'] as String),
onTap: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => KontrollScreen(
apiClient: widget.apiClient,
objektId: objekt['id'] as int,
),
),
),
);
},
),
),
],
),
);
}
}