Implemented settings, new scan, enhanced UI/UX
This commit is contained in:
405
lib/feature/scan/presentation/scan_screen.dart
Normal file
405
lib/feature/scan/presentation/scan_screen.dart
Normal file
@ -0,0 +1,405 @@
|
||||
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<ArticleScanningScreen> createState() => _ArticleScanningScreenState();
|
||||
}
|
||||
|
||||
class _ArticleScanningScreenState extends State<ArticleScanningScreen> {
|
||||
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<TourBloc>().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<OperationBloc>().add(
|
||||
FailOperation(message: "Wählen Sie zu erst ein Fahrzeug aus"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final state = context.read<TourBloc>().state as TourLoaded;
|
||||
|
||||
context.read<TourBloc>().add(
|
||||
ScanArticleEvent(
|
||||
articleNumber: barcode,
|
||||
carId: _selectedCarId!.toString(),
|
||||
deliveryId: state.tour.deliveries[_selectedDelivery].id,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _carSelection(List<Car> cars, List<Delivery> 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<TourBloc>().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<Article> articles) {
|
||||
List<Article> 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<Delivery> 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<SettingsBloc>().state;
|
||||
Widget scannerWidget = BarcodeScannerWidget(
|
||||
onBarcodeDetected: _handleBarcodeScanned,
|
||||
);
|
||||
|
||||
if (settingsState is AppSettingsFailed) {
|
||||
context.read<OperationBloc>().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<TourBloc, TourState>(
|
||||
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();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user