Implemented new set article mechanism for unscannable articles

This commit is contained in:
Dennis Nemec
2026-01-10 20:20:28 +01:00
parent 1848f47e7f
commit 2436177c95
17 changed files with 334 additions and 46 deletions

View File

@ -14,3 +14,7 @@ A few resources to get you started if this is your first Flutter project:
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
## JSON code generation
use `dart run build_runner watch --delete-conflicting-outputs` for generating

View File

@ -0,0 +1,23 @@
import 'package:json_annotation/json_annotation.dart';
part 'set_article_amount_request.g.dart';
@JsonSerializable(fieldRename: FieldRename.snake)
class SetArticleAmountRequestDTO {
SetArticleAmountRequestDTO({
required this.articleId,
required this.deliveryId,
required this.amount,
this.reason
});
String deliveryId;
int amount;
String articleId;
String? reason;
factory SetArticleAmountRequestDTO.fromJson(Map<String, dynamic> json) =>
_$SetArticleAmountRequestDTOFromJson(json);
Map<dynamic, dynamic> toJson() => _$SetArticleAmountRequestDTOToJson(this);
}

View File

@ -0,0 +1,21 @@
import 'package:hl_lieferservice/dto/basic_response.dart';
import 'package:json_annotation/json_annotation.dart';
part 'set_article_amount_response.g.dart';
@JsonSerializable(fieldRename: FieldRename.snake)
class SetArticleAmountResponseDTO extends BasicResponseDTO {
SetArticleAmountResponseDTO({
required super.succeeded,
required super.message,
this.noteId
});
String? noteId;
factory SetArticleAmountResponseDTO.fromJson(Map<String, dynamic> json) =>
_$SetArticleAmountResponseDTOFromJson(json);
@override
Map<dynamic, dynamic> toJson() => _$SetArticleAmountResponseDTOToJson(this);
}

View File

