Implemented settings, new scan, enhanced UI/UX
This commit is contained in:
94
lib/feature/scan/presentation/scan_article_overview.dart
Normal file
94
lib/feature/scan/presentation/scan_article_overview.dart
Normal file
@ -0,0 +1,94 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../model/article.dart';
|
||||
|
||||
class ArticleOverview extends StatefulWidget {
|
||||
const ArticleOverview({super.key, required this.articleGroups});
|
||||
|
||||
final Map<String, ArticleGroup> articleGroups;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _ArticleOverviewState();
|
||||
}
|
||||
|
||||
class _ArticleOverviewState extends State<ArticleOverview> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final sortedArticles =
|
||||
widget.articleGroups.values.toList()
|
||||
..sort((a, b) => a.articleName.compareTo(b.articleName));
|
||||
|
||||
return sortedArticles.isEmpty
|
||||
? Center(
|
||||
child: Text(
|
||||
'Keine Artikel zum Scannen vorhanden',
|
||||
style: TextStyle(fontSize: 18),
|
||||
),
|
||||
)
|
||||
: ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
itemCount: sortedArticles.length,
|
||||
separatorBuilder: (context, index) => Divider(height: 0, color: Theme.of(context).colorScheme.surfaceContainerHighest),
|
||||
itemBuilder: (context, index) {
|
||||
final group = sortedArticles[index];
|
||||
|
||||
return ListTile(
|
||||
leading:
|
||||
group.isComplete
|
||||
? Icon(Icons.check_circle, color: Colors.green, size: 32)
|
||||
: Container(
|
||||
width: 32,
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
'${group.scannedCount}/${group.totalCount}',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color:
|
||||
group.scannedCount > 0
|
||||
? Colors.blue
|
||||
: Colors.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
"${group.articleName} (Artikelnr. ${group.articleNumber})",
|
||||
style: TextStyle(fontWeight: FontWeight.w500),
|
||||
),
|
||||
subtitle: Padding(
|
||||
padding: const EdgeInsets.only(top: 10, bottom: 10),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children:
|
||||
group.deliveryIds
|
||||
.map(
|
||||
(delivery) => Row(
|
||||
children: [
|
||||
Icon(Icons.person),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 5, bottom: 10),
|
||||
child: Text(
|
||||
"${delivery.customer.name.toString()}\n${delivery.customer.address.toString()}",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
tileColor:
|
||||
group.isComplete
|
||||
? Colors.green.withValues(alpha: 0.1)
|
||||
: Theme.of(context).colorScheme.onSecondary,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
279
lib/feature/scan/presentation/scan_page.dart
Normal file
279
lib/feature/scan/presentation/scan_page.dart
Normal file
@ -0,0 +1,279 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:mobile_scanner/mobile_scanner.dart';
|
||||
|
||||
import 'package:hl_lieferservice/feature/delivery/overview/bloc/tour_bloc.dart';
|
||||
import 'package:hl_lieferservice/feature/delivery/overview/bloc/tour_state.dart';
|
||||
import 'package:hl_lieferservice/feature/scan/presentation/scan_screen.dart';
|
||||
import 'package:hl_lieferservice/model/tour.dart';
|
||||
import 'package:hl_lieferservice/widget/home/bloc/navigation_bloc.dart';
|
||||
import 'package:hl_lieferservice/widget/home/bloc/navigation_event.dart';
|
||||
|
||||
enum TourHomeSteps { planning, delivery, off }
|
||||
|
||||
class ScanPage extends StatefulWidget {
|
||||
const ScanPage({super.key});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _ScanPageState();
|
||||
}
|
||||
|
||||
class _ScanPageState extends State<ScanPage> {
|
||||
int _currentStepIndex = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_tryFinish(context
|
||||
.read<TourBloc>()
|
||||
.state);
|
||||
}
|
||||
|
||||
void _onStartScan() {
|
||||
Navigator.of(
|
||||
context,
|
||||
).push(MaterialPageRoute(builder: (context) => ArticleScanningScreen()));
|
||||
}
|
||||
|
||||
Widget _tourSteps(Tour tour) {
|
||||
var allArticlesScanned = tour.deliveries.every(
|
||||
(delivery) => delivery.allArticlesScanned(),
|
||||
);
|
||||
|
||||
return Stepper(
|
||||
currentStep: _currentStepIndex,
|
||||
controlsBuilder: (context, details) {
|
||||
if (details.stepIndex == TourHomeSteps.planning.index) {
|
||||
return Container(
|
||||
alignment: Alignment.center,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 15),
|
||||
child: FilledButton.icon(
|
||||
label: const Text("Scannen"),
|
||||
onPressed: _onStartScan,
|
||||
icon: const Icon(Icons.qr_code),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return Container(
|
||||
alignment: Alignment.center,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 15),
|
||||
child: FilledButton.icon(
|
||||
label: const Text("Auslieferung starten"),
|
||||
onPressed:
|
||||
allArticlesScanned
|
||||
? () =>
|
||||
context.read<NavigationBloc>().add(
|
||||
NavigateToIndex(index: 1))
|
||||
: null,
|
||||
icon: const Icon(Icons.local_shipping),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
onStepContinue:
|
||||
_currentStepIndex >= 1
|
||||
? null
|
||||
: () =>
|
||||
setState(() {
|
||||
if (_currentStepIndex < 2) {
|
||||
_currentStepIndex += 1;
|
||||
}
|
||||
}),
|
||||
onStepCancel:
|
||||
_currentStepIndex == 0
|
||||
? null
|
||||
: () =>
|
||||
setState(() {
|
||||
if (_currentStepIndex > 0) {
|
||||
_currentStepIndex -= 1;
|
||||
}
|
||||
}),
|
||||
onStepTapped:
|
||||
(value) =>
|
||||
setState(() {
|
||||
if (_currentStepIndex == 1 && allArticlesScanned) {
|
||||
return;
|
||||
}
|
||||
|
||||
_currentStepIndex = value;
|
||||
}),
|
||||
steps: [
|
||||
Step(
|
||||
title: Row(
|
||||
children: [
|
||||
Text(
|
||||
"Fahrzeuge beladen",
|
||||
style: TextStyle(
|
||||
color: allArticlesScanned ? Colors.grey : null,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 5),
|
||||
child:
|
||||
!allArticlesScanned
|
||||
? const Icon(
|
||||
Icons.access_time_filled,
|
||||
color: Colors.orangeAccent,
|
||||
)
|
||||
: const Icon(
|
||||
Icons.check_circle,
|
||||
color: Colors.lightGreen,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
content: const Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.only(bottom: 10),
|
||||
child: Icon(Icons.barcode_reader, color: Colors.black),
|
||||
),
|
||||
Text(
|
||||
"Scannen Sie die Ware, die Sie für die Auslieferungen benötigen.",
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Step(
|
||||
title: const Text("Ausliefern"),
|
||||
content: Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
child:
|
||||
!allArticlesScanned
|
||||
? const Text(
|
||||
"Scannen Sie erst die benötigte Ware, um die Auslieferungen zu beginnen.",
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _info(Tour tour) {
|
||||
int amountArticles = tour.deliveries.fold(
|
||||
0,
|
||||
(acc, delivery) =>
|
||||
acc +
|
||||
delivery.articles
|
||||
.where((article) => article.scannable)
|
||||
.fold(
|
||||
0,
|
||||
(amountArticles, article) => amountArticles + article.amount,
|
||||
),
|
||||
);
|
||||
|
||||
int amountCars = tour.driver.cars.length;
|
||||
int amountDeliveries = tour.deliveries.length;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: Card(
|
||||
color: Theme
|
||||
.of(context)
|
||||
.colorScheme
|
||||
.onSecondary,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.archive),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 5),
|
||||
child: Text("Anzahl Artikel"),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(amountArticles.toString()),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 15),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.local_shipping_outlined),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 5),
|
||||
child: Text("Anzahl Fahrzeuge"),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(amountCars.toString()),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 15),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.person),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 5),
|
||||
child: Text("Anzahl Lieferungen"),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(amountDeliveries.toString()),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _tryFinish(TourState state) {
|
||||
if (state is TourLoaded) {
|
||||
if (state.tour.deliveries.every(
|
||||
(delivery) => delivery.allArticlesScanned(),
|
||||
)) {
|
||||
setState(() {
|
||||
_currentStepIndex = 1;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocConsumer<TourBloc, TourState>(
|
||||
listener: (context, state) {
|
||||
_tryFinish(state);
|
||||
},
|
||||
builder: (context, state) {
|
||||
if (state is TourLoaded) {
|
||||
return Column(children: [_info(state.tour), _tourSteps(state.tour)]);
|
||||
}
|
||||
|
||||
return Center(child: CircularProgressIndicator());
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
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();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
80
lib/feature/scan/presentation/scanner.dart
Normal file
80
lib/feature/scan/presentation/scanner.dart
Normal file
@ -0,0 +1,80 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:mobile_scanner/mobile_scanner.dart';
|
||||
|
||||
/// StatefulWidget für den Barcode-Scanner mit grünem Border-Feedback
|
||||
class BarcodeScannerWidget extends StatefulWidget {
|
||||
final Function(String) onBarcodeDetected;
|
||||
|
||||
const BarcodeScannerWidget({
|
||||
Key? key,
|
||||
required this.onBarcodeDetected,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<BarcodeScannerWidget> createState() => _BarcodeScannerWidgetState();
|
||||
}
|
||||
|
||||
class _BarcodeScannerWidgetState extends State<BarcodeScannerWidget> {
|
||||
bool _isDetected = false;
|
||||
DateTime? _lastScannedTime;
|
||||
final Duration _scanTimeout = const Duration(milliseconds: 2000); // 2 Sekunden Cooldown
|
||||
|
||||
void _handleBarcodeDetected(String barcode) {
|
||||
final now = DateTime.now();
|
||||
|
||||
// Prüfe ob genug Zeit seit dem letzten erfolgreichen Scan vergangen ist
|
||||
if (_lastScannedTime != null &&
|
||||
now.difference(_lastScannedTime!).inMilliseconds < _scanTimeout.inMilliseconds) {
|
||||
// Timeout nicht abgelaufen - ignoriere diesen Scan
|
||||
debugPrint('Scan ignoriert - Cooldown aktiv');
|
||||
return;
|
||||
}
|
||||
|
||||
// Update letzte Scan-Zeit
|
||||
_lastScannedTime = now;
|
||||
|
||||
// Rand grün färben
|
||||
setState(() {
|
||||
_isDetected = true;
|
||||
});
|
||||
|
||||
// Nach 500ms wieder zurücksetzen
|
||||
Future.delayed(const Duration(milliseconds: 500), () {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isDetected = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Callback aufrufen
|
||||
widget.onBarcodeDetected(barcode);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenHeight = MediaQuery.of(context).size.height;
|
||||
final scannerHeight = screenHeight / 4;
|
||||
|
||||
return Container(
|
||||
height: scannerHeight,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: _isDetected ? Colors.green : Colors.grey,
|
||||
width: _isDetected ? 4 : 2,
|
||||
),
|
||||
),
|
||||
child: MobileScanner(
|
||||
onDetect: (capture) {
|
||||
final List<Barcode> barcodes = capture.barcodes;
|
||||
|
||||
for (final barcode in barcodes) {
|
||||
if (barcode.rawValue != null) {
|
||||
_handleBarcodeDetected(barcode.rawValue!);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user