Added components to article
This commit is contained in:
@ -1,5 +1,7 @@
|
|||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
import 'component.dart';
|
||||||
|
|
||||||
part 'article.g.dart';
|
part 'article.g.dart';
|
||||||
|
|
||||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
@JsonSerializable(fieldRename: FieldRename.snake)
|
||||||
@ -15,6 +17,10 @@ class ArticleDTO {
|
|||||||
required this.scannedAmount,
|
required this.scannedAmount,
|
||||||
required this.removeNoteId,
|
required this.removeNoteId,
|
||||||
required this.taxRate,
|
required this.taxRate,
|
||||||
|
required this.isParent,
|
||||||
|
this.components,
|
||||||
|
this.warehouseNr,
|
||||||
|
this.warehouseName,
|
||||||
});
|
});
|
||||||
|
|
||||||
String name;
|
String name;
|
||||||
@ -27,6 +33,10 @@ class ArticleDTO {
|
|||||||
String scannedRemovedAmount;
|
String scannedRemovedAmount;
|
||||||
String? removeNoteId;
|
String? removeNoteId;
|
||||||
bool scannable;
|
bool scannable;
|
||||||
|
bool isParent;
|
||||||
|
List<ComponentDTO>? components;
|
||||||
|
String? warehouseNr;
|
||||||
|
String? warehouseName;
|
||||||
|
|
||||||
factory ArticleDTO.fromJson(Map<String, dynamic> json) =>
|
factory ArticleDTO.fromJson(Map<String, dynamic> json) =>
|
||||||
_$ArticleDTOFromJson(json);
|
_$ArticleDTOFromJson(json);
|
||||||
|
|||||||
@ -17,6 +17,13 @@ ArticleDTO _$ArticleDTOFromJson(Map<String, dynamic> json) => ArticleDTO(
|
|||||||
scannedAmount: json['scanned_amount'] as String,
|
scannedAmount: json['scanned_amount'] as String,
|
||||||
removeNoteId: json['remove_note_id'] as String?,
|
removeNoteId: json['remove_note_id'] as String?,
|
||||||
taxRate: json['tax_rate'] as String,
|
taxRate: json['tax_rate'] as String,
|
||||||
|
isParent: json['is_parent'] as bool,
|
||||||
|
components:
|
||||||
|
(json['components'] as List<dynamic>?)
|
||||||
|
?.map((e) => ComponentDTO.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
warehouseNr: json['warehouse_nr'] as String?,
|
||||||
|
warehouseName: json['warehouse_name'] as String?,
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$ArticleDTOToJson(ArticleDTO instance) =>
|
Map<String, dynamic> _$ArticleDTOToJson(ArticleDTO instance) =>
|
||||||
@ -31,4 +38,8 @@ Map<String, dynamic> _$ArticleDTOToJson(ArticleDTO instance) =>
|
|||||||
'scanned_removed_amount': instance.scannedRemovedAmount,
|
'scanned_removed_amount': instance.scannedRemovedAmount,
|
||||||
'remove_note_id': instance.removeNoteId,
|
'remove_note_id': instance.removeNoteId,
|
||||||
'scannable': instance.scannable,
|
'scannable': instance.scannable,
|
||||||
|
'is_parent': instance.isParent,
|
||||||
|
'components': instance.components,
|
||||||
|
'warehouse_nr': instance.warehouseNr,
|
||||||
|
'warehouse_name': instance.warehouseName,
|
||||||
};
|
};
|
||||||
|
|||||||
23
lib/dto/component.dart
Normal file
23
lib/dto/component.dart
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
part 'component.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable(fieldRename: FieldRename.snake)
|
||||||
|
class ComponentDTO {
|
||||||
|
ComponentDTO({
|
||||||
|
required this.articleNr,
|
||||||
|
required this.name,
|
||||||
|
required this.quantity,
|
||||||
|
required this.pos,
|
||||||
|
});
|
||||||
|
|
||||||
|
String articleNr;
|
||||||
|
String name;
|
||||||
|
String quantity;
|
||||||
|
String pos;
|
||||||
|
|
||||||
|
factory ComponentDTO.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$ComponentDTOFromJson(json);
|
||||||
|
|
||||||
|
Map<dynamic, dynamic> toJson() => _$ComponentDTOToJson(this);
|
||||||
|
}
|
||||||
22
lib/dto/component.g.dart
Normal file
22
lib/dto/component.g.dart
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'component.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
ComponentDTO _$ComponentDTOFromJson(Map<String, dynamic> json) => ComponentDTO(
|
||||||
|
articleNr: json['article_nr'] as String,
|
||||||
|
name: json['name'] as String,
|
||||||
|
quantity: json['quantity'] as String,
|
||||||
|
pos: json['pos'] as String,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$ComponentDTOToJson(ComponentDTO instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'article_nr': instance.articleNr,
|
||||||
|
'name': instance.name,
|
||||||
|
'quantity': instance.quantity,
|
||||||
|
'pos': instance.pos,
|
||||||
|
};
|
||||||
@ -25,6 +25,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
|||||||
try {
|
try {
|
||||||
debugPrint("Retrieve user information");
|
debugPrint("Retrieve user information");
|
||||||
|
|
||||||
|
emit(Authenticating());
|
||||||
var response = await service.getUserinfo(event.sessionId);
|
var response = await service.getUserinfo(event.sessionId);
|
||||||
var state = Authenticated(sessionId: event.sessionId, user: response);
|
var state = Authenticated(sessionId: event.sessionId, user: response);
|
||||||
locator.registerSingleton<Authenticated>(state);
|
locator.registerSingleton<Authenticated>(state);
|
||||||
@ -34,6 +35,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
|||||||
debugPrint(err.toString());
|
debugPrint(err.toString());
|
||||||
debugPrint(st.toString());
|
debugPrint(st.toString());
|
||||||
|
|
||||||
|
emit(Unauthenticated());
|
||||||
operationBloc.add(
|
operationBloc.add(
|
||||||
FailOperation(
|
FailOperation(
|
||||||
message: "Login war nicht erfolgreich. Probieren Sie es erneut.",
|
message: "Login war nicht erfolgreich. Probieren Sie es erneut.",
|
||||||
|
|||||||
@ -7,6 +7,10 @@ class Unauthenticated extends AuthState {
|
|||||||
Unauthenticated({this.sessionExpired = false});
|
Unauthenticated({this.sessionExpired = false});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Transient state while [SetAuthenticatedEvent] is being processed and the
|
||||||
|
/// user info is being fetched from the server.
|
||||||
|
class Authenticating extends AuthState {}
|
||||||
|
|
||||||
class Authenticated extends AuthState {
|
class Authenticated extends AuthState {
|
||||||
User user;
|
User user;
|
||||||
String sessionId;
|
String sessionId;
|
||||||
|
|||||||
@ -3,7 +3,9 @@ import 'package:app_links/app_links.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:hl_lieferservice/feature/authentication/bloc/auth_bloc.dart';
|
import 'package:hl_lieferservice/feature/authentication/bloc/auth_bloc.dart';
|
||||||
import 'package:hl_lieferservice/feature/authentication/bloc/auth_event.dart';
|
import 'package:hl_lieferservice/feature/authentication/bloc/auth_event.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/authentication/bloc/auth_state.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
import 'package:hl_lieferservice/util.dart';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
class LoginPage extends StatefulWidget {
|
class LoginPage extends StatefulWidget {
|
||||||
@ -60,9 +62,7 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
// Small delay to ensure listener is ready
|
// Small delay to ensure listener is ready
|
||||||
await Future.delayed(const Duration(milliseconds: 500));
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
|
|
||||||
debugPrint("🔵 Opening browser to: http://localhost:3000/login");
|
final loginUrl = Uri.parse('${getConfig().backendUrl}/login');
|
||||||
|
|
||||||
final loginUrl = Uri.parse('http://192.168.1.9:3000/login');
|
|
||||||
final launched = await launchUrl(
|
final launched = await launchUrl(
|
||||||
loginUrl,
|
loginUrl,
|
||||||
mode: LaunchMode.externalApplication,
|
mode: LaunchMode.externalApplication,
|
||||||
@ -176,17 +176,30 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 15, bottom: 15),
|
padding: const EdgeInsets.only(top: 15, bottom: 15),
|
||||||
child: _isLoading
|
child: BlocBuilder<AuthBloc, AuthState>(
|
||||||
? const Column(
|
builder: (context, authState) {
|
||||||
children: [
|
final isBusy =
|
||||||
CircularProgressIndicator(),
|
_isLoading || authState is Authenticating;
|
||||||
SizedBox(height: 16),
|
if (!isBusy) {
|
||||||
Text('Warte auf Login...'),
|
return OutlinedButton(
|
||||||
],
|
onPressed: _onPressLogin,
|
||||||
)
|
child: const Text(
|
||||||
: OutlinedButton(
|
"Anmelden mit Holzleitner Login",
|
||||||
onPressed: _onPressLogin,
|
),
|
||||||
child: const Text("Anmelden mit Holzleitner Login"),
|
);
|
||||||
|
}
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
const CircularProgressIndicator(),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
authState is Authenticating
|
||||||
|
? 'Anmeldung wird abgeschlossen…'
|
||||||
|
: 'Warte auf Login...',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@ -24,7 +24,7 @@ class CarSelectBloc extends Bloc<CarSelectEvent, CarSelectState> {
|
|||||||
try {
|
try {
|
||||||
emit(CarSelectLoading());
|
emit(CarSelectLoading());
|
||||||
|
|
||||||
final CarSelection? stored = await repository.getSelection();
|
final CarSelection? stored = await repository.getSelection(event.userId);
|
||||||
final today = DateTime.now();
|
final today = DateTime.now();
|
||||||
|
|
||||||
final bool validForToday =
|
final bool validForToday =
|
||||||
@ -72,6 +72,7 @@ class CarSelectBloc extends Bloc<CarSelectEvent, CarSelectState> {
|
|||||||
try {
|
try {
|
||||||
final today = DateTime.now();
|
final today = DateTime.now();
|
||||||
await repository.saveSelection(
|
await repository.saveSelection(
|
||||||
|
event.userId,
|
||||||
CarSelection(
|
CarSelection(
|
||||||
date: today,
|
date: today,
|
||||||
selectedCarId: event.car.id,
|
selectedCarId: event.car.id,
|
||||||
|
|||||||
@ -2,14 +2,20 @@ import 'package:hl_lieferservice/model/car.dart';
|
|||||||
|
|
||||||
abstract class CarSelectEvent {}
|
abstract class CarSelectEvent {}
|
||||||
|
|
||||||
/// Fired at app startup to check if a car has already been selected for today.
|
/// Fired at app startup to check if a car has already been selected for today
|
||||||
class CarSelectLoad extends CarSelectEvent {}
|
/// for the given user.
|
||||||
|
class CarSelectLoad extends CarSelectEvent {
|
||||||
|
final String userId;
|
||||||
|
|
||||||
|
CarSelectLoad({required this.userId});
|
||||||
|
}
|
||||||
|
|
||||||
/// Fired when the driver confirms their car choice for the day.
|
/// Fired when the driver confirms their car choice for the day.
|
||||||
class CarSelectConfirm extends CarSelectEvent {
|
class CarSelectConfirm extends CarSelectEvent {
|
||||||
|
final String userId;
|
||||||
final Car car;
|
final Car car;
|
||||||
|
|
||||||
CarSelectConfirm({required this.car});
|
CarSelectConfirm({required this.userId, required this.car});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fired when the driver wants to switch to a different car.
|
/// Fired when the driver wants to switch to a different car.
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/authentication/bloc/auth_bloc.dart';
|
||||||
|
import 'package:hl_lieferservice/feature/authentication/bloc/auth_state.dart';
|
||||||
import 'package:hl_lieferservice/feature/car_selection/bloc/bloc.dart';
|
import 'package:hl_lieferservice/feature/car_selection/bloc/bloc.dart';
|
||||||
import 'package:hl_lieferservice/feature/car_selection/bloc/events.dart';
|
import 'package:hl_lieferservice/feature/car_selection/bloc/events.dart';
|
||||||
import 'package:hl_lieferservice/feature/car_selection/bloc/state.dart';
|
import 'package:hl_lieferservice/feature/car_selection/bloc/state.dart';
|
||||||
@ -18,7 +20,12 @@ class _CarSelectionEnforcerState extends State<CarSelectionEnforcer> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
context.read<CarSelectBloc>().add(CarSelectLoad());
|
final authState = context.read<AuthBloc>().state;
|
||||||
|
if (authState is Authenticated) {
|
||||||
|
context
|
||||||
|
.read<CarSelectBloc>()
|
||||||
|
.add(CarSelectLoad(userId: authState.user.number));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -53,8 +60,14 @@ class _CarSelectionEnforcerState extends State<CarSelectionEnforcer> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: () =>
|
onPressed: () {
|
||||||
context.read<CarSelectBloc>().add(CarSelectLoad()),
|
final authState = context.read<AuthBloc>().state;
|
||||||
|
if (authState is Authenticated) {
|
||||||
|
context.read<CarSelectBloc>().add(
|
||||||
|
CarSelectLoad(userId: authState.user.number),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
child: const Text("Erneut versuchen"),
|
child: const Text("Erneut versuchen"),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@ -52,7 +52,13 @@ class _CarSelectionPageState extends State<CarSelectionPage> {
|
|||||||
|
|
||||||
void _onConfirm() {
|
void _onConfirm() {
|
||||||
if (_selectedCar == null) return;
|
if (_selectedCar == null) return;
|
||||||
context.read<CarSelectBloc>().add(CarSelectConfirm(car: _selectedCar!));
|
final authState = context.read<AuthBloc>().state as Authenticated;
|
||||||
|
context.read<CarSelectBloc>().add(
|
||||||
|
CarSelectConfirm(
|
||||||
|
userId: authState.user.number,
|
||||||
|
car: _selectedCar!,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildCarList(List<Car> cars) {
|
Widget _buildCarList(List<Car> cars) {
|
||||||
|
|||||||
@ -2,17 +2,19 @@ import 'package:hl_lieferservice/feature/cars/model/selection.dart';
|
|||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
class CarSelectionRepository {
|
class CarSelectionRepository {
|
||||||
static const _keyDate = 'car_selection_date';
|
static String _keyDate(String userId) => 'car_selection_${userId}_date';
|
||||||
static const _keyCarId = 'car_selection_car_id';
|
static String _keyCarId(String userId) => 'car_selection_${userId}_car_id';
|
||||||
static const _keyCarPlate = 'car_selection_car_plate';
|
static String _keyCarPlate(String userId) =>
|
||||||
|
'car_selection_${userId}_car_plate';
|
||||||
|
|
||||||
/// Returns the stored [CarSelection], or null if nothing has been saved yet.
|
/// Returns the stored [CarSelection] for the given user, or null if nothing
|
||||||
Future<CarSelection?> getSelection() async {
|
/// has been saved yet for that user.
|
||||||
|
Future<CarSelection?> getSelection(String userId) async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
|
||||||
final dateString = prefs.getString(_keyDate);
|
final dateString = prefs.getString(_keyDate(userId));
|
||||||
final carId = prefs.getInt(_keyCarId);
|
final carId = prefs.getInt(_keyCarId(userId));
|
||||||
final plate = prefs.getString(_keyCarPlate);
|
final plate = prefs.getString(_keyCarPlate(userId));
|
||||||
|
|
||||||
if (dateString == null || carId == null || plate == null) return null;
|
if (dateString == null || carId == null || plate == null) return null;
|
||||||
|
|
||||||
@ -23,12 +25,12 @@ class CarSelectionRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Persists the given [selection] locally on this device.
|
/// Persists the given [selection] for the given user locally on this device.
|
||||||
Future<void> saveSelection(CarSelection selection) async {
|
Future<void> saveSelection(String userId, CarSelection selection) async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
|
||||||
await prefs.setString(_keyDate, selection.date.toIso8601String());
|
await prefs.setString(_keyDate(userId), selection.date.toIso8601String());
|
||||||
await prefs.setInt(_keyCarId, selection.selectedCarId!);
|
await prefs.setInt(_keyCarId(userId), selection.selectedCarId!);
|
||||||
await prefs.setString(_keyCarPlate, selection.selectedCarPlate!);
|
await prefs.setString(_keyCarPlate(userId), selection.selectedCarPlate!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -51,6 +51,7 @@ class CarsBloc extends Bloc<CarEvents, CarsState> {
|
|||||||
Future<void> _carAdd(CarAdd event, Emitter<CarsState> emit) async {
|
Future<void> _carAdd(CarAdd event, Emitter<CarsState> emit) async {
|
||||||
final currentState = state;
|
final currentState = state;
|
||||||
|
|
||||||
|
opBloc.add(StartOperation());
|
||||||
try {
|
try {
|
||||||
Car newCar = await repository.add(event.teamId, event.plate);
|
Car newCar = await repository.add(event.teamId, event.plate);
|
||||||
|
|
||||||
@ -71,6 +72,7 @@ class CarsBloc extends Bloc<CarEvents, CarsState> {
|
|||||||
Future<void> _carEdit(CarEdit event, Emitter<CarsState> emit) async {
|
Future<void> _carEdit(CarEdit event, Emitter<CarsState> emit) async {
|
||||||
final currentState = state;
|
final currentState = state;
|
||||||
|
|
||||||
|
opBloc.add(StartOperation());
|
||||||
try {
|
try {
|
||||||
await repository.edit(event.teamId, event.newCar);
|
await repository.edit(event.teamId, event.newCar);
|
||||||
|
|
||||||
@ -98,6 +100,7 @@ class CarsBloc extends Bloc<CarEvents, CarsState> {
|
|||||||
Future<void> _carDelete(CarDelete event, Emitter<CarsState> emit) async {
|
Future<void> _carDelete(CarDelete event, Emitter<CarsState> emit) async {
|
||||||
final currentState = state;
|
final currentState = state;
|
||||||
|
|
||||||
|
opBloc.add(StartOperation());
|
||||||
try {
|
try {
|
||||||
await repository.delete(event.carId, event.teamId);
|
await repository.delete(event.carId, event.teamId);
|
||||||
|
|
||||||
|
|||||||
@ -43,6 +43,7 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
|||||||
on<AssignCarEvent>(_assignCar);
|
on<AssignCarEvent>(_assignCar);
|
||||||
on<IncrementArticleScanAmount>(_increment);
|
on<IncrementArticleScanAmount>(_increment);
|
||||||
on<ScanArticleEvent>(_scan);
|
on<ScanArticleEvent>(_scan);
|
||||||
|
on<ScanComponentEvent>(_scanComponent);
|
||||||
on<HoldDeliveryEvent>(_holdDelivery);
|
on<HoldDeliveryEvent>(_holdDelivery);
|
||||||
on<CancelDeliveryEvent>(_cancelDelivery);
|
on<CancelDeliveryEvent>(_cancelDelivery);
|
||||||
on<ReactivateDeliveryEvent>(_reactivateDelivery);
|
on<ReactivateDeliveryEvent>(_reactivateDelivery);
|
||||||
@ -82,6 +83,7 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
|||||||
) async {
|
) async {
|
||||||
final currentState = state;
|
final currentState = state;
|
||||||
if (currentState is TourLoaded) {
|
if (currentState is TourLoaded) {
|
||||||
|
opBloc.add(StartOperation());
|
||||||
try {
|
try {
|
||||||
await tourRepository.setArticleAmount(
|
await tourRepository.setArticleAmount(
|
||||||
event.deliveryId,
|
event.deliveryId,
|
||||||
@ -89,6 +91,7 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
|||||||
event.amount,
|
event.amount,
|
||||||
event.reason,
|
event.reason,
|
||||||
);
|
);
|
||||||
|
opBloc.add(FinishOperation());
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
debugPrint("$e $st");
|
debugPrint("$e $st");
|
||||||
_handleError(e, "Fehler beim Ändern der Menge des Artikels");
|
_handleError(e, "Fehler beim Ändern der Menge des Artikels");
|
||||||
@ -131,8 +134,6 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
|||||||
) async {
|
) async {
|
||||||
Map<String, double> distances = {};
|
Map<String, double> distances = {};
|
||||||
|
|
||||||
emit(TourRequestingDistances(tour: event.tour, payments: event.payments));
|
|
||||||
|
|
||||||
for (final delivery in event.tour.deliveries) {
|
for (final delivery in event.tour.deliveries) {
|
||||||
try {
|
try {
|
||||||
distances[delivery.id] = await DistanceService.getDistanceByRoad(
|
distances[delivery.id] = await DistanceService.getDistanceByRoad(
|
||||||
@ -141,22 +142,14 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
|||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
debugPrint("Fehler beim Laden der Distanz: $e");
|
debugPrint("Fehler beim Laden der Distanz: $e");
|
||||||
debugPrint("$st");
|
debugPrint("$st");
|
||||||
|
|
||||||
// set the distance to none in order to handle the error case
|
|
||||||
// afterwards for that specific delivery
|
|
||||||
distances[delivery.id] = double.nan;
|
distances[delivery.id] = double.nan;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If an error occurred, then the distances will be empty
|
final currentState = state;
|
||||||
// If the distances are empty then they shouldn't be displayed
|
if (currentState is TourLoaded) {
|
||||||
add(
|
emit(currentState.copyWith(distances: distances));
|
||||||
RequestSortingInformationEvent(
|
}
|
||||||
tour: event.tour,
|
|
||||||
payments: event.payments,
|
|
||||||
distances: distances,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _requestSortingInformation(
|
void _requestSortingInformation(
|
||||||
@ -217,9 +210,10 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
|||||||
tour: event.tour,
|
tour: event.tour,
|
||||||
paymentOptions: event.payments,
|
paymentOptions: event.payments,
|
||||||
sortingInformation: container,
|
sortingInformation: container,
|
||||||
distances: event.distances,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
add(RequestDeliveryDistanceEvent(tour: event.tour));
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updated(TourUpdated event, Emitter<TourState> emit) {
|
void _updated(TourUpdated event, Emitter<TourState> emit) {
|
||||||
@ -235,14 +229,14 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
|||||||
paymentOptions: payments,
|
paymentOptions: payments,
|
||||||
distances: Map<String, double>.from(currentState.distances ?? {}),
|
distances: Map<String, double>.from(currentState.distances ?? {}),
|
||||||
sortingInformation: currentState.sortingInformation,
|
sortingInformation: currentState.sortingInformation,
|
||||||
|
pendingScanRequests: currentState.pendingScanRequests,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Download distances if tour has previously fetched by API
|
|
||||||
if (currentState is TourLoading) {
|
if (currentState is TourLoading) {
|
||||||
add(
|
add(
|
||||||
RequestDeliveryDistanceEvent(tour: tour.copyWith(), payments: payments),
|
RequestSortingInformationEvent(tour: tour.copyWith(), payments: payments),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -253,8 +247,10 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
|||||||
) async {
|
) async {
|
||||||
final currentState = state;
|
final currentState = state;
|
||||||
if (currentState is TourLoaded) {
|
if (currentState is TourLoaded) {
|
||||||
|
opBloc.add(StartOperation());
|
||||||
try {
|
try {
|
||||||
await tourRepository.reactivateDelivery(event.deliveryId);
|
await tourRepository.reactivateDelivery(event.deliveryId);
|
||||||
|
opBloc.add(FinishOperation());
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
debugPrint("$e $st");
|
debugPrint("$e $st");
|
||||||
_handleError(e, "Fehler beim Zurückstellen der Lieferung");
|
_handleError(e, "Fehler beim Zurückstellen der Lieferung");
|
||||||
@ -265,8 +261,10 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
|||||||
void _holdDelivery(HoldDeliveryEvent event, Emitter<TourState> emit) async {
|
void _holdDelivery(HoldDeliveryEvent event, Emitter<TourState> emit) async {
|
||||||
final currentState = state;
|
final currentState = state;
|
||||||
if (currentState is TourLoaded) {
|
if (currentState is TourLoaded) {
|
||||||
|
opBloc.add(StartOperation());
|
||||||
try {
|
try {
|
||||||
await tourRepository.holdDelivery(event.deliveryId);
|
await tourRepository.holdDelivery(event.deliveryId);
|
||||||
|
opBloc.add(FinishOperation());
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
debugPrint("$e $st");
|
debugPrint("$e $st");
|
||||||
_handleError(e, "Fehler beim Zurückstellen der Lieferung");
|
_handleError(e, "Fehler beim Zurückstellen der Lieferung");
|
||||||
@ -280,8 +278,10 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
|||||||
) async {
|
) async {
|
||||||
final currentState = state;
|
final currentState = state;
|
||||||
if (currentState is TourLoaded) {
|
if (currentState is TourLoaded) {
|
||||||
|
opBloc.add(StartOperation());
|
||||||
try {
|
try {
|
||||||
await tourRepository.cancelDelivery(event.deliveryId);
|
await tourRepository.cancelDelivery(event.deliveryId);
|
||||||
|
opBloc.add(FinishOperation());
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
debugPrint("$e $st");
|
debugPrint("$e $st");
|
||||||
_handleError(e, "Fehler beim Stornieren der Lieferung");
|
_handleError(e, "Fehler beim Stornieren der Lieferung");
|
||||||
@ -289,10 +289,58 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _bumpPendingScans(Emitter<TourState> emit, int delta) {
|
||||||
|
final currentState = state;
|
||||||
|
if (currentState is TourLoaded) {
|
||||||
|
final next = (currentState.pendingScanRequests + delta).clamp(0, 1 << 30);
|
||||||
|
emit(currentState.copyWith(pendingScanRequests: next));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _scanComponent(
|
||||||
|
ScanComponentEvent event,
|
||||||
|
Emitter<TourState> emit,
|
||||||
|
) async {
|
||||||
|
final currentState = state;
|
||||||
|
|
||||||
|
if (currentState is TourLoaded) {
|
||||||
|
_bumpPendingScans(emit, 1);
|
||||||
|
try {
|
||||||
|
switch (await tourRepository.scanComponent(
|
||||||
|
event.deliveryId,
|
||||||
|
event.carId,
|
||||||
|
event.componentArticleNumber,
|
||||||
|
)) {
|
||||||
|
case ScanResult.scanned:
|
||||||
|
opBloc.add(FinishOperation(message: 'Komponente gescannt'));
|
||||||
|
break;
|
||||||
|
case ScanResult.alreadyScanned:
|
||||||
|
opBloc.add(
|
||||||
|
FailOperation(message: 'Komponente wurde bereits gescannt'),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case ScanResult.notFound:
|
||||||
|
opBloc.add(
|
||||||
|
FailOperation(
|
||||||
|
message: 'Komponente ist für keine Lieferung vorgesehen',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (e, st) {
|
||||||
|
debugPrint("FEHLER beim Scannen einer Komponente: $e $st");
|
||||||
|
_handleError(e, "Fehler beim Scannen der Komponente");
|
||||||
|
} finally {
|
||||||
|
_bumpPendingScans(emit, -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _scan(ScanArticleEvent event, Emitter<TourState> emit) async {
|
void _scan(ScanArticleEvent event, Emitter<TourState> emit) async {
|
||||||
final currentState = state;
|
final currentState = state;
|
||||||
|
|
||||||
if (currentState is TourLoaded) {
|
if (currentState is TourLoaded) {
|
||||||
|
_bumpPendingScans(emit, 1);
|
||||||
try {
|
try {
|
||||||
switch (await tourRepository.scanArticle(
|
switch (await tourRepository.scanArticle(
|
||||||
event.deliveryId,
|
event.deliveryId,
|
||||||
@ -318,6 +366,8 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
|||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
debugPrint("FEHLER beim Scannen eines Artikels: $e $st");
|
debugPrint("FEHLER beim Scannen eines Artikels: $e $st");
|
||||||
_handleError(e, "Fehler beim Scannen des Artikels");
|
_handleError(e, "Fehler beim Scannen des Artikels");
|
||||||
|
} finally {
|
||||||
|
_bumpPendingScans(emit, -1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -329,6 +379,7 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
|||||||
final currentState = state;
|
final currentState = state;
|
||||||
|
|
||||||
if (currentState is TourLoaded) {
|
if (currentState is TourLoaded) {
|
||||||
|
_bumpPendingScans(emit, 1);
|
||||||
try {
|
try {
|
||||||
await tourRepository.scanArticle(
|
await tourRepository.scanArticle(
|
||||||
event.deliveryId,
|
event.deliveryId,
|
||||||
@ -338,6 +389,8 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
|||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
debugPrint("$e $st");
|
debugPrint("$e $st");
|
||||||
_handleError(e, "Fehler beim Scannen des Artikels");
|
_handleError(e, "Fehler beim Scannen des Artikels");
|
||||||
|
} finally {
|
||||||
|
_bumpPendingScans(emit, -1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -345,8 +398,10 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
|||||||
Future<void> _assignCar(AssignCarEvent event, Emitter<TourState> emit) async {
|
Future<void> _assignCar(AssignCarEvent event, Emitter<TourState> emit) async {
|
||||||
final currentState = state;
|
final currentState = state;
|
||||||
if (currentState is TourLoaded) {
|
if (currentState is TourLoaded) {
|
||||||
|
opBloc.add(StartOperation());
|
||||||
try {
|
try {
|
||||||
await tourRepository.assignCar(event.deliveryId, event.carId);
|
await tourRepository.assignCar(event.deliveryId, event.carId);
|
||||||
|
opBloc.add(FinishOperation());
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
debugPrint("$e $st");
|
debugPrint("$e $st");
|
||||||
_handleError(e, "Fehler beim Zuweisen des Fahrzeugs");
|
_handleError(e, "Fehler beim Zuweisen des Fahrzeugs");
|
||||||
@ -376,6 +431,7 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
|||||||
final currentState = state;
|
final currentState = state;
|
||||||
|
|
||||||
if (currentState is TourLoaded) {
|
if (currentState is TourLoaded) {
|
||||||
|
opBloc.add(StartOperation(message: "Lieferung wird abgeschlossen…"));
|
||||||
try {
|
try {
|
||||||
await tourRepository.uploadDriverSignature(
|
await tourRepository.uploadDriverSignature(
|
||||||
event.deliveryId,
|
event.deliveryId,
|
||||||
@ -387,6 +443,7 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await tourRepository.finishDelivery(event.deliveryId);
|
await tourRepository.finishDelivery(event.deliveryId);
|
||||||
|
opBloc.add(FinishOperation(message: "Lieferung abgeschlossen"));
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
debugPrint("$e $st");
|
debugPrint("$e $st");
|
||||||
_handleError(e, "Fehler beim Abschließen der Lieferung");
|
_handleError(e, "Fehler beim Abschließen der Lieferung");
|
||||||
@ -398,8 +455,10 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
|||||||
UpdateSelectedPaymentMethodEvent event,
|
UpdateSelectedPaymentMethodEvent event,
|
||||||
Emitter<TourState> emit,
|
Emitter<TourState> emit,
|
||||||
) async {
|
) async {
|
||||||
|
opBloc.add(StartOperation());
|
||||||
try {
|
try {
|
||||||
await tourRepository.updatePayment(event.deliveryId, event.payment);
|
await tourRepository.updatePayment(event.deliveryId, event.payment);
|
||||||
|
opBloc.add(FinishOperation());
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
debugPrint("$e $st");
|
debugPrint("$e $st");
|
||||||
_handleError(e, "Fehler beim Aktualisieren des Betrags");
|
_handleError(e, "Fehler beim Aktualisieren des Betrags");
|
||||||
@ -410,12 +469,14 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
|||||||
UpdateDeliveryOptionEvent event,
|
UpdateDeliveryOptionEvent event,
|
||||||
Emitter<TourState> emit,
|
Emitter<TourState> emit,
|
||||||
) async {
|
) async {
|
||||||
|
opBloc.add(StartOperation());
|
||||||
try {
|
try {
|
||||||
await tourRepository.updateOption(
|
await tourRepository.updateOption(
|
||||||
event.deliveryId,
|
event.deliveryId,
|
||||||
event.key,
|
event.key,
|
||||||
event.value,
|
event.value,
|
||||||
);
|
);
|
||||||
|
opBloc.add(FinishOperation());
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
debugPrint("$e $st");
|
debugPrint("$e $st");
|
||||||
_handleError(e, "Fehler beim Aktualisieren der Optionen");
|
_handleError(e, "Fehler beim Aktualisieren der Optionen");
|
||||||
@ -426,12 +487,14 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
|||||||
UpdateDiscountEvent event,
|
UpdateDiscountEvent event,
|
||||||
Emitter<TourState> emit,
|
Emitter<TourState> emit,
|
||||||
) async {
|
) async {
|
||||||
|
opBloc.add(StartOperation());
|
||||||
try {
|
try {
|
||||||
await tourRepository.updateDiscount(
|
await tourRepository.updateDiscount(
|
||||||
event.deliveryId,
|
event.deliveryId,
|
||||||
event.reason,
|
event.reason,
|
||||||
event.value,
|
event.value,
|
||||||
);
|
);
|
||||||
|
opBloc.add(FinishOperation());
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
debugPrint("Fehler beim Aktualisieren des Discounts: $e $st");
|
debugPrint("Fehler beim Aktualisieren des Discounts: $e $st");
|
||||||
_handleError(e, "Fehler beim Aktualisieren des Discounts");
|
_handleError(e, "Fehler beim Aktualisieren des Discounts");
|
||||||
@ -442,8 +505,10 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
|||||||
RemoveDiscountEvent event,
|
RemoveDiscountEvent event,
|
||||||
Emitter<TourState> emit,
|
Emitter<TourState> emit,
|
||||||
) async {
|
) async {
|
||||||
|
opBloc.add(StartOperation());
|
||||||
try {
|
try {
|
||||||
await tourRepository.removeDiscount(event.deliveryId);
|
await tourRepository.removeDiscount(event.deliveryId);
|
||||||
|
opBloc.add(FinishOperation());
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
debugPrint("Fehler beim Löschen des Discounts: $e $st");
|
debugPrint("Fehler beim Löschen des Discounts: $e $st");
|
||||||
_handleError(e, "Fehler beim Löschen des Discounts");
|
_handleError(e, "Fehler beim Löschen des Discounts");
|
||||||
@ -451,12 +516,14 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _addDiscount(AddDiscountEvent event, Emitter<TourState> emit) async {
|
void _addDiscount(AddDiscountEvent event, Emitter<TourState> emit) async {
|
||||||
|
opBloc.add(StartOperation());
|
||||||
try {
|
try {
|
||||||
await tourRepository.addDiscount(
|
await tourRepository.addDiscount(
|
||||||
event.deliveryId,
|
event.deliveryId,
|
||||||
event.reason,
|
event.reason,
|
||||||
event.value,
|
event.value,
|
||||||
);
|
);
|
||||||
|
opBloc.add(FinishOperation());
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
debugPrint("Fehler beim Hinzufügen des Discounts: $e $st");
|
debugPrint("Fehler beim Hinzufügen des Discounts: $e $st");
|
||||||
_handleError(e, "Fehler beim Hinzufügen des Discounts");
|
_handleError(e, "Fehler beim Hinzufügen des Discounts");
|
||||||
@ -464,6 +531,7 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _unscan(UnscanArticleEvent event, Emitter<TourState> emit) async {
|
void _unscan(UnscanArticleEvent event, Emitter<TourState> emit) async {
|
||||||
|
opBloc.add(StartOperation());
|
||||||
try {
|
try {
|
||||||
await tourRepository.unscan(
|
await tourRepository.unscan(
|
||||||
event.deliveryId,
|
event.deliveryId,
|
||||||
@ -471,6 +539,7 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
|||||||
event.newAmount,
|
event.newAmount,
|
||||||
event.reason,
|
event.reason,
|
||||||
);
|
);
|
||||||
|
opBloc.add(FinishOperation());
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
debugPrint("Fehler beim Unscan des Artikels ${event.articleId}: $e $st");
|
debugPrint("Fehler beim Unscan des Artikels ${event.articleId}: $e $st");
|
||||||
_handleError(e, "Fehler beim Unscan des Artikels");
|
_handleError(e, "Fehler beim Unscan des Artikels");
|
||||||
@ -478,8 +547,10 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _resetAmount(ResetScanAmountEvent event, Emitter<TourState> emit) async {
|
void _resetAmount(ResetScanAmountEvent event, Emitter<TourState> emit) async {
|
||||||
|
opBloc.add(StartOperation());
|
||||||
try {
|
try {
|
||||||
await tourRepository.resetScan(event.articleId, event.deliveryId);
|
await tourRepository.resetScan(event.articleId, event.deliveryId);
|
||||||
|
opBloc.add(FinishOperation());
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
debugPrint("Fehler beim Zurücksetzen Artikel ${event.articleId}: $e $st");
|
debugPrint("Fehler beim Zurücksetzen Artikel ${event.articleId}: $e $st");
|
||||||
_handleError(e, "Fehler beim Zurücksetzen");
|
_handleError(e, "Fehler beim Zurücksetzen");
|
||||||
|
|||||||
@ -15,20 +15,17 @@ class LoadTour extends TourEvent {
|
|||||||
|
|
||||||
class RequestDeliveryDistanceEvent extends TourEvent {
|
class RequestDeliveryDistanceEvent extends TourEvent {
|
||||||
Tour tour;
|
Tour tour;
|
||||||
List<Payment> payments;
|
|
||||||
|
|
||||||
RequestDeliveryDistanceEvent({required this.tour, required this.payments});
|
RequestDeliveryDistanceEvent({required this.tour});
|
||||||
}
|
}
|
||||||
|
|
||||||
class RequestSortingInformationEvent extends TourEvent {
|
class RequestSortingInformationEvent extends TourEvent {
|
||||||
Tour tour;
|
Tour tour;
|
||||||
List<Payment> payments;
|
List<Payment> payments;
|
||||||
Map<String, double>? distances;
|
|
||||||
|
|
||||||
RequestSortingInformationEvent({
|
RequestSortingInformationEvent({
|
||||||
required this.tour,
|
required this.tour,
|
||||||
required this.payments,
|
required this.payments,
|
||||||
this.distances,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,6 +87,20 @@ class ScanArticleEvent extends TourEvent {
|
|||||||
String carId;
|
String carId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Scan a single BOM component. The server call for the parent article is
|
||||||
|
/// deferred until *all* components are fully scanned.
|
||||||
|
class ScanComponentEvent extends TourEvent {
|
||||||
|
ScanComponentEvent({
|
||||||
|
required this.componentArticleNumber,
|
||||||
|
required this.carId,
|
||||||
|
required this.deliveryId,
|
||||||
|
});
|
||||||
|
|
||||||
|
String componentArticleNumber;
|
||||||
|
String deliveryId;
|
||||||
|
String carId;
|
||||||
|
}
|
||||||
|
|
||||||
class CancelDeliveryEvent extends TourEvent {
|
class CancelDeliveryEvent extends TourEvent {
|
||||||
String deliveryId;
|
String deliveryId;
|
||||||
|
|
||||||
|
|||||||
@ -8,49 +8,38 @@ class TourLoading extends TourState {}
|
|||||||
|
|
||||||
class TourLoadingFailed extends TourState {}
|
class TourLoadingFailed extends TourState {}
|
||||||
|
|
||||||
class TourRequestingDistances extends TourState {
|
|
||||||
Tour tour;
|
|
||||||
List<Payment> payments;
|
|
||||||
|
|
||||||
TourRequestingDistances({required this.tour, required this.payments});
|
|
||||||
}
|
|
||||||
|
|
||||||
class TourRequestingSortingInformation extends TourState {
|
|
||||||
Tour tour;
|
|
||||||
Map<String, double>? distances;
|
|
||||||
List<Payment> paymentOptions;
|
|
||||||
|
|
||||||
TourRequestingSortingInformation({
|
|
||||||
required this.tour,
|
|
||||||
this.distances,
|
|
||||||
required this.paymentOptions,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
class TourLoaded extends TourState {
|
class TourLoaded extends TourState {
|
||||||
Tour tour;
|
Tour tour;
|
||||||
Map<String, double>? distances;
|
Map<String, double>? distances;
|
||||||
List<Payment> paymentOptions;
|
List<Payment> paymentOptions;
|
||||||
Map<String, List<String>> sortingInformation;
|
Map<String, List<String>> sortingInformation;
|
||||||
|
|
||||||
|
/// Number of scan-related server requests currently in flight. Drives the
|
||||||
|
/// inline indicator on the scanner widget. Using a counter (not bool) lets
|
||||||
|
/// rapid-fire scans coexist without one prematurely clearing the indicator.
|
||||||
|
int pendingScanRequests;
|
||||||
|
|
||||||
TourLoaded({
|
TourLoaded({
|
||||||
required this.tour,
|
required this.tour,
|
||||||
this.distances,
|
this.distances,
|
||||||
required this.paymentOptions,
|
required this.paymentOptions,
|
||||||
required this.sortingInformation
|
required this.sortingInformation,
|
||||||
|
this.pendingScanRequests = 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
TourLoaded copyWith({
|
TourLoaded copyWith({
|
||||||
Tour? tour,
|
Tour? tour,
|
||||||
Map<String, double>? distances,
|
Map<String, double>? distances,
|
||||||
List<Payment>? paymentOptions,
|
List<Payment>? paymentOptions,
|
||||||
Map<String, List<String>>? sortingInformation
|
Map<String, List<String>>? sortingInformation,
|
||||||
|
int? pendingScanRequests,
|
||||||
}) {
|
}) {
|
||||||
return TourLoaded(
|
return TourLoaded(
|
||||||
tour: tour ?? this.tour,
|
tour: tour ?? this.tour,
|
||||||
distances: distances ?? this.distances,
|
distances: distances ?? this.distances,
|
||||||
paymentOptions: paymentOptions ?? this.paymentOptions,
|
paymentOptions: paymentOptions ?? this.paymentOptions,
|
||||||
sortingInformation: sortingInformation ?? this.sortingInformation
|
sortingInformation: sortingInformation ?? this.sortingInformation,
|
||||||
|
pendingScanRequests: pendingScanRequests ?? this.pendingScanRequests,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -94,8 +94,10 @@ class NoteBloc extends Bloc<NoteEvent, NoteState> {
|
|||||||
RemoveImageNote event,
|
RemoveImageNote event,
|
||||||
Emitter<NoteState> emit,
|
Emitter<NoteState> emit,
|
||||||
) async {
|
) async {
|
||||||
|
opBloc.add(StartOperation());
|
||||||
try {
|
try {
|
||||||
await repository.deleteImage(event.deliveryId, event.objectId);
|
await repository.deleteImage(event.deliveryId, event.objectId);
|
||||||
|
opBloc.add(FinishOperation());
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
debugPrint("Fehler beim Löschen des Bildes: $e $st");
|
debugPrint("Fehler beim Löschen des Bildes: $e $st");
|
||||||
_handleError(e, "Fehler beim Löschen des Bildes");
|
_handleError(e, "Fehler beim Löschen des Bildes");
|
||||||
@ -103,9 +105,11 @@ class NoteBloc extends Bloc<NoteEvent, NoteState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _upload(AddImageNote event, Emitter<NoteState> emit) async {
|
Future<void> _upload(AddImageNote event, Emitter<NoteState> emit) async {
|
||||||
|
opBloc.add(StartOperation());
|
||||||
try {
|
try {
|
||||||
Uint8List imageBytes = await event.file.readAsBytes();
|
Uint8List imageBytes = await event.file.readAsBytes();
|
||||||
await repository.addImage(event.deliveryId, imageBytes);
|
await repository.addImage(event.deliveryId, imageBytes);
|
||||||
|
opBloc.add(FinishOperation());
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
debugPrint("Fehler beim Hinzufügen des Bildes: $e $st");
|
debugPrint("Fehler beim Hinzufügen des Bildes: $e $st");
|
||||||
_handleError(e, "Fehler beim Hinzufügen des Bildes");
|
_handleError(e, "Fehler beim Hinzufügen des Bildes");
|
||||||
@ -113,6 +117,10 @@ class NoteBloc extends Bloc<NoteEvent, NoteState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _load(LoadNote event, Emitter<NoteState> emit) async {
|
Future<void> _load(LoadNote event, Emitter<NoteState> emit) async {
|
||||||
|
if (state is NoteLoaded || state is NoteLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
emit.call(NoteLoading());
|
emit.call(NoteLoading());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -130,8 +138,10 @@ class NoteBloc extends Bloc<NoteEvent, NoteState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _add(AddNote event, Emitter<NoteState> emit) async {
|
Future<void> _add(AddNote event, Emitter<NoteState> emit) async {
|
||||||
|
opBloc.add(StartOperation());
|
||||||
try {
|
try {
|
||||||
await repository.addNote(event.deliveryId, event.note);
|
await repository.addNote(event.deliveryId, event.note);
|
||||||
|
opBloc.add(FinishOperation());
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
debugPrint("Fehler beim Hinzufügen der Notiz: $e $st");
|
debugPrint("Fehler beim Hinzufügen der Notiz: $e $st");
|
||||||
_handleError(e, "Fehler beim Hinzufügen der Notiz");
|
_handleError(e, "Fehler beim Hinzufügen der Notiz");
|
||||||
@ -139,8 +149,10 @@ class NoteBloc extends Bloc<NoteEvent, NoteState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _edit(EditNote event, Emitter<NoteState> emit) async {
|
Future<void> _edit(EditNote event, Emitter<NoteState> emit) async {
|
||||||
|
opBloc.add(StartOperation());
|
||||||
try {
|
try {
|
||||||
await repository.editNote(event.noteId, event.content);
|
await repository.editNote(event.noteId, event.content);
|
||||||
|
opBloc.add(FinishOperation());
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
debugPrint("Fehler beim Editieren der Notiz: $e $st");
|
debugPrint("Fehler beim Editieren der Notiz: $e $st");
|
||||||
_handleError(e, "Fehler beim Editieren der Notiz");
|
_handleError(e, "Fehler beim Editieren der Notiz");
|
||||||
@ -148,8 +160,10 @@ class NoteBloc extends Bloc<NoteEvent, NoteState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _remove(RemoveNote event, Emitter<NoteState> emit) async {
|
Future<void> _remove(RemoveNote event, Emitter<NoteState> emit) async {
|
||||||
|
opBloc.add(StartOperation());
|
||||||
try {
|
try {
|
||||||
await repository.deleteNote(event.noteId);
|
await repository.deleteNote(event.noteId);
|
||||||
|
opBloc.add(FinishOperation());
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
debugPrint("Fehler beim Löschen der Notiz: $e $st");
|
debugPrint("Fehler beim Löschen der Notiz: $e $st");
|
||||||
_handleError(e, "Notiz konnte nicht gelöscht werden");
|
_handleError(e, "Notiz konnte nicht gelöscht werden");
|
||||||
|
|||||||
@ -91,8 +91,10 @@ class _ResetArticleAmountDialogState extends State<ResetArticleAmountDialog> {
|
|||||||
children: [
|
children: [
|
||||||
const Text("Wollen Sie die entfernten Artikel wieder hinzufügen?"),
|
const Text("Wollen Sie die entfernten Artikel wieder hinzufügen?"),
|
||||||
!widget.article.scannable ? _amountSelection() : Container(),
|
!widget.article.scannable ? _amountSelection() : Container(),
|
||||||
Row(
|
Wrap(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
spacing: 10,
|
||||||
|
runSpacing: 8,
|
||||||
|
alignment: WrapAlignment.spaceEvenly,
|
||||||
children: [
|
children: [
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: _reset,
|
onPressed: _reset,
|
||||||
|
|||||||
@ -154,8 +154,10 @@ class _ArticleUnscanDialogState extends State<ArticleUnscanDialog> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Row(
|
Wrap(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
spacing: 10,
|
||||||
|
runSpacing: 8,
|
||||||
|
alignment: WrapAlignment.spaceAround,
|
||||||
children: [
|
children: [
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: isValidText ? _unscan : null,
|
onPressed: isValidText ? _unscan : null,
|
||||||
|
|||||||
@ -142,66 +142,70 @@ class _DeliveryDetailState extends State<DeliveryDetail> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _stepsNavigation(Delivery delivery) {
|
Widget _stepsNavigation(Delivery delivery) {
|
||||||
return SizedBox(
|
return SafeArea(
|
||||||
width: double.infinity,
|
top: false,
|
||||||
height: 90,
|
child: SizedBox(
|
||||||
child: Row(
|
width: double.infinity,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
height: 90,
|
||||||
children: [
|
child: Row(
|
||||||
OutlinedButton(
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
onPressed: _step == 0 ? null : _clickBack,
|
children: [
|
||||||
child: const Text("zurück"),
|
OutlinedButton(
|
||||||
),
|
onPressed: _step == 0 ? null : _clickBack,
|
||||||
Padding(
|
child: const Text("zurück"),
|
||||||
padding: const EdgeInsets.only(left: 20),
|
|
||||||
child: FilledButton(
|
|
||||||
onPressed: () {
|
|
||||||
if (_step == _steps.length - 1) {
|
|
||||||
_openSignatureView(delivery);
|
|
||||||
} else {
|
|
||||||
_clickForward();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child:
|
|
||||||
_step == _steps.length - 1
|
|
||||||
? const Text("Unterschreiben")
|
|
||||||
: const Text("weiter"),
|
|
||||||
),
|
),
|
||||||
),
|
Padding(
|
||||||
],
|
padding: const EdgeInsets.only(left: 20),
|
||||||
|
child: FilledButton(
|
||||||
|
onPressed: () {
|
||||||
|
if (_step == _steps.length - 1) {
|
||||||
|
_openSignatureView(delivery);
|
||||||
|
} else {
|
||||||
|
_clickForward();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child:
|
||||||
|
_step == _steps.length - 1
|
||||||
|
? const Text("Unterschreiben")
|
||||||
|
: const Text("weiter"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return BlocBuilder<TourBloc, TourState>(
|
||||||
appBar: AppBar(title: const Text("Auslieferungsdetails")),
|
builder: (context, state) {
|
||||||
body: BlocBuilder<TourBloc, TourState>(
|
Delivery? delivery;
|
||||||
builder: (context, state) {
|
if (state is TourLoaded) {
|
||||||
final currentState = state;
|
delivery = state.tour.deliveries.firstWhere(
|
||||||
|
(d) => d.id == widget.deliveryId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (currentState is TourLoaded) {
|
return Scaffold(
|
||||||
Delivery delivery = currentState.tour.deliveries.firstWhere(
|
appBar: AppBar(title: const Text("Auslieferungsdetails")),
|
||||||
(delivery) => delivery.id == widget.deliveryId,
|
body: delivery == null
|
||||||
);
|
? const Center(child: CircularProgressIndicator())
|
||||||
return Column(
|
: Column(
|
||||||
children: [
|
children: [
|
||||||
_stepInfo(),
|
_stepInfo(),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
Expanded(
|
Expanded(
|
||||||
child:
|
child:
|
||||||
StepFactory().make(_step, delivery) ??
|
StepFactory().make(_step, delivery) ??
|
||||||
_stepMissingWarning(),
|
_stepMissingWarning(),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
_stepsNavigation(delivery),
|
bottomNavigationBar:
|
||||||
],
|
delivery == null ? null : _stepsNavigation(delivery),
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
|
|
||||||
return const Center(child: CircularProgressIndicator());
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -195,17 +195,16 @@ class _DeliveryDiscountState extends State<DeliveryDiscount> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Row(
|
Wrap(
|
||||||
|
spacing: 10,
|
||||||
|
runSpacing: 8,
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
FilledButton(
|
||||||
padding: const EdgeInsets.only(right: 10),
|
onPressed:
|
||||||
child: FilledButton(
|
!_isReasonEmpty && _discountValue > 0
|
||||||
onPressed:
|
? _updateValues
|
||||||
!_isReasonEmpty && _discountValue > 0
|
: null,
|
||||||
? _updateValues
|
child: const Text("Speichern"),
|
||||||
: null,
|
|
||||||
child: const Text("Speichern"),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onPressed: _discountValue > 0 && widget.discount != null ? _resetValues : null,
|
onPressed: _discountValue > 0 && widget.discount != null ? _resetValues : null,
|
||||||
|
|||||||
@ -9,6 +9,8 @@ import 'package:hl_lieferservice/model/delivery.dart';
|
|||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:signature/signature.dart';
|
import 'package:signature/signature.dart';
|
||||||
|
|
||||||
|
enum _SigningPhase { customerAcceptance, customerSignature, driverSignature }
|
||||||
|
|
||||||
class SignatureView extends StatefulWidget {
|
class SignatureView extends StatefulWidget {
|
||||||
const SignatureView({
|
const SignatureView({
|
||||||
super.key,
|
super.key,
|
||||||
@ -43,33 +45,11 @@ class _SignatureViewState extends State<SignatureView> {
|
|||||||
exportBackgroundColor: Colors.white,
|
exportBackgroundColor: Colors.white,
|
||||||
);
|
);
|
||||||
|
|
||||||
bool _isDriverSigning = false;
|
_SigningPhase _phase = _SigningPhase.customerAcceptance;
|
||||||
bool _customerAccepted = false;
|
|
||||||
bool _noteAccepted = false;
|
|
||||||
bool _notesEmpty = true;
|
|
||||||
bool _isCustomerSignatureEmpty = true;
|
|
||||||
bool _isDriverSignatureEmpty = true;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
_customerController.addListener(() {
|
|
||||||
if (_isCustomerSignatureEmpty != _customerController.isEmpty) {
|
|
||||||
setState(() {
|
|
||||||
_isCustomerSignatureEmpty = _customerController.isEmpty;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
_driverController.addListener(() {
|
|
||||||
if (_isDriverSignatureEmpty != _driverController.isEmpty) {
|
|
||||||
setState(() {
|
|
||||||
_isDriverSignatureEmpty = _driverController.isEmpty;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
context.read<NoteBloc>().add(LoadNote(delivery: widget.delivery));
|
context.read<NoteBloc>().add(LoadNote(delivery: widget.delivery));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,14 +60,88 @@ class _SignatureViewState extends State<SignatureView> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _signatureField() {
|
void _onAcceptanceDone() {
|
||||||
return Signature(
|
setState(() => _phase = _SigningPhase.customerSignature);
|
||||||
controller: _isDriverSigning ? _driverController : _customerController,
|
}
|
||||||
backgroundColor: Colors.white,
|
|
||||||
|
void _onCustomerSigned() {
|
||||||
|
setState(() => _phase = _SigningPhase.driverSignature);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onDriverSigned() async {
|
||||||
|
widget.onSigned(
|
||||||
|
(await _customerController.toPngBytes())!,
|
||||||
|
(await _driverController.toPngBytes())!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _notes() {
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return switch (_phase) {
|
||||||
|
_SigningPhase.customerAcceptance => _AcceptanceStep(
|
||||||
|
onContinue: _onAcceptanceDone,
|
||||||
|
),
|
||||||
|
_SigningPhase.customerSignature => _SignaturePadStep(
|
||||||
|
controller: _customerController,
|
||||||
|
delivery: widget.delivery,
|
||||||
|
appBarTitle: "Unterschrift des Kunden",
|
||||||
|
buttonLabel: "Weiter",
|
||||||
|
onContinue: _onCustomerSigned,
|
||||||
|
),
|
||||||
|
_SigningPhase.driverSignature => _SignaturePadStep(
|
||||||
|
controller: _driverController,
|
||||||
|
delivery: widget.delivery,
|
||||||
|
appBarTitle: "Unterschrift des Fahrers",
|
||||||
|
buttonLabel: "Absenden",
|
||||||
|
onContinue: _onDriverSigned,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AcceptanceStep extends StatefulWidget {
|
||||||
|
const _AcceptanceStep({required this.onContinue});
|
||||||
|
|
||||||
|
final VoidCallback onContinue;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_AcceptanceStep> createState() => _AcceptanceStepState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AcceptanceStepState extends State<_AcceptanceStep> {
|
||||||
|
bool _customerAccepted = false;
|
||||||
|
bool _noteAccepted = false;
|
||||||
|
|
||||||
|
Widget _notesContent(NoteState noteState) {
|
||||||
|
if (noteState is! NoteLoaded) {
|
||||||
|
return const SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: Center(child: CircularProgressIndicator()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (noteState.notes.isEmpty) {
|
||||||
|
return const SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: Center(child: Text("Keine Notizen vorhanden")),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return ListView.separated(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return ListTile(
|
||||||
|
leading: const Icon(Icons.event_note_outlined),
|
||||||
|
title: Text(noteState.notes[index].content),
|
||||||
|
contentPadding: const EdgeInsets.all(20),
|
||||||
|
tileColor: Theme.of(context).colorScheme.onSecondary,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
separatorBuilder: (context, index) => const Divider(height: 0),
|
||||||
|
itemCount: noteState.notes.length,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _notes(NoteState noteState) {
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@ -98,163 +152,171 @@ class _SignatureViewState extends State<SignatureView> {
|
|||||||
style: Theme.of(context).textTheme.headlineSmall,
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
BlocConsumer<NoteBloc, NoteState>(
|
_notesContent(noteState),
|
||||||
listener: (context, state) {
|
|
||||||
final current = state;
|
|
||||||
if (current is NoteLoaded) {
|
|
||||||
setState(() {
|
|
||||||
_notesEmpty = current.notes.isEmpty;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (current is NoteLoadedBase) {
|
|
||||||
setState(() {
|
|
||||||
_notesEmpty = current.notes.isEmpty;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
builder: (context, state) {
|
|
||||||
final current = state;
|
|
||||||
|
|
||||||
if (current is NoteLoaded) {
|
|
||||||
if (current.notes.isEmpty) {
|
|
||||||
return const SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: Center(child: Text("Keine Notizen vorhanden")),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ListView.separated(
|
|
||||||
shrinkWrap: true,
|
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
return ListTile(
|
|
||||||
leading: const Icon(Icons.event_note_outlined),
|
|
||||||
title: Text(current.notes[index].content),
|
|
||||||
contentPadding: const EdgeInsets.all(20),
|
|
||||||
tileColor: Theme.of(context).colorScheme.onSecondary,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
separatorBuilder: (context, index) => const Divider(height: 0),
|
|
||||||
itemCount: current.notes.length,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return const SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: Center(child: CircularProgressIndicator()),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
|
|
||||||
const Padding(padding: EdgeInsets.only(top: 25), child: Divider()),
|
const Padding(padding: EdgeInsets.only(top: 25), child: Divider()),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _customerCheckboxes() {
|
@override
|
||||||
return !_isDriverSigning
|
Widget build(BuildContext context) {
|
||||||
? Column(
|
return BlocBuilder<NoteBloc, NoteState>(
|
||||||
children: [
|
builder: (context, noteState) {
|
||||||
Padding(
|
final notesEmpty = switch (noteState) {
|
||||||
padding: const EdgeInsets.only(top: 25, bottom: 0),
|
NoteLoadedBase(notes: final ns) => ns.isEmpty,
|
||||||
child: _notes(),
|
_ => true,
|
||||||
),
|
};
|
||||||
Padding(
|
final isButtonEnabled =
|
||||||
padding: const EdgeInsets.only(top: 25.0, bottom: 0),
|
_customerAccepted && (_noteAccepted || notesEmpty);
|
||||||
child: Row(
|
|
||||||
children: [
|
return Scaffold(
|
||||||
Checkbox(
|
appBar: AppBar(title: const Text("Unterschrift des Kunden")),
|
||||||
value: _noteAccepted,
|
body: Padding(
|
||||||
onChanged:
|
padding: const EdgeInsets.all(20.0),
|
||||||
_notesEmpty
|
child: ListView(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 25, bottom: 0),
|
||||||
|
child: _notes(noteState),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 25.0, bottom: 0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Checkbox(
|
||||||
|
value: _noteAccepted,
|
||||||
|
onChanged: notesEmpty
|
||||||
? null
|
? null
|
||||||
: (value) {
|
: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_noteAccepted = value!;
|
_noteAccepted = value!;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
|
||||||
Flexible(
|
|
||||||
child: InkWell(
|
|
||||||
onTap: _notesEmpty ? null : () {
|
|
||||||
setState(() {
|
|
||||||
_noteAccepted = !_noteAccepted;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
child: Text(
|
|
||||||
"Ich nehme die oben genannten Anmerkungen zur Lieferung zur Kenntnis.",
|
|
||||||
overflow: TextOverflow.fade,
|
|
||||||
),
|
),
|
||||||
),
|
Flexible(
|
||||||
|
child: InkWell(
|
||||||
|
onTap: notesEmpty
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
setState(() {
|
||||||
|
_noteAccepted = !_noteAccepted;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
"Ich nehme die oben genannten Anmerkungen zur Lieferung zur Kenntnis.",
|
||||||
|
overflow: TextOverflow.fade,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 25.0, bottom: 10.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Checkbox(
|
||||||
|
value: _customerAccepted,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_customerAccepted = value!;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Flexible(
|
||||||
|
child: InkWell(
|
||||||
|
child: Text(
|
||||||
|
"Ware in ordnungsgemäßem Zustand erhalten. Aufstell- und Einbauarbeiten wurden korrekt durchgeführt",
|
||||||
|
overflow: TextOverflow.fade,
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
setState(() {
|
||||||
|
_customerAccepted = !_customerAccepted;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
bottomNavigationBar: SafeArea(
|
||||||
|
top: false,
|
||||||
|
child: SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 90,
|
||||||
|
child: Center(
|
||||||
|
child: FilledButton(
|
||||||
|
onPressed: isButtonEnabled ? widget.onContinue : null,
|
||||||
|
child: const Text("Unterschreiben"),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
),
|
||||||
padding: const EdgeInsets.only(top: 25.0, bottom: 10.0),
|
);
|
||||||
child: Row(
|
},
|
||||||
children: [
|
);
|
||||||
Checkbox(
|
}
|
||||||
value: _customerAccepted,
|
}
|
||||||
onChanged: (value) {
|
|
||||||
setState(() {
|
class _SignaturePadStep extends StatefulWidget {
|
||||||
_customerAccepted = value!;
|
const _SignaturePadStep({
|
||||||
});
|
required this.controller,
|
||||||
},
|
required this.delivery,
|
||||||
),
|
required this.appBarTitle,
|
||||||
Flexible(
|
required this.buttonLabel,
|
||||||
child: InkWell(
|
required this.onContinue,
|
||||||
child: Text(
|
});
|
||||||
"Ware in ordnungsgemäßem Zustand erhalten. Aufstell- und Einbauarbeiten wurden korrekt durchgeführt",
|
|
||||||
overflow: TextOverflow.fade,
|
final SignatureController controller;
|
||||||
),
|
final Delivery delivery;
|
||||||
onTap: () {
|
final String appBarTitle;
|
||||||
setState(() {
|
final String buttonLabel;
|
||||||
_customerAccepted = !_customerAccepted;
|
final VoidCallback onContinue;
|
||||||
});
|
|
||||||
},
|
@override
|
||||||
),
|
State<_SignaturePadStep> createState() => _SignaturePadStepState();
|
||||||
),
|
}
|
||||||
],
|
|
||||||
),
|
class _SignaturePadStepState extends State<_SignaturePadStep> {
|
||||||
),
|
bool _isEmpty = true;
|
||||||
],
|
late final VoidCallback _listener;
|
||||||
)
|
|
||||||
: Container();
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_isEmpty = widget.controller.isEmpty;
|
||||||
|
_listener = () {
|
||||||
|
if (_isEmpty != widget.controller.isEmpty) {
|
||||||
|
setState(() {
|
||||||
|
_isEmpty = widget.controller.isEmpty;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
widget.controller.addListener(_listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
widget.controller.removeListener(_listener);
|
||||||
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
String formattedDate = DateFormat("dd.MM.yyyy").format(DateTime.now());
|
final formattedDate = DateFormat("dd.MM.yyyy").format(DateTime.now());
|
||||||
|
|
||||||
bool isButtonEnabled;
|
|
||||||
if (!_isDriverSigning) {
|
|
||||||
isButtonEnabled =
|
|
||||||
_customerAccepted &&
|
|
||||||
(_noteAccepted || _notesEmpty) &&
|
|
||||||
!_isCustomerSignatureEmpty;
|
|
||||||
} else {
|
|
||||||
isButtonEnabled = !_isDriverSignatureEmpty;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(title: Text(widget.appBarTitle)),
|
||||||
title:
|
|
||||||
!_isDriverSigning
|
|
||||||
? const Text("Unterschrift des Kunden")
|
|
||||||
: const Text("Unterschrift des Fahrers"),
|
|
||||||
),
|
|
||||||
body: Padding(
|
body: Padding(
|
||||||
padding: const EdgeInsets.all(20.0),
|
padding: const EdgeInsets.all(20.0),
|
||||||
child: ListView(
|
child: ListView(
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height:
|
height: MediaQuery.of(context).size.height * 0.75,
|
||||||
MediaQuery.of(context).size.height *
|
|
||||||
(_isDriverSigning ? 0.75 : 0.5),
|
|
||||||
child: DecoratedBox(
|
child: DecoratedBox(
|
||||||
decoration: const BoxDecoration(color: Colors.white),
|
decoration: const BoxDecoration(color: Colors.white),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@ -272,7 +334,12 @@ class _SignatureViewState extends State<SignatureView> {
|
|||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(child: _signatureField()),
|
Expanded(
|
||||||
|
child: Signature(
|
||||||
|
controller: widget.controller,
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -285,36 +352,22 @@ class _SignatureViewState extends State<SignatureView> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
_customerCheckboxes(),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 25.0, bottom: 25.0),
|
|
||||||
child: Center(
|
|
||||||
child: FilledButton(
|
|
||||||
onPressed:
|
|
||||||
isButtonEnabled
|
|
||||||
? () async {
|
|
||||||
if (!_isDriverSigning) {
|
|
||||||
setState(() {
|
|
||||||
_isDriverSigning = true;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
widget.onSigned(
|
|
||||||
(await _customerController.toPngBytes())!,
|
|
||||||
(await _driverController.toPngBytes())!,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
child:
|
|
||||||
!_isDriverSigning
|
|
||||||
? const Text("Weiter")
|
|
||||||
: const Text("Absenden"),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
bottomNavigationBar: SafeArea(
|
||||||
|
top: false,
|
||||||
|
child: SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 90,
|
||||||
|
child: Center(
|
||||||
|
child: FilledButton(
|
||||||
|
onPressed: _isEmpty ? null : widget.onContinue,
|
||||||
|
child: Text(widget.buttonLabel),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -51,6 +51,10 @@ class _NoteAddDialogState extends State<NoteAddDialog> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Dialog(
|
return Dialog(
|
||||||
|
// Default Dialog.insetPadding eats 80 dp horizontally on phones, leaving
|
||||||
|
// too little room for two side-by-side buttons on narrow devices like
|
||||||
|
// the Samsung A16F. Shrinking the inset gives back ~64 dp.
|
||||||
|
insetPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 24),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: MediaQuery.of(context).size.width,
|
width: MediaQuery.of(context).size.width,
|
||||||
height: MediaQuery.of(context).size.height * 0.6,
|
height: MediaQuery.of(context).size.height * 0.6,
|
||||||
@ -115,8 +119,9 @@ class _NoteAddDialogState extends State<NoteAddDialog> {
|
|||||||
maxLines: 10,
|
maxLines: 10,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Row(
|
Wrap(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
spacing: 10,
|
||||||
|
runSpacing: 8,
|
||||||
children: [
|
children: [
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed:
|
onPressed:
|
||||||
@ -126,15 +131,12 @@ class _NoteAddDialogState extends State<NoteAddDialog> {
|
|||||||
: null,
|
: null,
|
||||||
child: const Text("Hinzufügen"),
|
child: const Text("Hinzufügen"),
|
||||||
),
|
),
|
||||||
Padding(
|
OutlinedButton(
|
||||||
padding: const EdgeInsets.only(left: 10.0),
|
onPressed: () {
|
||||||
child: OutlinedButton(
|
_noteController.clear();
|
||||||
onPressed: () {
|
_noteSelectionController.clear();
|
||||||
_noteController.clear();
|
},
|
||||||
_noteSelectionController.clear();
|
child: const Text("Zurücksetzen"),
|
||||||
},
|
|
||||||
child: const Text("Zurücksetzen"),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@ -11,12 +11,12 @@ import '../../detail/service/notes_service.dart';
|
|||||||
|
|
||||||
class DeliveryListItem extends StatelessWidget {
|
class DeliveryListItem extends StatelessWidget {
|
||||||
final Delivery delivery;
|
final Delivery delivery;
|
||||||
final double distance;
|
final double? distance;
|
||||||
|
|
||||||
const DeliveryListItem({
|
const DeliveryListItem({
|
||||||
super.key,
|
super.key,
|
||||||
required this.delivery,
|
required this.delivery,
|
||||||
required this.distance,
|
this.distance,
|
||||||
});
|
});
|
||||||
|
|
||||||
void _goToDelivery(BuildContext context) {
|
void _goToDelivery(BuildContext context) {
|
||||||
@ -59,11 +59,14 @@ class DeliveryListItem extends StatelessWidget {
|
|||||||
"Pausiert",
|
"Pausiert",
|
||||||
);
|
);
|
||||||
case DeliveryState.ongoing:
|
case DeliveryState.ongoing:
|
||||||
|
final distanceLabel = distance != null && !distance!.isNaN
|
||||||
|
? "${distance!.toStringAsFixed(1)} km"
|
||||||
|
: "–";
|
||||||
return (
|
return (
|
||||||
Theme.of(context).colorScheme.surfaceContainerLow,
|
Theme.of(context).colorScheme.surfaceContainerLow,
|
||||||
Colors.transparent,
|
Colors.transparent,
|
||||||
Icons.local_shipping_outlined,
|
Icons.local_shipping_outlined,
|
||||||
"${distance.toStringAsFixed(1)} km",
|
distanceLabel,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -43,7 +43,7 @@ class _DeliveryListState extends State<DeliveryList> {
|
|||||||
|
|
||||||
return DeliveryListItem(
|
return DeliveryListItem(
|
||||||
delivery: delivery,
|
delivery: delivery,
|
||||||
distance: distances[delivery.id] ?? 0.0,
|
distance: distances[delivery.id],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
itemCount: sortingInformation.length,
|
itemCount: sortingInformation.length,
|
||||||
@ -114,7 +114,7 @@ class _DeliveryListState extends State<DeliveryList> {
|
|||||||
itemCount: sorted.length,
|
itemCount: sorted.length,
|
||||||
itemBuilder: (context, index) => DeliveryListItem(
|
itemBuilder: (context, index) => DeliveryListItem(
|
||||||
delivery: sorted[index],
|
delivery: sorted[index],
|
||||||
distance: currentState.distances?[sorted[index].id] ?? 0.0,
|
distance: currentState.distances?[sorted[index].id],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,11 +18,9 @@ class DeliveryOverview extends StatefulWidget {
|
|||||||
const DeliveryOverview({
|
const DeliveryOverview({
|
||||||
super.key,
|
super.key,
|
||||||
required this.tour,
|
required this.tour,
|
||||||
required this.distances,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
final Tour tour;
|
final Tour tour;
|
||||||
final Map<String, double> distances;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<StatefulWidget> createState() => _DeliveryOverviewState();
|
State<StatefulWidget> createState() => _DeliveryOverviewState();
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import 'package:hl_lieferservice/feature/car_selection/bloc/bloc.dart';
|
|||||||
import 'package:hl_lieferservice/feature/car_selection/bloc/state.dart';
|
import 'package:hl_lieferservice/feature/car_selection/bloc/state.dart';
|
||||||
import 'package:hl_lieferservice/feature/delivery/overview/presentation/delivery_fail_page.dart';
|
import 'package:hl_lieferservice/feature/delivery/overview/presentation/delivery_fail_page.dart';
|
||||||
import 'package:hl_lieferservice/feature/delivery/overview/presentation/delivery_overview.dart';
|
import 'package:hl_lieferservice/feature/delivery/overview/presentation/delivery_overview.dart';
|
||||||
|
import 'package:hl_lieferservice/model/tour.dart';
|
||||||
import '../../bloc/tour_bloc.dart';
|
import '../../bloc/tour_bloc.dart';
|
||||||
import '../../bloc/tour_state.dart';
|
import '../../bloc/tour_state.dart';
|
||||||
|
|
||||||
@ -16,6 +16,36 @@ class DeliveryOverviewPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _DeliveryOverviewPageState extends State<DeliveryOverviewPage> {
|
class _DeliveryOverviewPageState extends State<DeliveryOverviewPage> {
|
||||||
|
Widget _buildOverviewWithBanner({
|
||||||
|
required Tour tour,
|
||||||
|
required String bannerText,
|
||||||
|
}) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Material(
|
||||||
|
color: Colors.amber.shade100,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const SizedBox(
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(child: Text(bannerText)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: DeliveryOverview(tour: tour),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final carState = context.watch<CarSelectBloc>().state;
|
final carState = context.watch<CarSelectBloc>().state;
|
||||||
@ -54,10 +84,13 @@ class _DeliveryOverviewPageState extends State<DeliveryOverviewPage> {
|
|||||||
body: BlocBuilder<TourBloc, TourState>(
|
body: BlocBuilder<TourBloc, TourState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
if (state is TourLoaded) {
|
if (state is TourLoaded) {
|
||||||
return DeliveryOverview(
|
if (state.distances == null) {
|
||||||
tour: state.tour,
|
return _buildOverviewWithBanner(
|
||||||
distances: state.distances ?? {},
|
tour: state.tour,
|
||||||
);
|
bannerText: "Berechne Distanzen…",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return DeliveryOverview(tour: state.tour);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state is TourLoadingFailed) {
|
if (state is TourLoadingFailed) {
|
||||||
|
|||||||
@ -92,6 +92,48 @@ class TourRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Scan a single BOM component locally. The server-side `scanArticle` call
|
||||||
|
/// for the parent article is deferred until **every** component of the
|
||||||
|
/// parent is fully scanned — only then does the parent count as loaded.
|
||||||
|
Future<ScanResult> scanComponent(
|
||||||
|
String deliveryId,
|
||||||
|
String carId,
|
||||||
|
String componentArticleNumber,
|
||||||
|
) async {
|
||||||
|
if (!_tourStream.hasValue) {
|
||||||
|
throw TourNotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
final tour = _tourStream.value!;
|
||||||
|
final delivery = tour.deliveries.firstWhere(
|
||||||
|
(d) => d.id == deliveryId,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Locate the parent article and the matching component.
|
||||||
|
final parentArticle = delivery.findParentOfComponent(
|
||||||
|
componentArticleNumber,
|
||||||
|
);
|
||||||
|
if (parentArticle == null) return ScanResult.notFound;
|
||||||
|
|
||||||
|
final component = parentArticle.findComponent(componentArticleNumber)!;
|
||||||
|
|
||||||
|
if (component.isFullyScanned) return ScanResult.alreadyScanned;
|
||||||
|
|
||||||
|
// ── Local-only increment ──
|
||||||
|
component.scannedAmount += 1;
|
||||||
|
|
||||||
|
// ── When every component is done, sync the parent with the server ──
|
||||||
|
if (parentArticle.isFullyScanned) {
|
||||||
|
await service.scanArticle(parentArticle.internalId.toString());
|
||||||
|
parentArticle.scannedAmount += 1;
|
||||||
|
delivery.carId = int.tryParse(carId) ?? delivery.carId;
|
||||||
|
await service.assignCar(deliveryId, carId);
|
||||||
|
}
|
||||||
|
|
||||||
|
_tourStream.add(tour);
|
||||||
|
return ScanResult.scanned;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> unscan(
|
Future<void> unscan(
|
||||||
String deliveryId,
|
String deliveryId,
|
||||||
String articleId,
|
String articleId,
|
||||||
|
|||||||
@ -13,6 +13,7 @@ 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_bloc.dart';
|
||||||
import 'package:hl_lieferservice/feature/settings/bloc/settings_state.dart';
|
import 'package:hl_lieferservice/feature/settings/bloc/settings_state.dart';
|
||||||
import 'package:hl_lieferservice/model/article.dart';
|
import 'package:hl_lieferservice/model/article.dart';
|
||||||
|
import 'package:hl_lieferservice/model/component.dart';
|
||||||
import 'package:hl_lieferservice/model/delivery.dart';
|
import 'package:hl_lieferservice/model/delivery.dart';
|
||||||
import 'package:hl_lieferservice/model/tour.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_bloc.dart';
|
||||||
@ -24,37 +25,42 @@ import 'package:hl_lieferservice/widget/operations/bloc/operation_event.dart';
|
|||||||
// Data helpers
|
// Data helpers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
class _ArticleDeliveryEntry {
|
class _DeliveryGroup {
|
||||||
final Delivery delivery;
|
final Delivery delivery;
|
||||||
final Article article;
|
|
||||||
final String? carPlate;
|
final String? carPlate;
|
||||||
|
final List<Article> articles;
|
||||||
|
|
||||||
const _ArticleDeliveryEntry({
|
const _DeliveryGroup({
|
||||||
required this.delivery,
|
required this.delivery,
|
||||||
required this.article,
|
required this.articles,
|
||||||
this.carPlate,
|
this.carPlate,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
class _ArticleGroup {
|
int get totalArticles => articles.length;
|
||||||
final String articleNumber;
|
|
||||||
final String name;
|
|
||||||
final int totalAmount;
|
|
||||||
final int totalScanned;
|
|
||||||
final int totalRemoved;
|
|
||||||
final List<_ArticleDeliveryEntry> entries;
|
|
||||||
|
|
||||||
bool get isComplete => totalScanned + totalRemoved >= totalAmount;
|
int get completeArticles => articles
|
||||||
int get scannedOrRemoved => totalScanned + totalRemoved;
|
.where((a) => a.isFullyScanned)
|
||||||
|
.length;
|
||||||
|
|
||||||
const _ArticleGroup({
|
int get totalUnits => articles.fold(0, (sum, a) {
|
||||||
required this.articleNumber,
|
if (a.isParent && a.components.isNotEmpty) {
|
||||||
required this.name,
|
return sum + a.components.fold(0, (s, c) => s + c.requiredAmount);
|
||||||
required this.totalAmount,
|
}
|
||||||
required this.totalScanned,
|
return sum + a.amount;
|
||||||
required this.totalRemoved,
|
});
|
||||||
required this.entries,
|
|
||||||
});
|
int get scannedUnits => articles.fold(0, (sum, a) {
|
||||||
|
if (a.isParent && a.components.isNotEmpty) {
|
||||||
|
return sum + a.components.fold(0, (s, c) => s + c.scannedAmount);
|
||||||
|
}
|
||||||
|
return sum + a.scannedAmount + a.scannedRemovedAmount;
|
||||||
|
});
|
||||||
|
|
||||||
|
bool get isComplete => totalArticles > 0 && completeArticles == totalArticles;
|
||||||
|
|
||||||
|
bool get hasAnyScanned => scannedUnits > 0;
|
||||||
|
|
||||||
|
bool get isPartial => hasAnyScanned && !isComplete;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@ -128,6 +134,8 @@ class _ScanPageState extends State<ScanPage> with SingleTickerProviderStateMixin
|
|||||||
/// `<artikelnummer>;<kundennummer>;<belegnummer>`.
|
/// `<artikelnummer>;<kundennummer>;<belegnummer>`.
|
||||||
/// Liefert `null`, wenn der Barcode dem erwarteten Format nicht entspricht.
|
/// Liefert `null`, wenn der Barcode dem erwarteten Format nicht entspricht.
|
||||||
String? _extractArticleNumber(String barcode) {
|
String? _extractArticleNumber(String barcode) {
|
||||||
|
debugPrint("QR CODE: $barcode");
|
||||||
|
|
||||||
final parts = barcode.split(';');
|
final parts = barcode.split(';');
|
||||||
if (parts.length != 3) return null;
|
if (parts.length != 3) return null;
|
||||||
final articleNumber = parts[0].trim();
|
final articleNumber = parts[0].trim();
|
||||||
@ -156,10 +164,43 @@ class _ScanPageState extends State<ScanPage> with SingleTickerProviderStateMixin
|
|||||||
final tourState = context.read<TourBloc>().state;
|
final tourState = context.read<TourBloc>().state;
|
||||||
if (tourState is! TourLoaded) return;
|
if (tourState is! TourLoaded) return;
|
||||||
|
|
||||||
|
// ── 1. Try component match first (Stückliste) ──
|
||||||
|
final componentDeliveries = tourState.tour.deliveries
|
||||||
|
.where((d) => d.state != DeliveryState.finished)
|
||||||
|
.where((d) {
|
||||||
|
final parent = d.findParentOfComponent(articleNumber);
|
||||||
|
if (parent == null) return false;
|
||||||
|
final comp = parent.findComponent(articleNumber);
|
||||||
|
return comp != null && !comp.isFullyScanned;
|
||||||
|
})
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (componentDeliveries.isNotEmpty) {
|
||||||
|
if (componentDeliveries.length == 1) {
|
||||||
|
setState(() => _isScanning = true);
|
||||||
|
context.read<TourBloc>().add(ScanComponentEvent(
|
||||||
|
componentArticleNumber: articleNumber,
|
||||||
|
carId: _selectedCarId!.toString(),
|
||||||
|
deliveryId: componentDeliveries.first.id,
|
||||||
|
));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_showCustomerSelectionSheet(
|
||||||
|
articleNumber,
|
||||||
|
componentDeliveries,
|
||||||
|
tourState.tour,
|
||||||
|
isComponent: true,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 2. Regular article scan ──
|
||||||
final needingDeliveries = tourState.tour.deliveries
|
final needingDeliveries = tourState.tour.deliveries
|
||||||
.where((d) => d.state != DeliveryState.finished)
|
.where((d) => d.state != DeliveryState.finished)
|
||||||
.where((d) => d.articles.any((a) =>
|
.where((d) => d.articles.any((a) =>
|
||||||
a.articleNumber == articleNumber &&
|
a.articleNumber == articleNumber &&
|
||||||
|
!a.isParent &&
|
||||||
a.scannedAmount + a.scannedRemovedAmount < a.amount))
|
a.scannedAmount + a.scannedRemovedAmount < a.amount))
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
@ -189,8 +230,9 @@ class _ScanPageState extends State<ScanPage> with SingleTickerProviderStateMixin
|
|||||||
void _showCustomerSelectionSheet(
|
void _showCustomerSelectionSheet(
|
||||||
String articleNumber,
|
String articleNumber,
|
||||||
List<Delivery> deliveries,
|
List<Delivery> deliveries,
|
||||||
Tour tour,
|
Tour tour, {
|
||||||
) {
|
bool isComponent = false,
|
||||||
|
}) {
|
||||||
final tourBloc = context.read<TourBloc>();
|
final tourBloc = context.read<TourBloc>();
|
||||||
final carId = _selectedCarId!;
|
final carId = _selectedCarId!;
|
||||||
|
|
||||||
@ -244,11 +286,19 @@ class _ScanPageState extends State<ScanPage> with SingleTickerProviderStateMixin
|
|||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.pop(ctx);
|
Navigator.pop(ctx);
|
||||||
setState(() => _isScanning = true);
|
setState(() => _isScanning = true);
|
||||||
tourBloc.add(ScanArticleEvent(
|
if (isComponent) {
|
||||||
articleNumber: articleNumber,
|
tourBloc.add(ScanComponentEvent(
|
||||||
carId: carId.toString(),
|
componentArticleNumber: articleNumber,
|
||||||
deliveryId: delivery.id,
|
carId: carId.toString(),
|
||||||
));
|
deliveryId: delivery.id,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
tourBloc.add(ScanArticleEvent(
|
||||||
|
articleNumber: articleNumber,
|
||||||
|
carId: carId.toString(),
|
||||||
|
deliveryId: delivery.id,
|
||||||
|
));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
@ -269,37 +319,23 @@ class _ScanPageState extends State<ScanPage> with SingleTickerProviderStateMixin
|
|||||||
return tour.driver.cars.firstWhereOrNull((c) => c.id == carId)?.plate;
|
return tour.driver.cars.firstWhereOrNull((c) => c.id == carId)?.plate;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<_ArticleGroup> _buildArticleGroups(Tour tour) {
|
List<_DeliveryGroup> _buildDeliveryGroups(Tour tour) {
|
||||||
final Map<String, List<_ArticleDeliveryEntry>> grouped = {};
|
final List<_DeliveryGroup> groups = [];
|
||||||
|
|
||||||
for (final delivery in tour.deliveries) {
|
for (final delivery in tour.deliveries) {
|
||||||
if (delivery.state == DeliveryState.finished) continue;
|
if (delivery.state == DeliveryState.finished) continue;
|
||||||
final carPlate = _lookupCarPlate(delivery.carId, tour);
|
final scannableArticles =
|
||||||
for (final article in delivery.articles) {
|
delivery.articles.where((a) => a.scannable).toList();
|
||||||
if (!article.scannable) continue;
|
if (scannableArticles.isEmpty) continue;
|
||||||
grouped.putIfAbsent(article.articleNumber, () => []);
|
|
||||||
grouped[article.articleNumber]!.add(
|
groups.add(_DeliveryGroup(
|
||||||
_ArticleDeliveryEntry(
|
delivery: delivery,
|
||||||
delivery: delivery,
|
articles: scannableArticles,
|
||||||
article: article,
|
carPlate: _lookupCarPlate(delivery.carId, tour),
|
||||||
carPlate: carPlate,
|
));
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return grouped.entries.map((e) {
|
return groups;
|
||||||
final entries = e.value;
|
|
||||||
return _ArticleGroup(
|
|
||||||
articleNumber: e.key,
|
|
||||||
name: entries.first.article.name,
|
|
||||||
totalAmount: entries.fold(0, (sum, e) => sum + e.article.amount),
|
|
||||||
totalScanned: entries.fold(0, (sum, e) => sum + e.article.scannedAmount),
|
|
||||||
totalRemoved: entries.fold(0, (sum, e) => sum + e.article.scannedRemovedAmount),
|
|
||||||
entries: entries,
|
|
||||||
);
|
|
||||||
}).toList()
|
|
||||||
..sort((a, b) => a.name.compareTo(b.name));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
@ -335,7 +371,7 @@ class _ScanPageState extends State<ScanPage> with SingleTickerProviderStateMixin
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildProgressHeader(List<_ArticleGroup> allGroups) {
|
Widget _buildProgressHeader(List<_DeliveryGroup> allGroups) {
|
||||||
final total = allGroups.length;
|
final total = allGroups.length;
|
||||||
final done = allGroups.where((g) => g.isComplete).length;
|
final done = allGroups.where((g) => g.isComplete).length;
|
||||||
final progress = total > 0 ? done / total : 0.0;
|
final progress = total > 0 ? done / total : 0.0;
|
||||||
@ -355,7 +391,7 @@ class _ScanPageState extends State<ScanPage> with SingleTickerProviderStateMixin
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
"$done / $total Artikel",
|
"$done / $total Kunden",
|
||||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
@ -382,14 +418,9 @@ class _ScanPageState extends State<ScanPage> with SingleTickerProviderStateMixin
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildArticleTile(_ArticleGroup group, {int? carIdFilter}) {
|
Widget _buildDeliveryTile(_DeliveryGroup group) {
|
||||||
final isComplete = group.isComplete;
|
final isComplete = group.isComplete;
|
||||||
final isPartial = group.scannedOrRemoved > 0 && !isComplete;
|
final isPartial = group.isPartial;
|
||||||
final entries = carIdFilter != null
|
|
||||||
? group.entries
|
|
||||||
.where((e) => e.delivery.carId == carIdFilter)
|
|
||||||
.toList()
|
|
||||||
: group.entries;
|
|
||||||
|
|
||||||
final Color cardColor;
|
final Color cardColor;
|
||||||
final Color borderColor;
|
final Color borderColor;
|
||||||
@ -434,11 +465,11 @@ class _ScanPageState extends State<ScanPage> with SingleTickerProviderStateMixin
|
|||||||
key: const ValueKey('done'),
|
key: const ValueKey('done'),
|
||||||
)
|
)
|
||||||
: SizedBox(
|
: SizedBox(
|
||||||
width: 32,
|
width: 36,
|
||||||
key: const ValueKey('progress'),
|
key: const ValueKey('progress'),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
'${group.scannedOrRemoved}/${group.totalAmount}',
|
'${group.completeArticles}/${group.totalArticles}',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
@ -449,73 +480,154 @@ class _ScanPageState extends State<ScanPage> with SingleTickerProviderStateMixin
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
group.name,
|
group.delivery.customer.name,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: titleColor,
|
color: titleColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
"Artikelnr. ${group.articleNumber}",
|
group.delivery.customer.address.toString(),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
trailing: group.carPlate != null
|
||||||
|
? _carBadge(context, group.carPlate!)
|
||||||
|
: null,
|
||||||
children: [
|
children: [
|
||||||
const Divider(height: 1, indent: 16, endIndent: 16),
|
const Divider(height: 1, indent: 16, endIndent: 16),
|
||||||
...entries.map(_buildDeliveryEntry),
|
...group.articles.map(_buildArticleEntry),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDeliveryEntry(_ArticleDeliveryEntry entry) {
|
Widget _buildArticleEntry(Article article) {
|
||||||
final article = entry.article;
|
if (article.isParent && article.components.isNotEmpty) {
|
||||||
final customer = entry.delivery.customer;
|
return _buildParentArticleEntry(article);
|
||||||
final entryDone =
|
}
|
||||||
article.scannedAmount + article.scannedRemovedAmount >= article.amount;
|
|
||||||
|
final entryDone = article.isFullyScanned;
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 2),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 2),
|
||||||
leading: Icon(
|
leading: Icon(
|
||||||
entryDone ? Icons.check_circle_outline : Icons.person_outline,
|
entryDone ? Icons.check_circle_outline : Icons.inventory_2_outlined,
|
||||||
color: entryDone
|
color: entryDone
|
||||||
? Colors.green
|
? Colors.green
|
||||||
: Theme.of(context).colorScheme.onSurfaceVariant,
|
: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
size: 20,
|
size: 20,
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
customer.name,
|
article.name,
|
||||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
|
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
|
||||||
),
|
),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
customer.address.toString(),
|
"Artikelnr. ${article.articleNumber}",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
trailing: Column(
|
trailing: Text(
|
||||||
mainAxisSize: MainAxisSize.min,
|
'${article.scannedAmount + article.scannedRemovedAmount} / ${article.amount}×',
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
style: TextStyle(
|
||||||
children: [
|
fontWeight: FontWeight.bold,
|
||||||
if (entry.carPlate != null) ...[
|
fontSize: 13,
|
||||||
_carBadge(context, entry.carPlate!),
|
color: entryDone
|
||||||
const SizedBox(height: 4),
|
? Colors.green
|
||||||
],
|
: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
Text(
|
),
|
||||||
'${article.scannedAmount + article.scannedRemovedAmount} / ${article.amount}×',
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renders a parent article (Stückliste) with its components listed below.
|
||||||
|
Widget _buildParentArticleEntry(Article article) {
|
||||||
|
final allDone = article.isFullyScanned;
|
||||||
|
final scannedCount =
|
||||||
|
article.components.where((c) => c.isFullyScanned).length;
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
contentPadding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 24, vertical: 2),
|
||||||
|
leading: Icon(
|
||||||
|
allDone
|
||||||
|
? Icons.check_circle_outline
|
||||||
|
: Icons.account_tree_outlined,
|
||||||
|
color: allDone
|
||||||
|
? Colors.green
|
||||||
|
: Theme.of(context).colorScheme.primary,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
article.name,
|
||||||
|
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
"Stückliste · $scannedCount/${article.components.length} Komponenten",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontSize: 12,
|
||||||
fontSize: 13,
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
color: entryDone
|
|
||||||
? Colors.green
|
|
||||||
: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
trailing: Icon(
|
||||||
|
allDone ? Icons.check_circle : Icons.pending_outlined,
|
||||||
|
color: allDone ? Colors.green : Colors.orange,
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
...article.components.map(_buildComponentEntry),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Single component row, indented below the parent article.
|
||||||
|
Widget _buildComponentEntry(Component component) {
|
||||||
|
final done = component.isFullyScanned;
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 32),
|
||||||
|
child: ListTile(
|
||||||
|
dense: true,
|
||||||
|
contentPadding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 24, vertical: 0),
|
||||||
|
leading: Icon(
|
||||||
|
done
|
||||||
|
? Icons.check_circle_outline
|
||||||
|
: Icons.radio_button_unchecked,
|
||||||
|
color: done
|
||||||
|
? Colors.green
|
||||||
|
: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
component.name,
|
||||||
|
style: const TextStyle(fontSize: 13),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
"Artikelnr. ${component.articleNumber}",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: Text(
|
||||||
|
'${component.scannedAmount} / ${component.requiredAmount}×',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 12,
|
||||||
|
color: done
|
||||||
|
? Colors.green
|
||||||
|
: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -526,8 +638,8 @@ class _ScanPageState extends State<ScanPage> with SingleTickerProviderStateMixin
|
|||||||
|
|
||||||
Widget _buildOpenTab(
|
Widget _buildOpenTab(
|
||||||
TourLoaded state,
|
TourLoaded state,
|
||||||
List<_ArticleGroup> openGroups,
|
List<_DeliveryGroup> openGroups,
|
||||||
List<_ArticleGroup> allGroups,
|
List<_DeliveryGroup> allGroups,
|
||||||
bool useHardwareScanner,
|
bool useHardwareScanner,
|
||||||
) {
|
) {
|
||||||
return Column(
|
return Column(
|
||||||
@ -535,7 +647,31 @@ class _ScanPageState extends State<ScanPage> with SingleTickerProviderStateMixin
|
|||||||
if (_isScanning)
|
if (_isScanning)
|
||||||
const LinearProgressIndicator(),
|
const LinearProgressIndicator(),
|
||||||
if (!useHardwareScanner && openGroups.isNotEmpty)
|
if (!useHardwareScanner && openGroups.isNotEmpty)
|
||||||
BarcodeScannerWidget(onBarcodeDetected: _handleBarcodeScanned),
|
Stack(
|
||||||
|
children: [
|
||||||
|
BarcodeScannerWidget(onBarcodeDetected: _handleBarcodeScanned),
|
||||||
|
if (state.pendingScanRequests > 0)
|
||||||
|
Positioned(
|
||||||
|
top: 8,
|
||||||
|
right: 8,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black54,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: const SizedBox(
|
||||||
|
width: 18,
|
||||||
|
height: 18,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
valueColor: AlwaysStoppedAnimation(Colors.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
_buildProgressHeader(allGroups),
|
_buildProgressHeader(allGroups),
|
||||||
const Divider(height: 1),
|
const Divider(height: 1),
|
||||||
Expanded(
|
Expanded(
|
||||||
@ -551,7 +687,7 @@ class _ScanPageState extends State<ScanPage> with SingleTickerProviderStateMixin
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
const Text(
|
const Text(
|
||||||
"Alle Artikel geladen!",
|
"Alle Kunden vollständig beladen!",
|
||||||
style: TextStyle(fontSize: 16),
|
style: TextStyle(fontSize: 16),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -561,14 +697,14 @@ class _ScanPageState extends State<ScanPage> with SingleTickerProviderStateMixin
|
|||||||
padding: const EdgeInsets.only(top: 8, bottom: 96),
|
padding: const EdgeInsets.only(top: 8, bottom: 96),
|
||||||
itemCount: openGroups.length,
|
itemCount: openGroups.length,
|
||||||
itemBuilder: (context, index) =>
|
itemBuilder: (context, index) =>
|
||||||
_buildArticleTile(openGroups[index]),
|
_buildDeliveryTile(openGroups[index]),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildLoadedTab(List<_ArticleGroup> loadedGroups) {
|
Widget _buildLoadedTab(List<_DeliveryGroup> loadedGroups) {
|
||||||
if (_selectedCarId == null) {
|
if (_selectedCarId == null) {
|
||||||
return const Center(child: Text("Kein Fahrzeug ausgewählt"));
|
return const Center(child: Text("Kein Fahrzeug ausgewählt"));
|
||||||
}
|
}
|
||||||
@ -585,7 +721,7 @@ class _ScanPageState extends State<ScanPage> with SingleTickerProviderStateMixin
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Text(
|
Text(
|
||||||
"Noch keine Artikel im Auto",
|
"Noch keine Kunden im Auto",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
@ -599,10 +735,8 @@ class _ScanPageState extends State<ScanPage> with SingleTickerProviderStateMixin
|
|||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
padding: const EdgeInsets.only(top: 8, bottom: 96),
|
padding: const EdgeInsets.only(top: 8, bottom: 96),
|
||||||
itemCount: loadedGroups.length,
|
itemCount: loadedGroups.length,
|
||||||
itemBuilder: (context, index) => _buildArticleTile(
|
itemBuilder: (context, index) =>
|
||||||
loadedGroups[index],
|
_buildDeliveryTile(loadedGroups[index]),
|
||||||
carIdFilter: _selectedCarId,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -617,7 +751,7 @@ class _ScanPageState extends State<ScanPage> with SingleTickerProviderStateMixin
|
|||||||
builder: (context, carState) {
|
builder: (context, carState) {
|
||||||
return BlocConsumer<TourBloc, TourState>(
|
return BlocConsumer<TourBloc, TourState>(
|
||||||
listener: (context, tourState) {
|
listener: (context, tourState) {
|
||||||
if (tourState is TourLoaded) {
|
if (tourState is TourLoaded && tourState.pendingScanRequests == 0) {
|
||||||
setState(() => _isScanning = false);
|
setState(() => _isScanning = false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -641,20 +775,19 @@ class _ScanPageState extends State<ScanPage> with SingleTickerProviderStateMixin
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
final allGroups = _buildArticleGroups(tourState.tour);
|
final allGroups = _buildDeliveryGroups(tourState.tour);
|
||||||
|
|
||||||
// Offen: mindestens ein Kundeneintrag ist noch nicht vollständig gescannt
|
// Offen: Lieferung hat noch mindestens einen nicht vollständig
|
||||||
final openGroups = allGroups.where((g) => g.entries.any((e) =>
|
// gescannten Artikel (über alle Autos hinweg).
|
||||||
e.article.scannedAmount + e.article.scannedRemovedAmount <
|
final openGroups =
|
||||||
e.article.amount,
|
allGroups.where((g) => !g.isComplete).toList();
|
||||||
)).toList();
|
|
||||||
|
|
||||||
// Im Auto: mindestens ein Kundeneintrag für das aktuelle Auto ist vollständig
|
// Im Auto: Lieferung des aktuellen Autos, bei der mindestens ein
|
||||||
final loadedGroups = allGroups.where((g) => g.entries.any((e) =>
|
// Stück gescannt wurde.
|
||||||
e.delivery.carId == _selectedCarId &&
|
final loadedGroups = allGroups
|
||||||
e.article.scannedAmount + e.article.scannedRemovedAmount >=
|
.where((g) =>
|
||||||
e.article.amount,
|
g.delivery.carId == _selectedCarId && g.hasAnyScanned)
|
||||||
)).toList();
|
.toList();
|
||||||
|
|
||||||
final allDone = tourState.tour.deliveries.isNotEmpty &&
|
final allDone = tourState.tour.deliveries.isNotEmpty &&
|
||||||
openGroups.isEmpty;
|
openGroups.isEmpty;
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import 'package:hl_lieferservice/dto/article.dart';
|
import 'package:hl_lieferservice/dto/article.dart';
|
||||||
|
|
||||||
|
import 'component.dart';
|
||||||
|
|
||||||
class Article {
|
class Article {
|
||||||
Article({
|
Article({
|
||||||
required this.name,
|
required this.name,
|
||||||
@ -11,13 +13,21 @@ class Article {
|
|||||||
required this.scannable,
|
required this.scannable,
|
||||||
required this.scannedAmount,
|
required this.scannedAmount,
|
||||||
required this.scannedRemovedAmount,
|
required this.scannedRemovedAmount,
|
||||||
|
required this.isParent,
|
||||||
|
this.components = const [],
|
||||||
this.scannedDate,
|
this.scannedDate,
|
||||||
this.removeNoteId
|
this.removeNoteId,
|
||||||
|
this.warehouseNr,
|
||||||
|
this.warehouseName,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String name;
|
final String name;
|
||||||
final String articleNumber;
|
final String articleNumber;
|
||||||
final int internalId;
|
final int internalId;
|
||||||
|
final bool isParent;
|
||||||
|
final List<Component> components;
|
||||||
|
final String? warehouseNr;
|
||||||
|
final String? warehouseName;
|
||||||
|
|
||||||
int amount;
|
int amount;
|
||||||
double price;
|
double price;
|
||||||
@ -36,7 +46,35 @@ class Article {
|
|||||||
return price * scannedAmount * ((100 + tax) / 100);
|
return price * scannedAmount * ((100 + tax) / 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether this article is fully scanned.
|
||||||
|
///
|
||||||
|
/// For parent articles (Stückliste): delegates to components — all must be
|
||||||
|
/// individually scanned. For regular articles: the classic amount check.
|
||||||
|
bool get isFullyScanned {
|
||||||
|
if (isParent && components.isNotEmpty) {
|
||||||
|
return components.every((c) => c.isFullyScanned);
|
||||||
|
}
|
||||||
|
return scannedAmount + scannedRemovedAmount >= amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find a component by its article number, or `null` if none matches.
|
||||||
|
Component? findComponent(String articleNumber) {
|
||||||
|
for (final c in components) {
|
||||||
|
if (c.articleNumber == articleNumber) return c;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether this article *or* any of its components carries [articleNumber].
|
||||||
|
bool hasArticleNumber(String articleNumber) {
|
||||||
|
if (this.articleNumber == articleNumber) return true;
|
||||||
|
return components.any((c) => c.articleNumber == articleNumber);
|
||||||
|
}
|
||||||
|
|
||||||
bool unscanned() {
|
bool unscanned() {
|
||||||
|
if (isParent && components.isNotEmpty) {
|
||||||
|
return components.every((c) => c.scannedAmount == 0);
|
||||||
|
}
|
||||||
return scannedAmount == 0;
|
return scannedAmount == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,6 +96,11 @@ class Article {
|
|||||||
price: double.parse(dto.price == "" ? "0.0" : dto.price),
|
price: double.parse(dto.price == "" ? "0.0" : dto.price),
|
||||||
scannable: dto.scannable,
|
scannable: dto.scannable,
|
||||||
tax: double.parse(dto.taxRate == "" ? "19" : dto.taxRate),
|
tax: double.parse(dto.taxRate == "" ? "19" : dto.taxRate),
|
||||||
|
isParent: dto.isParent,
|
||||||
|
components: dto.components?.map(Component.fromDTO).toList() ?? [],
|
||||||
|
warehouseNr: dto.warehouseNr?.isEmpty ?? true ? null : dto.warehouseNr,
|
||||||
|
warehouseName:
|
||||||
|
dto.warehouseName?.isEmpty ?? true ? null : dto.warehouseName,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
34
lib/model/component.dart
Normal file
34
lib/model/component.dart
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import 'package:hl_lieferservice/dto/component.dart';
|
||||||
|
|
||||||
|
class Component {
|
||||||
|
Component({
|
||||||
|
required this.articleNumber,
|
||||||
|
required this.name,
|
||||||
|
required this.quantity,
|
||||||
|
required this.position,
|
||||||
|
this.scannedAmount = 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String articleNumber;
|
||||||
|
final String name;
|
||||||
|
final double quantity;
|
||||||
|
final double position;
|
||||||
|
|
||||||
|
int scannedAmount;
|
||||||
|
|
||||||
|
/// Required scan count derived from BOM quantity (e.g. 7.0 → 7).
|
||||||
|
int get requiredAmount => quantity.ceil();
|
||||||
|
|
||||||
|
bool get isFullyScanned => scannedAmount >= requiredAmount;
|
||||||
|
|
||||||
|
bool get needsScanning => scannedAmount < requiredAmount;
|
||||||
|
|
||||||
|
factory Component.fromDTO(ComponentDTO dto) {
|
||||||
|
return Component(
|
||||||
|
articleNumber: dto.articleNr,
|
||||||
|
name: dto.name,
|
||||||
|
quantity: double.tryParse(dto.quantity) ?? 0.0,
|
||||||
|
position: double.tryParse(dto.pos) ?? 0.0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -295,29 +295,42 @@ class Delivery implements Comparable<Delivery> {
|
|||||||
|
|
||||||
List<Article> getDeliveredArticles() {
|
List<Article> getDeliveredArticles() {
|
||||||
return articles
|
return articles
|
||||||
.where(
|
.where((article) {
|
||||||
(article) => article.scannedAmount > 0 || !article.scannable,
|
if (!article.scannable) return true;
|
||||||
)
|
if (article.isParent && article.components.isNotEmpty) {
|
||||||
|
return article.isFullyScanned;
|
||||||
|
}
|
||||||
|
return article.scannedAmount > 0;
|
||||||
|
})
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool containsArticle(String articleNr) {
|
bool containsArticle(String articleNr) {
|
||||||
return articles.any((article) => article.articleNumber == articleNr);
|
return articles.any((article) => article.hasArticleNumber(articleNr));
|
||||||
}
|
}
|
||||||
|
|
||||||
Article getArticle(String nr) {
|
Article getArticle(String nr) {
|
||||||
return articles.firstWhere((article) => article.articleNumber == nr);
|
return articles.firstWhere((article) => article.articleNumber == nr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Find the parent article whose BOM contains [componentArticleNr].
|
||||||
|
Article? findParentOfComponent(String componentArticleNr) {
|
||||||
|
for (final article in articles) {
|
||||||
|
if (article.isParent &&
|
||||||
|
article.findComponent(componentArticleNr) != null) {
|
||||||
|
return article;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
List<Article> getScannableArticles() {
|
List<Article> getScannableArticles() {
|
||||||
return articles.where((article) => article.scannable).toList();
|
return articles.where((article) => article.scannable).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool allArticlesScanned() {
|
bool allArticlesScanned() {
|
||||||
return getScannableArticles().every(
|
return getScannableArticles().every(
|
||||||
(article) =>
|
(article) => article.isFullyScanned,
|
||||||
article.amount ==
|
|
||||||
article.scannedAmount + article.scannedRemovedAmount,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -71,28 +71,31 @@ class _DeliveryAppState extends State<DeliveryApp> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: MaterialApp(
|
child: MaterialApp(
|
||||||
home: OperationViewEnforcer(
|
// Wrap the Navigator (not just the home route) so the loading
|
||||||
child: BlocBuilder<AppBloc, AppState>(
|
// overlay covers every pushed route — DeliveryDetail, Cars,
|
||||||
builder: (context, state) {
|
// dialogs, etc. — not only the initial home tree.
|
||||||
if (state is AppConfigLoading) {
|
builder: (context, child) =>
|
||||||
return Scaffold(
|
OperationViewEnforcer(child: child ?? const SizedBox.shrink()),
|
||||||
body: Center(child: CircularProgressIndicator()),
|
home: BlocBuilder<AppBloc, AppState>(
|
||||||
);
|
builder: (context, state) {
|
||||||
}
|
if (state is AppConfigLoading) {
|
||||||
|
return Scaffold(
|
||||||
|
body: Center(child: CircularProgressIndicator()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (state is AppConfigLoadingFailed) {
|
if (state is AppConfigLoadingFailed) {
|
||||||
return Scaffold(body: Center(child: Text(state.message)));
|
return Scaffold(body: Center(child: Text(state.message)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state is AppConfigLoaded) {
|
if (state is AppConfigLoaded) {
|
||||||
return LoginEnforcer(
|
return LoginEnforcer(
|
||||||
child: CarSelectionEnforcer(child: Home()),
|
child: CarSelectionEnforcer(child: Home()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Container();
|
return Container();
|
||||||
},
|
},
|
||||||
),
|
|
||||||
),
|
),
|
||||||
routes: {"/cars": (context) => CarManagementPage()},
|
routes: {"/cars": (context) => CarManagementPage()},
|
||||||
),
|
),
|
||||||
|
|||||||
@ -3,20 +3,79 @@ import 'package:hl_lieferservice/widget/operations/bloc/operation_event.dart';
|
|||||||
import 'package:hl_lieferservice/widget/operations/bloc/operation_state.dart';
|
import 'package:hl_lieferservice/widget/operations/bloc/operation_state.dart';
|
||||||
|
|
||||||
class OperationBloc extends Bloc<OperationEvent, OperationState> {
|
class OperationBloc extends Bloc<OperationEvent, OperationState> {
|
||||||
|
/// Counts how many in-flight mutations want to show the blocking overlay.
|
||||||
|
/// Allows multiple parallel mutations without one prematurely closing the
|
||||||
|
/// overlay before the others complete.
|
||||||
|
int _inFlightCount = 0;
|
||||||
|
|
||||||
|
/// When the current overlay session began (set when [_inFlightCount]
|
||||||
|
/// transitions 0 → 1). Used to enforce [_minimumDisplayDuration].
|
||||||
|
DateTime? _overlayStartedAt;
|
||||||
|
|
||||||
|
/// Minimum time the overlay stays visible, even if the underlying request
|
||||||
|
/// completes faster. Prevents a "did anything happen?" UX where a sub-100 ms
|
||||||
|
/// roundtrip flashes the overlay for one frame.
|
||||||
|
static const Duration _minimumDisplayDuration = Duration(milliseconds: 350);
|
||||||
|
|
||||||
OperationBloc() : super(OperationIdle()) {
|
OperationBloc() : super(OperationIdle()) {
|
||||||
|
on<StartOperation>(_startOperation);
|
||||||
on<FailOperation>(_failOperation);
|
on<FailOperation>(_failOperation);
|
||||||
on<FinishOperation>(_finishOperation);
|
on<FinishOperation>(_finishOperation);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _failOperation(FailOperation event, Emitter<OperationState> emit) async {
|
Future<void> _startOperation(
|
||||||
emit(OperationFailed(message: event.message));
|
StartOperation event,
|
||||||
await Future.delayed(const Duration(seconds: 5));
|
Emitter<OperationState> emit,
|
||||||
emit(OperationIdle());
|
) async {
|
||||||
|
if (_inFlightCount == 0) {
|
||||||
|
_overlayStartedAt = DateTime.now();
|
||||||
|
}
|
||||||
|
_inFlightCount += 1;
|
||||||
|
emit(OperationInProgress(message: event.message));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _finishOperation(FinishOperation event, Emitter<OperationState> emit) async {
|
Future<void> _finishOperation(
|
||||||
emit(OperationFinished(message: event.message));
|
FinishOperation event,
|
||||||
|
Emitter<OperationState> emit,
|
||||||
|
) async {
|
||||||
|
_inFlightCount = (_inFlightCount - 1).clamp(0, 1 << 30);
|
||||||
|
|
||||||
|
if (event.message != null) {
|
||||||
|
emit(OperationFinished(message: event.message));
|
||||||
|
await Future.delayed(const Duration(seconds: 5));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_inFlightCount > 0) {
|
||||||
|
emit(OperationInProgress());
|
||||||
|
} else {
|
||||||
|
await _awaitMinimumOverlayDuration();
|
||||||
|
_overlayStartedAt = null;
|
||||||
|
emit(OperationIdle());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _failOperation(
|
||||||
|
FailOperation event,
|
||||||
|
Emitter<OperationState> emit,
|
||||||
|
) async {
|
||||||
|
_inFlightCount = (_inFlightCount - 1).clamp(0, 1 << 30);
|
||||||
|
emit(OperationFailed(message: event.message));
|
||||||
await Future.delayed(const Duration(seconds: 5));
|
await Future.delayed(const Duration(seconds: 5));
|
||||||
emit(OperationIdle());
|
|
||||||
|
if (_inFlightCount > 0) {
|
||||||
|
emit(OperationInProgress());
|
||||||
|
} else {
|
||||||
|
_overlayStartedAt = null;
|
||||||
|
emit(OperationIdle());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _awaitMinimumOverlayDuration() async {
|
||||||
|
final startedAt = _overlayStartedAt;
|
||||||
|
if (startedAt == null) return;
|
||||||
|
final elapsed = DateTime.now().difference(startedAt);
|
||||||
|
if (elapsed < _minimumDisplayDuration) {
|
||||||
|
await Future.delayed(_minimumDisplayDuration - elapsed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,11 @@
|
|||||||
abstract class OperationEvent {}
|
abstract class OperationEvent {}
|
||||||
|
|
||||||
|
class StartOperation extends OperationEvent {
|
||||||
|
String? message;
|
||||||
|
|
||||||
|
StartOperation({this.message});
|
||||||
|
}
|
||||||
|
|
||||||
class FailOperation extends OperationEvent {
|
class FailOperation extends OperationEvent {
|
||||||
String message;
|
String message;
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,12 @@ abstract class OperationState {}
|
|||||||
|
|
||||||
class OperationIdle extends OperationState {}
|
class OperationIdle extends OperationState {}
|
||||||
|
|
||||||
|
class OperationInProgress extends OperationState {
|
||||||
|
String? message;
|
||||||
|
|
||||||
|
OperationInProgress({this.message});
|
||||||
|
}
|
||||||
|
|
||||||
class OperationFailed extends OperationState {
|
class OperationFailed extends OperationState {
|
||||||
String message;
|
String message;
|
||||||
|
|
||||||
|
|||||||
@ -4,8 +4,11 @@ import 'package:hl_lieferservice/widget/operations/bloc/operation_bloc.dart';
|
|||||||
|
|
||||||
import '../bloc/operation_state.dart';
|
import '../bloc/operation_state.dart';
|
||||||
|
|
||||||
/// Listens to [OperationBloc] and shows SnackBars for success and error
|
/// Listens to [OperationBloc] and shows:
|
||||||
/// messages. Loading indicators are handled locally by each feature.
|
/// - SnackBars for success and error messages.
|
||||||
|
/// - A blocking modal barrier with a spinner while a mutation is in flight,
|
||||||
|
/// so the user gets unambiguous "wait" feedback and cannot double-tap or
|
||||||
|
/// navigate away mid-request.
|
||||||
class OperationViewEnforcer extends StatelessWidget {
|
class OperationViewEnforcer extends StatelessWidget {
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
|
||||||
@ -13,7 +16,7 @@ class OperationViewEnforcer extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocListener<OperationBloc, OperationState>(
|
return BlocConsumer<OperationBloc, OperationState>(
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
if (state is OperationFinished && state.message != null) {
|
if (state is OperationFinished && state.message != null) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
@ -27,7 +30,44 @@ class OperationViewEnforcer extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: child,
|
builder: (context, state) {
|
||||||
|
final isInProgress = state is OperationInProgress;
|
||||||
|
final progressMessage =
|
||||||
|
isInProgress ? state.message : null;
|
||||||
|
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
child,
|
||||||
|
if (isInProgress)
|
||||||
|
PopScope(
|
||||||
|
canPop: false,
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
const ModalBarrier(
|
||||||
|
dismissible: false,
|
||||||
|
color: Colors.black54,
|
||||||
|
),
|
||||||
|
Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const CircularProgressIndicator(),
|
||||||
|
if (progressMessage != null) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
progressMessage,
|
||||||
|
style: const TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user