@ -5,7 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hl_lieferservice/feature/delivery/bloc/tour_event.dart';
import 'package:hl_lieferservice/feature/delivery/bloc/tour_state.dart';
import 'package:hl_lieferservice/feature/delivery/overview/model/sorting_information.dart';
import 'package:hl_lieferservice/feature/delivery/overview/repository/tour_repository.dart';
import 'package:hl_lieferservice/feature/delivery/repository/tour_repository.dart';
import 'package:hl_lieferservice/feature/delivery/overview/service/distance_service.dart';
import 'package:hl_lieferservice/feature/delivery/overview/service/reorder_service.dart';
import 'package:hl_lieferservice/model/tour.dart';
@ -55,6 +55,7 @@ class TourBloc extends Bloc<TourEvent, TourState> {
on<RequestSortingInformationEvent>(_requestSortingInformation);
on<ReorderDeliveryEvent>(_reorderDelivery);
on<CarsLoadedEvent>(_carsLoaded);
on<SetArticleAmountEvent>(_setArticleAmount);
}
@override
@ -64,6 +65,33 @@ class TourBloc extends Bloc<TourEvent, TourState> {
return super.close();
}
void _setArticleAmount(
SetArticleAmountEvent event,
Emitter<TourState> emit,
) async {
final currentState = state;
if (currentState is TourLoaded) {
opBloc.add(LoadOperation());
try {
await tourRepository.setArticleAmount(
event.deliveryId,
event.articleId,
event.amount,
event.reason
);
opBloc.add(FinishOperation());
} catch (e, st) {
opBloc.add(
FailOperation(message: "Fehler beim Ändern der Menge des Artikels"),
);
debugPrint("$e");
debugPrint("$st");
}
}
}
void _carsLoaded(CarsLoadedEvent event, Emitter<TourState> emit) {
final currentState = state;
if (currentState is TourLoaded) {
@ -78,13 +106,16 @@ class TourBloc extends Bloc<TourEvent, TourState> {
) async {
final currentState = state;
if (currentState is TourLoaded) {
int newPosition = event.newPosition == currentState.sortingInformation.sorting.length ? event.newPosition - 1 : event.newPosition;
SortingInformation informationOld = currentState.sortingInformation.sorting
.firstWhere((info) => info.position == event.oldPosition);
SortingInformation information = currentState
int newPosition =
event.newPosition == currentState.sortingInformation.sorting.length
? event.newPosition - 1
: event.newPosition;
SortingInformation informationOld = currentState
.sortingInformation
.sorting
.firstWhere((info) => info.position == event.oldPosition);
SortingInformation information = currentState.sortingInformation.sorting
.firstWhere((info) => info.position == newPosition);
information.position = event.oldPosition;

View File

@ -203,3 +203,17 @@ class FinishDeliveryEvent extends TourEvent {
Uint8List customerSignature;
Uint8List driverSignature;
}
class SetArticleAmountEvent extends TourEvent {
final String deliveryId;
final String articleId;
final String? reason;
final int amount;
SetArticleAmountEvent({
required this.deliveryId,
required this.articleId,
required this.amount,
this.reason,
});
}

View File

@ -23,6 +23,10 @@ class _ArticleListItem extends State<ArticleListItem> {
Color? color;
Color? textColor;
if (!widget.article.scannable) {
amount = widget.article.amount;
}
if (amount == 0) {
color = Colors.redAccent;
textColor = Theme.of(context).colorScheme.onSecondary;
@ -56,7 +60,8 @@ class _ArticleListItem extends State<ArticleListItem> {
),
);
if (widget.article.unscanned()) {
if ((widget.article.unscanned() && widget.article.scannable) ||
!widget.article.scannable && widget.article.amount == 0) {
actionButton = IconButton.outlined(
style: ButtonStyle(
backgroundColor: WidgetStatePropertyAll(Colors.blueAccent),

View File

@ -20,33 +20,86 @@ class ResetArticleAmountDialog extends StatefulWidget {
}
class _ResetArticleAmountDialogState extends State<ResetArticleAmountDialog> {
int _selectedAmount = 1;
void _reset() {
String deliveryId = widget.deliveryId;
String articleId = widget.article.internalId.toString();
if (widget.article.scannable) {
context.read<TourBloc>().add(
ResetScanAmountEvent(
articleId: widget.article.internalId.toString(),
deliveryId: widget.deliveryId,
),
);
} else {
debugPrint("ID: $articleId");
debugPrint("AMOUNT :$_selectedAmount");
context.read<TourBloc>().add(
SetArticleAmountEvent(
deliveryId: deliveryId,
articleId: articleId,
amount: _selectedAmount,
),
);
}
Navigator.pop(context);
}
Widget _amountSelection() {
final list = List.generate(3, (index) => index + 1);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Anzahl:", style: Theme.of(context).textTheme.labelLarge),
Padding(
padding: const EdgeInsets.all(10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children:
list
.map(
(index) => ChoiceChip(
label: Text("$index"),
selected: _selectedAmount == index,
onSelected: (bool selected) {
setState(() {
_selectedAmount = index;
});
},
),
)
.toList(),
),
),
],
);
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text("Anzahl Artikel zurücksetzen?"),
content: SizedBox(
height: 120,
height: MediaQuery.of(context).size.height * 0.25,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text("Wollen Sie die entfernten Artikel wieder hinzufügen?"),
!widget.article.scannable ? _amountSelection() : Container(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
FilledButton(
onPressed: _reset,
child: const Text("Zurücksetzen"),
child:
widget.article.scannable
? const Text("Zurücksetzen")
: const Text("Hinzufügen"),
),
OutlinedButton(
onPressed: () {

View File

@ -7,7 +7,11 @@ import 'package:hl_lieferservice/feature/delivery/bloc/tour_event.dart';
import '../../../../../model/article.dart';
class ArticleUnscanDialog extends StatefulWidget {
const ArticleUnscanDialog({super.key, required this.article, required this.deliveryId});
const ArticleUnscanDialog({
super.key,
required this.article,
required this.deliveryId,
});
final String deliveryId;
final Article article;
@ -23,14 +27,32 @@ class _ArticleUnscanDialogState extends State<ArticleUnscanDialog> {
final _formKey = GlobalKey<FormState>();
void _unscan() {
int amountToBeDeleted = int.parse(unscanAmountController.text);
String deliveryId = widget.deliveryId;
String articleId = widget.article.internalId.toString();
String reason = unscanNoteController.text;
if (widget.article.scannable) {
context.read<TourBloc>().add(
UnscanArticleEvent(
deliveryId: widget.deliveryId,
articleId: widget.article.internalId.toString(),
newAmount: int.parse(unscanAmountController.text),
reason: unscanNoteController.text,
deliveryId: deliveryId,
articleId: articleId,
newAmount: amountToBeDeleted,
reason: reason,
),
);
} else {
// If the article is not scannable we need to adjust the quantity of the article
// directly.
context.read<TourBloc>().add(
SetArticleAmountEvent(
deliveryId: deliveryId,
articleId: articleId,
amount: widget.article.amount - amountToBeDeleted,
reason: reason
),
);
}
Navigator.pop(context);
}

View File

@ -136,6 +136,9 @@ class _DeliveryDetailState extends State<DeliveryDetail> {
driverSignature: driver,
),
);
Navigator.pop(context);
Navigator.pop(context);
}
Widget _stepsNavigation(Delivery delivery) {

View File

@ -117,8 +117,6 @@ class _SignatureViewState extends State<SignatureView> {
builder: (context, state) {
final current = state;
debugPrint("STATE: $current");
if (current is NoteLoaded) {
if (current.notes.isEmpty) {
return const SizedBox(

View File

@ -5,7 +5,7 @@ import 'package:hl_lieferservice/dto/discount_remove_response.dart';
import 'package:hl_lieferservice/dto/discount_update_response.dart';
import 'package:hl_lieferservice/feature/delivery/detail/repository/note_repository.dart';
import 'package:hl_lieferservice/feature/delivery/detail/service/notes_service.dart';
import 'package:hl_lieferservice/feature/delivery/overview/service/delivery_info_service.dart';
import 'package:hl_lieferservice/feature/delivery/service/tour_service.dart';
import 'package:hl_lieferservice/model/delivery.dart';
class DeliveryRepository {

View File

@ -60,7 +60,7 @@ class _DeliveryListState extends State<DeliveryList> {
.where(
(delivery) =>
delivery.carId == widget.selectedCarId &&
delivery.allArticlesScanned(),
delivery.allArticlesScanned() || delivery.state == DeliveryState.finished,
)
.toList();

View File

@ -1,6 +1,7 @@
import 'dart:typed_data';
import 'package:hl_lieferservice/feature/delivery/overview/service/delivery_info_service.dart';
import 'package:hl_lieferservice/dto/set_article_amount_response.dart';
import 'package:hl_lieferservice/feature/delivery/service/tour_service.dart';
import 'package:hl_lieferservice/model/delivery.dart';
import 'package:hl_lieferservice/model/tour.dart';
import 'package:rxdart/rxdart.dart';
@ -9,8 +10,8 @@ import '../../../../dto/discount_add_response.dart';
import '../../../../dto/discount_remove_response.dart';
import '../../../../dto/discount_update_response.dart';
import '../../../../model/article.dart';
import '../../detail/repository/note_repository.dart';
import '../../detail/service/notes_service.dart';
import '../detail/repository/note_repository.dart';
import '../detail/service/notes_service.dart';
enum ScanResult { scanned, alreadyScanned, notFound }
@ -172,13 +173,19 @@ class TourRepository {
Delivery delivery = tour.deliveries.firstWhere(
(delivery) => delivery.id == deliveryId,
);
Article discountArticle = Article.fromDTO(response.values.article);
delivery.totalNetValue = response.values.receipt.net;
delivery.totalGrossValue = response.values.receipt.gross;
delivery.discount = Discount(
article: Article.fromDTO(response.values.article),
note: response.values.note.noteDescription,
noteId: response.values.note.rowId,
);
delivery.articles = [
...delivery.articles,
discountArticle,
];
_tourStream.add(tour);
}
@ -275,6 +282,45 @@ class TourRepository {
Future<void> finishDelivery(String deliveryId) async {
await _changeState(deliveryId, DeliveryState.finished);
Delivery delivery = _tourStream.value!.deliveries.firstWhere(
(delivery) => delivery.id == deliveryId,
);
await _updateDelivery(delivery);
await service.finishDelivery(deliveryId);
}
Future<void> setArticleAmount(
String deliveryId,
String articleId,
int amount,
String? reason,
) async {
if (!_tourStream.hasValue) {
throw TourNotFoundException();
}
try {
SetArticleAmountResponseDTO dto = await service.setArticleAmount(
deliveryId,
articleId,
amount,
reason,
);
Delivery delivery = _tourStream.value!.deliveries.firstWhere(
(delivery) => delivery.id == deliveryId,
);
Article article = delivery.articles.firstWhere(
(article) => article.internalId == int.parse(articleId),
);
article.amount = amount;
article.removeNoteId = dto.noteId;
_tourStream.add(_tourStream.value);
} catch (_) {
rethrow;
}
}
Future<void> _changeState(String deliveryId, DeliveryState state) async {

View File

@ -7,18 +7,20 @@ import 'package:hl_lieferservice/dto/delivery_update.dart';
import 'package:hl_lieferservice/dto/delivery_update_response.dart';
import 'package:hl_lieferservice/dto/payment.dart';
import 'package:hl_lieferservice/dto/payments.dart';
import 'package:hl_lieferservice/dto/set_article_amount_request.dart';
import 'package:hl_lieferservice/dto/set_article_amount_response.dart';
import 'package:hl_lieferservice/model/car.dart';
import 'package:hl_lieferservice/model/delivery.dart';
import 'package:hl_lieferservice/model/tour.dart';
import 'package:hl_lieferservice/util.dart';
import 'package:http/http.dart';
import '../../../../dto/basic_response.dart';
import '../../../../dto/discount_add_response.dart';
import '../../../../dto/discount_remove_response.dart';
import '../../../../dto/discount_update_response.dart';
import '../../../../dto/scan_response.dart';
import '../../../authentication/exceptions.dart';
import '../../../dto/basic_response.dart';
import '../../../dto/discount_add_response.dart';
import '../../../dto/discount_remove_response.dart';
import '../../../dto/discount_update_response.dart';
import '../../../dto/scan_response.dart';
import '../../authentication/exceptions.dart';
class TourService {
TourService();
@ -267,6 +269,76 @@ class TourService {
}
}
Future<BasicResponseDTO> finishDelivery(String deliveryId) async {
try {
var response = await post(
urlBuilder("_web_finishDelivery"),
headers: getSessionOrThrow(),
body: {"delivery_id": deliveryId},
);
if (response.statusCode == HttpStatus.unauthorized) {
throw UserUnauthorized();
}
debugPrint("BODY: ${response.body}");
Map<String, dynamic> responseJson = jsonDecode(response.body);
// let it throw, if the values are invalid
return BasicResponseDTO.fromJson(responseJson);
} catch (e, st) {
debugPrint("ERROR while adding discount");
debugPrint(e.toString());
debugPrint(st.toString());
rethrow;
}
}
Future<SetArticleAmountResponseDTO> setArticleAmount(
String deliveryId,
String articleId,
int amount,
String? reason,
) async {
try {
var response = await post(
urlBuilder("_web_setArticleAmount"),
headers: {...getSessionOrThrow(), "Content-Type": "application/json"},
body: jsonEncode(
SetArticleAmountRequestDTO(
articleId: articleId,
deliveryId: deliveryId,
amount: amount,
reason: reason,
),
),
);
if (response.statusCode == HttpStatus.unauthorized) {
throw UserUnauthorized();
}
debugPrint("BODY: ${response.body}");
Map<String, dynamic> responseJson = jsonDecode(response.body);
// let it throw, if the values are invalid
SetArticleAmountResponseDTO responseDto =
SetArticleAmountResponseDTO.fromJson(responseJson);
if (!responseDto.succeeded) {
throw responseDto.message;
} else {
return responseDto;
}
} catch (e, st) {
debugPrint(e.toString());
debugPrint(st.toString());
rethrow;
}
}
Future<DiscountRemoveResponseDTO> removeDiscount(String deliveryId) async {
try {
var response = await post(

View File

@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hl_lieferservice/feature/delivery/bloc/tour_bloc.dart';
import 'package:hl_lieferservice/feature/delivery/bloc/tour_state.dart';
import 'package:hl_lieferservice/feature/scan/presentation/scan_screen.dart';
import 'package:hl_lieferservice/model/delivery.dart';
import 'package:hl_lieferservice/model/tour.dart';
import 'package:hl_lieferservice/widget/home/bloc/navigation_bloc.dart';
import 'package:hl_lieferservice/widget/home/bloc/navigation_event.dart';
@ -36,7 +37,7 @@ class _ScanPageState extends State<ScanPage> {
Widget _tourSteps(Tour tour) {
var allArticlesScanned = tour.deliveries.every(
(delivery) => delivery.allArticlesScanned(),
(delivery) => delivery.allArticlesScanned() || delivery.state == DeliveryState.finished,
);
return Stepper(

View File

@ -1,6 +1,5 @@
import 'dart:typed_data';
import 'package:flutter/cupertino.dart';
import 'package:hl_lieferservice/dto/contact_person.dart';
import 'package:hl_lieferservice/dto/delivery.dart';
import 'package:hl_lieferservice/dto/image_note_response.dart';
@ -297,11 +296,7 @@ class Delivery implements Comparable<Delivery> {
List<Article> getDeliveredArticles() {
return articles
.where(
(article) {
debugPrint("Scannable: ${article.scannable}");
return article.scannedAmount > 0 || !article.scannable;
},
(article) => article.scannedAmount > 0 || !article.scannable,
)
.toList();
}

View File

@ -6,13 +6,13 @@ import 'package:hl_lieferservice/feature/authentication/presentation/login_enfor
import 'package:hl_lieferservice/feature/authentication/service/userinfo.dart';
import 'package:hl_lieferservice/feature/cars/presentation/car_management_page.dart';
import 'package:hl_lieferservice/feature/delivery/bloc/tour_bloc.dart';
import 'package:hl_lieferservice/feature/delivery/overview/repository/tour_repository.dart';
import 'package:hl_lieferservice/feature/delivery/repository/tour_repository.dart';
import 'package:hl_lieferservice/widget/home/bloc/navigation_bloc.dart';
import 'package:hl_lieferservice/widget/operations/bloc/operation_bloc.dart';
import 'package:hl_lieferservice/widget/operations/presentation/operation_view_enforcer.dart';
import 'package:hl_lieferservice/bloc/app_states.dart';
import '../feature/delivery/overview/service/delivery_info_service.dart';
import '../feature/delivery/service/tour_service.dart';
import 'home/presentation/home.dart';
class DeliveryApp extends StatefulWidget {