406 lines
13 KiB
Dart
406 lines
13 KiB
Dart
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();
|
|
},
|
|
);
|
|
}
|
|
}
|