import 'dart:async'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:hl_lieferservice/feature/delivery/overview/bloc/tour_event.dart'; import 'package:hl_lieferservice/feature/delivery/overview/bloc/tour_state.dart'; import 'package:hl_lieferservice/feature/scan/presentation/scanner.dart'; import 'package:hl_lieferservice/feature/settings/bloc/settings_bloc.dart'; import 'package:hl_lieferservice/feature/settings/bloc/settings_state.dart'; import 'package:hl_lieferservice/model/article.dart'; import 'package:hl_lieferservice/model/car.dart'; import 'package:hl_lieferservice/model/delivery.dart'; import 'package:hl_lieferservice/model/tour.dart'; import 'package:hl_lieferservice/widget/operations/bloc/operation_bloc.dart'; import 'package:hl_lieferservice/widget/operations/bloc/operation_event.dart'; import '../../delivery/overview/bloc/tour_bloc.dart'; class ArticleScanningScreen extends StatefulWidget { const ArticleScanningScreen({super.key}); @override State createState() => _ArticleScanningScreenState(); } class _ArticleScanningScreenState extends State { final FocusNode _focusNode = FocusNode(); String _buffer = ''; Timer? _bufferTimer; int _selectedDelivery = 0; int? _selectedCarId; @override void initState() { super.initState(); // Focus anfordern, um Keyboard-Events zu empfangen WidgetsBinding.instance.addPostFrameCallback((_) { _focusNode.requestFocus(); }); final state = context.read().state; if (state is TourLoaded) { setState(() { _selectedCarId = state.tour.deliveries[_selectedDelivery].carId; }); } } @override void dispose() { _focusNode.dispose(); _bufferTimer?.cancel(); super.dispose(); } void _handleKey(KeyEvent event) { if (event is KeyDownEvent) { if (event.logicalKey == LogicalKeyboardKey.enter) { // Enter = Scan abgeschlossen _bufferTimer?.cancel(); if (_buffer.isNotEmpty) { _handleBarcodeScanned(_buffer); _buffer = ''; } } else { // Zeichen zum Buffer hinzufügen final character = event.character; if (character != null && character.isNotEmpty) { _buffer += character; // Timer zurücksetzen _bufferTimer?.cancel(); _bufferTimer = Timer(Duration(milliseconds: 1000), () { // Nach 1 Sekunde ohne neue Eingabe: Buffer verarbeiten if (_buffer.isNotEmpty) { _handleBarcodeScanned(_buffer); _buffer = ''; } }); } } } } void _handleBarcodeScanned(String barcode) { if (_selectedCarId == null) { context.read().add( FailOperation(message: "Wählen Sie zu erst ein Fahrzeug aus"), ); return; } final state = context.read().state as TourLoaded; context.read().add( ScanArticleEvent( articleNumber: barcode, carId: _selectedCarId!.toString(), deliveryId: state.tour.deliveries[_selectedDelivery].id, ), ); } Widget _carSelection(List cars, List deliveries) { return Padding( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "Fahrzeug auswählen", style: Theme.of(context).textTheme.headlineSmall, ), Padding( padding: const EdgeInsets.only(top: 10), child: SizedBox( width: double.infinity, height: 50, child: ListView( scrollDirection: Axis.horizontal, children: cars.map((car) { Color? backgroundColor; Color? iconColor = Theme.of(context).primaryColor; Color? textColor; if (_selectedCarId == car.id) { backgroundColor = Theme.of(context).primaryColor; textColor = Theme.of(context).colorScheme.onSecondary; iconColor = Theme.of(context).colorScheme.onSecondary; } return Padding( padding: const EdgeInsets.only(right: 8), child: GestureDetector( onTap: () { context.read().add( AssignCarEvent( deliveryId: deliveries[_selectedDelivery].id, carId: car.id.toString(), ), ); setState(() { _selectedCarId = car.id; }); }, child: Chip( backgroundColor: backgroundColor, label: Row( children: [ Icon( Icons.local_shipping, color: iconColor, size: 20, ), Padding( padding: const EdgeInsets.only(left: 5), child: Text( car.plate, style: TextStyle( color: textColor, fontSize: 12, ), ), ), ], ), ), ), ); }).toList(), ), ), ), ], ), ); } Widget _articles(List
articles) { List
scannableArticles = articles.where((article) => article.scannable).toList(); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.only(left: 20, bottom: 20), child: Text( "Artikel", style: Theme.of(context).textTheme.headlineSmall, ), ), scannableArticles.isEmpty ? Center( child: Text( 'Keine Artikel zum Scannen vorhanden', style: TextStyle(fontSize: 18), ), ) : ListView.separated( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), itemCount: scannableArticles.length, separatorBuilder: (context, index) => Divider( height: 0, color: Theme.of(context).colorScheme.surfaceContainerHighest, ), itemBuilder: (context, index) { final article = scannableArticles[index]; return ListTile( leading: article.scannedAmount == article.amount ? Icon( Icons.check_circle, color: Colors.green, size: 32, ) : Container( width: 32, alignment: Alignment.center, child: Text( '${article.scannedAmount}/${article.amount}', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: article.scannedAmount > 0 ? Colors.blue : Colors.grey, ), ), ), title: Text( article.name, style: TextStyle(fontWeight: FontWeight.w500), ), subtitle: Text("Artikelnr. ${article.articleNumber}"), tileColor: article.scannedAmount == article.amount ? Colors.green.withValues(alpha: 0.1) : Theme.of(context).colorScheme.onSecondary, ); }, ), ], ); } void _selectDelivery(int? index) { setState(() { _selectedDelivery = index!; }); } Widget _navigation(List deliveries) { return Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ OutlinedButton( onPressed: _selectedDelivery > 0 ? () => { if (_selectedDelivery > 0) { setState(() { _selectedDelivery -= 1; _selectedCarId = deliveries[_selectedDelivery].carId; }), }, } : null, child: Text("zurück"), ), Expanded( child: Padding( padding: const EdgeInsets.only(left: 20, right: 20), child: DropdownButton( menuWidth: MediaQuery.of(context).size.width, isExpanded: true, items: deliveries .mapIndexed( (index, delivery) => DropdownMenuItem( value: index, child: Text( delivery.customer.name, overflow: TextOverflow.ellipsis, ), ), ) .toList(), onChanged: _selectDelivery, value: _selectedDelivery, ), ), ), OutlinedButton( onPressed: _selectedDelivery < deliveries.length - 1 ? () => { if (_selectedDelivery + 1 < deliveries.length) { setState(() { _selectedDelivery += 1; _selectedCarId = deliveries[_selectedDelivery].carId; }), }, } : null, child: Text("weiter"), ), ], ); } Widget _deliveryStepper(Tour tour) { final settingsState = context.read().state; Widget scannerWidget = BarcodeScannerWidget( onBarcodeDetected: _handleBarcodeScanned, ); if (settingsState is AppSettingsFailed) { context.read().add( FailOperation( message: "Einstellungen konnten nicht geladen werden. Nutze Kamera-Scanner.", ), ); } if (settingsState is AppSettingsLoaded) { if (settingsState.settings.useHardwareScanner) { scannerWidget = Container(); } } return Padding( padding: const EdgeInsets.all(0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ scannerWidget, _carSelection(tour.driver.cars, tour.deliveries), _articles(tour.deliveries[_selectedDelivery].articles), ], ), ); } @override Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { if (state is TourLoaded) { Delivery delivery = state.tour.deliveries[_selectedDelivery]; return Scaffold( appBar: AppBar( title: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( delivery.customer.name, style: TextStyle( color: Theme.of(context).colorScheme.onSecondary, fontWeight: FontWeight.w500, ), ), Text( delivery.customer.address.toString(), style: TextStyle( fontSize: 14, fontWeight: FontWeight.normal, color: Theme.of(context).colorScheme.onSecondary, ), ), ], ), backgroundColor: Theme.of(context).primaryColor, ), bottomNavigationBar: Padding( padding: const EdgeInsets.all(25), child: _navigation(state.tour.deliveries), ), body: KeyboardListener( focusNode: _focusNode, onKeyEvent: _handleKey, child: _deliveryStepper(state.tour), ), ); } return Container(); }, ); } }