This commit is contained in:
Dennis Nemec
2026-04-28 13:03:09 +02:00
parent de8668c11a
commit 2470299a10
53 changed files with 2409 additions and 1433 deletions

View File

@ -9,16 +9,20 @@ import 'package:hl_lieferservice/feature/delivery/overview/service/distance_serv
import 'package:hl_lieferservice/feature/delivery/overview/service/reorder_service.dart';
import 'package:hl_lieferservice/feature/delivery/util.dart';
import 'package:hl_lieferservice/model/tour.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/exceptions.dart';
import 'package:hl_lieferservice/widget/operations/bloc/operation_bloc.dart';
import 'package:hl_lieferservice/widget/operations/bloc/operation_event.dart';
import 'package:rxdart/rxdart.dart';
class TourBloc extends Bloc<TourEvent, TourState> {
OperationBloc opBloc;
AuthBloc authBloc;
TourRepository tourRepository;
StreamSubscription? _combinedSubscription;
TourBloc({required this.opBloc, required this.tourRepository})
TourBloc({required this.opBloc, required this.authBloc, required this.tourRepository})
: super(TourInitial()) {
_combinedSubscription = CombineLatestStream.combine2(
tourRepository.tour,
@ -61,17 +65,23 @@ class TourBloc extends Bloc<TourEvent, TourState> {
@override
Future<void> close() {
_combinedSubscription?.cancel();
return super.close();
}
void _handleError(Object e, String fallbackMessage) {
if (e is UserUnauthorized) {
authBloc.add(SessionExpiredEvent());
} else {
opBloc.add(FailOperation(message: fallbackMessage));
}
}
void _setArticleAmount(
SetArticleAmountEvent event,
Emitter<TourState> emit,
) async {
final currentState = state;
if (currentState is TourLoaded) {
opBloc.add(LoadOperation());
try {
await tourRepository.setArticleAmount(
event.deliveryId,
@ -79,15 +89,9 @@ class TourBloc extends Bloc<TourEvent, TourState> {
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");
debugPrint("$e $st");
_handleError(e, "Fehler beim Ändern der Menge des Artikels");
}
}
}
@ -126,7 +130,6 @@ class TourBloc extends Bloc<TourEvent, TourState> {
Emitter<TourState> emit,
) async {
Map<String, double> distances = {};
opBloc.add(LoadOperation());
emit(TourRequestingDistances(tour: event.tour, payments: event.payments));
@ -135,7 +138,7 @@ class TourBloc extends Bloc<TourEvent, TourState> {
distances[delivery.id] = await DistanceService.getDistanceByRoad(
delivery.customer.address.toString(),
);
} catch (e,st) {
} catch (e, st) {
debugPrint("Fehler beim Laden der Distanz: $e");
debugPrint("$st");
@ -145,7 +148,6 @@ class TourBloc extends Bloc<TourEvent, TourState> {
}
}
opBloc.add(FinishOperation());
// If an error occurred, then the distances will be empty
// If the distances are empty then they shouldn't be displayed
add(
@ -251,17 +253,11 @@ class TourBloc extends Bloc<TourEvent, TourState> {
) async {
final currentState = state;
if (currentState is TourLoaded) {
opBloc.add(LoadOperation());
try {
await tourRepository.reactivateDelivery(event.deliveryId);
opBloc.add(FinishOperation());
} catch (e, st) {
debugPrint("$e");
debugPrint("$st");
opBloc.add(
FailOperation(message: "Fehler beim Zurückstellen der Lieferung"),
);
debugPrint("$e $st");
_handleError(e, "Fehler beim Zurückstellen der Lieferung");
}
}
}
@ -269,17 +265,11 @@ class TourBloc extends Bloc<TourEvent, TourState> {
void _holdDelivery(HoldDeliveryEvent event, Emitter<TourState> emit) async {
final currentState = state;
if (currentState is TourLoaded) {
opBloc.add(LoadOperation());
try {
await tourRepository.holdDelivery(event.deliveryId);
opBloc.add(FinishOperation());
} catch (e, st) {
debugPrint("$e");
debugPrint("$st");
opBloc.add(
FailOperation(message: "Fehler beim Zurückstellen der Lieferung"),
);
debugPrint("$e $st");
_handleError(e, "Fehler beim Zurückstellen der Lieferung");
}
}
}
@ -290,24 +280,17 @@ class TourBloc extends Bloc<TourEvent, TourState> {
) async {
final currentState = state;
if (currentState is TourLoaded) {
opBloc.add(LoadOperation());
try {
await tourRepository.cancelDelivery(event.deliveryId);
opBloc.add(FinishOperation());
} catch (e, st) {
debugPrint("$e");
debugPrint("$st");
opBloc.add(
FailOperation(message: "Fehler beim Zurückstellen der Lieferung"),
);
debugPrint("$e $st");
_handleError(e, "Fehler beim Stornieren der Lieferung");
}
}
}
void _scan(ScanArticleEvent event, Emitter<TourState> emit) async {
final currentState = state;
opBloc.add(LoadOperation());
if (currentState is TourLoaded) {
try {
@ -333,9 +316,8 @@ class TourBloc extends Bloc<TourEvent, TourState> {
break;
}
} catch (e, st) {
debugPrint("FEHLER beim Scannen eines Artikels: $e");
debugPrint("$st");
opBloc.add(FailOperation(message: "Fehler beim Scannen des Artikels"));
debugPrint("FEHLER beim Scannen eines Artikels: $e $st");
_handleError(e, "Fehler beim Scannen des Artikels");
}
}
}
@ -347,17 +329,15 @@ class TourBloc extends Bloc<TourEvent, TourState> {
final currentState = state;
if (currentState is TourLoaded) {
opBloc.add(LoadOperation());
try {
await tourRepository.scanArticle(
event.deliveryId,
event.carId,
event.internalArticleId,
);
opBloc.add(FinishOperation());
} catch (e, st) {
debugPrint(st.toString());
opBloc.add(FailOperation(message: "Fehler beim Scannen des Artikels"));
debugPrint("$e $st");
_handleError(e, "Fehler beim Scannen des Artikels");
}
}
}
@ -365,34 +345,27 @@ class TourBloc extends Bloc<TourEvent, TourState> {
Future<void> _assignCar(AssignCarEvent event, Emitter<TourState> emit) async {
final currentState = state;
if (currentState is TourLoaded) {
opBloc.add(LoadOperation());
try {
await tourRepository.assignCar(event.deliveryId, event.carId);
opBloc.add(FinishOperation());
} catch (e, st) {
debugPrint(st.toString());
opBloc.add(
FailOperation(message: "Fehler beim Zuweisen des Fahrzeugs"),
);
debugPrint("$e $st");
_handleError(e, "Fehler beim Zuweisen des Fahrzeugs");
}
}
}
Future<void> _load(LoadTour event, Emitter<TourState> emit) async {
opBloc.add(LoadOperation());
try {
emit(TourLoading());
await tourRepository.loadTourOfToday(event.teamId);
await tourRepository.loadPaymentOptions();
opBloc.add(FinishOperation());
} catch (e) {
// go to the error state in order to give the user the chance
// to reload if necessary.
if (e is UserUnauthorized) {
authBloc.add(SessionExpiredEvent());
return;
}
emit(TourLoadingFailed());
opBloc.add(
FailOperation(message: "Fehler beim Laden der heutigen Fahrten"),
);
opBloc.add(FailOperation(message: "Fehler beim Laden der heutigen Fahrten"));
}
}
@ -401,7 +374,6 @@ class TourBloc extends Bloc<TourEvent, TourState> {
Emitter<TourState> emit,
) async {
final currentState = state;
opBloc.add(LoadOperation());
if (currentState is TourLoaded) {
try {
@ -415,11 +387,9 @@ class TourBloc extends Bloc<TourEvent, TourState> {
);
await tourRepository.finishDelivery(event.deliveryId);
opBloc.add(FinishOperation());
} catch (e, st) {
opBloc.add(FailOperation(message: "Failed to update delivery"));
debugPrint(st.toString());
debugPrint("$e $st");
_handleError(e, "Fehler beim Abschließen der Lieferung");
}
}
}
@ -429,14 +399,10 @@ class TourBloc extends Bloc<TourEvent, TourState> {
Emitter<TourState> emit,
) async {
try {
opBloc.add(LoadOperation());
await tourRepository.updatePayment(event.deliveryId, event.payment);
opBloc.add(FinishOperation());
} catch (e, st) {
debugPrint(st.toString());
opBloc.add(
FailOperation(message: "Fehler beim Aktualisieren des Betrags"),
);
debugPrint("$e $st");
_handleError(e, "Fehler beim Aktualisieren des Betrags");
}
}
@ -445,18 +411,14 @@ class TourBloc extends Bloc<TourEvent, TourState> {
Emitter<TourState> emit,
) async {
try {
opBloc.add(LoadOperation());
await tourRepository.updateOption(
event.deliveryId,
event.key,
event.value,
);
opBloc.add(FinishOperation());
} catch (e, st) {
debugPrint("$st");
opBloc.add(
FailOperation(message: "Fehler beim Aktualisieren der Optionen"),
);
debugPrint("$e $st");
_handleError(e, "Fehler beim Aktualisieren der Optionen");
}
}
@ -464,26 +426,15 @@ class TourBloc extends Bloc<TourEvent, TourState> {
UpdateDiscountEvent event,
Emitter<TourState> emit,
) async {
opBloc.add(LoadOperation());
try {
opBloc.add(FinishOperation());
await tourRepository.updateDiscount(
event.deliveryId,
event.reason,
event.value,
);
opBloc.add(FinishOperation());
} catch (e, st) {
debugPrint(
"Fehler beim Hinzufügen eins Discounts zur Lieferung: ${event.deliveryId}:",
);
debugPrint("$e");
debugPrint("$st");
opBloc.add(
FailOperation(message: "Fehler beim Hinzufügen des Discounts: $e"),
);
debugPrint("Fehler beim Aktualisieren des Discounts: $e $st");
_handleError(e, "Fehler beim Aktualisieren des Discounts");
}
}
@ -491,51 +442,28 @@ class TourBloc extends Bloc<TourEvent, TourState> {
RemoveDiscountEvent event,
Emitter<TourState> emit,
) async {
opBloc.add(LoadOperation());
try {
await tourRepository.removeDiscount(event.deliveryId);
opBloc.add(FinishOperation());
} catch (e, st) {
debugPrint(
"Fehler beim Löschen des Discounts der Lieferung: ${event.deliveryId}:",
);
debugPrint("$e");
debugPrint("$st");
opBloc.add(
FailOperation(message: "Fehler beim Löschen des Discounts: $e"),
);
debugPrint("Fehler beim Löschen des Discounts: $e $st");
_handleError(e, "Fehler beim Löschen des Discounts");
}
}
void _addDiscount(AddDiscountEvent event, Emitter<TourState> emit) async {
opBloc.add(LoadOperation());
try {
await tourRepository.addDiscount(
event.deliveryId,
event.reason,
event.value,
);
opBloc.add(FinishOperation());
} catch (e, st) {
debugPrint(
"Fehler beim Hinzufügen eins Discounts zur Lieferung: ${event.deliveryId}:",
);
debugPrint("$e");
debugPrint("$st");
opBloc.add(
FailOperation(message: "Fehler beim Hinzufügen des Discounts: $e"),
);
debugPrint("Fehler beim Hinzufügen des Discounts: $e $st");
_handleError(e, "Fehler beim Hinzufügen des Discounts");
}
}
void _unscan(UnscanArticleEvent event, Emitter<TourState> emit) async {
opBloc.add(LoadOperation());
try {
await tourRepository.unscan(
event.deliveryId,
@ -543,29 +471,18 @@ class TourBloc extends Bloc<TourEvent, TourState> {
event.newAmount,
event.reason,
);
opBloc.add(FinishOperation());
} catch (e, st) {
debugPrint("Fehler beim Unscan des Artikels: ${event.articleId}:");
debugPrint("$e");
debugPrint("$st");
opBloc.add(FailOperation(message: "Fehler beim Unscan des Artikels: $e"));
debugPrint("Fehler beim Unscan des Artikels ${event.articleId}: $e $st");
_handleError(e, "Fehler beim Unscan des Artikels");
}
}
void _resetAmount(ResetScanAmountEvent event, Emitter<TourState> emit) async {
opBloc.add(LoadOperation());
try {
await tourRepository.resetScan(event.articleId, event.deliveryId);
opBloc.add(FinishOperation());
} catch (e, st) {
debugPrint("Fehler beim Unscan des Artikels: ${event.articleId}:");
debugPrint("$e");
debugPrint("$st");
opBloc.add(FailOperation(message: "Fehler beim Zurücksetzen: $e"));
debugPrint("Fehler beim Zurücksetzen Artikel ${event.articleId}: $e $st");
_handleError(e, "Fehler beim Zurücksetzen");
}
}
}

View File

@ -3,6 +3,9 @@ import 'dart:typed_data';
import 'package:flutter/cupertino.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_event.dart';
import 'package:hl_lieferservice/feature/authentication/exceptions.dart';
import 'package:hl_lieferservice/widget/operations/bloc/operation_bloc.dart';
import 'package:hl_lieferservice/widget/operations/bloc/operation_event.dart';
import 'package:rxdart/rxdart.dart';
@ -15,6 +18,7 @@ import 'package:hl_lieferservice/feature/delivery/detail/repository/note_reposit
class NoteBloc extends Bloc<NoteEvent, NoteState> {
final NoteRepository repository;
final OperationBloc opBloc;
final AuthBloc authBloc;
final String deliveryId;
StreamSubscription? _combinedSubscription;
@ -22,6 +26,7 @@ class NoteBloc extends Bloc<NoteEvent, NoteState> {
NoteBloc({
required this.repository,
required this.opBloc,
required this.authBloc,
required this.deliveryId,
}) : super(NoteInitial()) {
_combinedSubscription = CombineLatestStream.combine3(
@ -60,10 +65,17 @@ class NoteBloc extends Bloc<NoteEvent, NoteState> {
@override
Future<void> close() {
_combinedSubscription?.cancel();
return super.close();
}
void _handleError(Object e, String fallbackMessage) {
if (e is UserUnauthorized) {
authBloc.add(SessionExpiredEvent());
} else {
opBloc.add(FailOperation(message: fallbackMessage));
}
}
Future<void> _dataUpdated(DataUpdated event, Emitter<NoteState> emit) async {
emit(
NoteLoaded(
@ -82,32 +94,21 @@ class NoteBloc extends Bloc<NoteEvent, NoteState> {
RemoveImageNote event,
Emitter<NoteState> emit,
) async {
opBloc.add(LoadOperation());
try {
await repository.deleteImage(event.deliveryId, event.objectId);
opBloc.add(FinishOperation());
} catch (e, st) {
debugPrint("Fehler beim Löschen des Bildes: $e");
debugPrint(st.toString());
opBloc.add(FailOperation(message: e.toString()));
debugPrint("Fehler beim Löschen des Bildes: $e $st");
_handleError(e, "Fehler beim Löschen des Bildes");
}
}
Future<void> _upload(AddImageNote event, Emitter<NoteState> emit) async {
opBloc.add(LoadOperation());
try {
Uint8List imageBytes = await event.file.readAsBytes();
await repository.addImage(event.deliveryId, imageBytes);
opBloc.add(FinishOperation());
} catch (e, st) {
debugPrint("Fehler beim Hinzufügen des Bildes: $e");
debugPrint(st.toString());
opBloc.add(FailOperation(message: e.toString()));
debugPrint("Fehler beim Hinzufügen des Bildes: $e $st");
_handleError(e, "Fehler beim Hinzufügen des Bildes");
}
}
@ -117,61 +118,41 @@ class NoteBloc extends Bloc<NoteEvent, NoteState> {
try {
await repository.loadNotes(event.delivery.id);
await repository.loadTemplates();
opBloc.add(FinishOperation());
} catch (e, st) {
debugPrint("Fehler beim Herunterladen der Notizen: $e");
debugPrint(st.toString());
opBloc.add(
FailOperation(message: "Notizen konnten nicht heruntergeladen werden."),
);
debugPrint("Fehler beim Herunterladen der Notizen: $e $st");
if (e is UserUnauthorized) {
authBloc.add(SessionExpiredEvent());
return;
}
opBloc.add(FailOperation(message: "Notizen konnten nicht heruntergeladen werden."));
emit.call(NoteLoadingFailed());
}
}
Future<void> _add(AddNote event, Emitter<NoteState> emit) async {
opBloc.add(LoadOperation());
try {
await repository.addNote(event.deliveryId, event.note);
opBloc.add(FinishOperation());
} catch (e, st) {
debugPrint("Fehler beim Hinzufügen der Notiz: $e");
debugPrint(st.toString());
opBloc.add(FailOperation(message: e.toString()));
debugPrint("Fehler beim Hinzufügen der Notiz: $e $st");
_handleError(e, "Fehler beim Hinzufügen der Notiz");
}
}
Future<void> _edit(EditNote event, Emitter<NoteState> emit) async {
opBloc.add(LoadOperation());
try {
await repository.editNote(event.noteId, event.content);
opBloc.add(FinishOperation());
} catch (e, st) {
debugPrint("Fehler beim Hinzufügen der Notiz: $e");
debugPrint(st.toString());
opBloc.add(FailOperation(message: e.toString()));
debugPrint("Fehler beim Editieren der Notiz: $e $st");
_handleError(e, "Fehler beim Editieren der Notiz");
}
}
Future<void> _remove(RemoveNote event, Emitter<NoteState> emit) async {
opBloc.add(LoadOperation());
try {
await repository.deleteNote(event.noteId);
opBloc.add(FinishOperation());
} catch (e, st) {
debugPrint("Fehler beim Hinzufügen der Notiz: $e");
debugPrint(st.toString());
opBloc.add(
FailOperation(message: "Notizen konnte nicht gelöscht werden."),
);
debugPrint("Fehler beim Löschen der Notiz: $e $st");
_handleError(e, "Notiz konnte nicht gelöscht werden");
}
}
}

View File

@ -8,6 +8,8 @@ import 'package:url_launcher/url_launcher.dart';
import '../../../bloc/tour_bloc.dart';
import '../../../bloc/tour_state.dart';
enum _StatusAction { hold, cancel, reactivate }
class DeliveryStepInfo extends StatefulWidget {
final Delivery delivery;
@ -39,76 +41,73 @@ class _DeliveryStepInfo extends State<DeliveryStepInfo> {
await launchUrl(url, mode: LaunchMode.externalApplication);
}
Widget _deliveryStatusChangeActions() {
List<Widget> actions = [];
Widget _statusOverflow() {
final state = widget.delivery.state;
final List<PopupMenuEntry<_StatusAction>> entries;
if (widget.delivery.state == DeliveryState.ongoing) {
actions = [
Column(
children: [
IconButton(
onPressed: () {
context.read<TourBloc>().add(
HoldDeliveryEvent(deliveryId: widget.delivery.id),
);
Navigator.of(context).pop();
},
icon: Icon(
Icons.change_circle,
color: Colors.orangeAccent,
size: 42,
),
),
Text("Zurückstellen"),
],
if (state == DeliveryState.ongoing) {
entries = const [
PopupMenuItem(
value: _StatusAction.hold,
child: Row(
children: [
Icon(Icons.change_circle, color: Colors.orangeAccent),
SizedBox(width: 12),
Text("Zurückstellen"),
],
),
),
Column(
children: [
IconButton(
onPressed: () {
context.read<TourBloc>().add(
CancelDeliveryEvent(deliveryId: widget.delivery.id),
);
Navigator.of(context).pop();
},
//style: IconButton.styleFrom(backgroundColor: Colors.red),
icon: Icon(Icons.cancel, color: Colors.red, size: 42),
),
Text("Abbrechen"),
],
PopupMenuItem(
value: _StatusAction.cancel,
child: Row(
children: [
Icon(Icons.cancel, color: Colors.red),
SizedBox(width: 12),
Text("Abbrechen"),
],
),
),
];
} else {
entries = const [
PopupMenuItem(
value: _StatusAction.reactivate,
child: Row(
children: [
Icon(Icons.published_with_changes, color: Colors.blueAccent),
SizedBox(width: 12),
Text("Reaktivieren"),
],
),
),
];
}
if (widget.delivery.state == DeliveryState.canceled ||
widget.delivery.state == DeliveryState.onhold ||
widget.delivery.state == DeliveryState.finished) {
actions = [
Column(
children: [
IconButton(
onPressed: () {
context.read<TourBloc>().add(
ReactivateDeliveryEvent(deliveryId: widget.delivery.id),
);
},
icon: Icon(
Icons.published_with_changes,
color: Colors.blueAccent,
size: 42
),
),
Text("Reaktivieren"),
],
),
];
}
return Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: actions,
return PopupMenuButton<_StatusAction>(
icon: const Icon(Icons.more_vert),
tooltip: "Status ändern",
itemBuilder: (context) => entries,
onSelected: (action) {
switch (action) {
case _StatusAction.hold:
context.read<TourBloc>().add(
HoldDeliveryEvent(deliveryId: widget.delivery.id),
);
Navigator.of(context).pop();
break;
case _StatusAction.cancel:
context.read<TourBloc>().add(
CancelDeliveryEvent(deliveryId: widget.delivery.id),
);
Navigator.of(context).pop();
break;
case _StatusAction.reactivate:
context.read<TourBloc>().add(
ReactivateDeliveryEvent(deliveryId: widget.delivery.id),
);
break;
}
},
);
}
@ -119,55 +118,46 @@ class _DeliveryStepInfo extends State<DeliveryStepInfo> {
color: Theme.of(context).colorScheme.onSecondary,
child: Padding(
padding: const EdgeInsets.all(10),
child: Column(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Column(
children: [
IconButton.filled(
onPressed:
widget.delivery.contactPerson?.phoneNumber != null
? () async {
Expanded(
child: Builder(
builder: (context) {
final phone = widget.delivery.contactPerson?.phoneNumber;
final bool hasPhone = phone != null && phone.isNotEmpty;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
IconButton.filled(
onPressed: hasPhone
? () async {
await launchUrl(
Uri(
scheme: "tel",
path:
widget
.delivery
.contactPerson
?.phoneNumber!,
),
Uri(scheme: "tel", path: phone),
);
}
: null,
icon: Icon(Icons.phone),
),
Text("Anrufen"),
],
),
Column(
children: [
IconButton.filled(
onPressed: () {
_launchMapsUrl("google");
},
icon: Icon(Icons.map_outlined),
),
Text("Google Maps"),
],
),
],
: null,
icon: const Icon(Icons.phone),
),
const Text("Anrufen"),
],
);
},
),
),
const Padding(
padding: EdgeInsets.only(top: 10, bottom: 10),
child: Divider(),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
IconButton.filled(
onPressed: () => _launchMapsUrl("google"),
icon: const Icon(Icons.map_outlined),
),
const Text("Google Maps"),
],
),
),
_deliveryStatusChangeActions(),
_statusOverflow(),
],
),
),
@ -176,6 +166,16 @@ class _DeliveryStepInfo extends State<DeliveryStepInfo> {
}
Widget _customerInformation() {
final phone = widget.delivery.contactPerson?.phoneNumber;
final String phoneText = (phone != null && phone.isNotEmpty)
? phone
: "keine Nummer angegeben";
final email = widget.delivery.customer.email;
final String emailText = (email != null && email.isNotEmpty)
? email
: "keine E-Mail angegeben";
return SizedBox(
width: double.infinity,
child: Card(
@ -228,9 +228,24 @@ class _DeliveryStepInfo extends State<DeliveryStepInfo> {
Icon(Icons.phone, color: Theme.of(context).primaryColor),
Padding(
padding: const EdgeInsets.only(left: 10),
child: Text(
widget.delivery.contactPerson?.phoneNumber.toString() ??
"",
child: Text(phoneText),
),
],
),
),
Padding(
padding: const EdgeInsets.only(top: 15),
child: Row(
children: [
Icon(Icons.mail, color: Theme.of(context).primaryColor),
Expanded(
child: Padding(
padding: const EdgeInsets.only(left: 10),
child: Text(
emailText,
overflow: TextOverflow.ellipsis,
),
),
),
],
@ -275,28 +290,66 @@ class _DeliveryStepInfo extends State<DeliveryStepInfo> {
);
}
Widget _deliveryAgreements() {
Widget _agreementsAndDesiredTime() {
String agreements = "keine Vereinbarungen getroffen!";
if (widget.delivery.specialAgreements != null &&
widget.delivery.specialAgreements != "") {
agreements = widget.delivery.specialAgreements!;
}
final desiredTime = widget.delivery.desiredTime;
final bool hasDesiredTime = desiredTime != null && desiredTime.isNotEmpty;
final primary = Theme.of(context).primaryColor;
return Card(
color: Theme.of(context).colorScheme.onSecondary,
child: Padding(
padding: const EdgeInsets.all(10),
child: Row(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: EdgeInsets.all(15),
child: Icon(
Icons.warning,
color: Theme.of(context).primaryColor,
size: 28,
if (hasDesiredTime) ...[
Row(
children: [
Padding(
padding: const EdgeInsets.all(15),
child: Icon(Icons.schedule, color: primary, size: 28),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Wunschtermin",
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
Text(
desiredTime,
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: primary,
),
),
],
),
),
],
),
const Divider(height: 24),
],
Row(
children: [
Padding(
padding: const EdgeInsets.all(15),
child: Icon(Icons.warning, color: primary, size: 28),
),
Expanded(child: Text(agreements)),
],
),
Expanded(child: Text(agreements)),
],
),
),
@ -330,7 +383,7 @@ class _DeliveryStepInfo extends State<DeliveryStepInfo> {
),
Padding(
padding: const EdgeInsets.only(top: 10),
child: _deliveryAgreements(),
child: _agreementsAndDesiredTime(),
),
Padding(

View File

@ -275,10 +275,14 @@ class NoteService {
LocalDocuFrameConfiguration config = getConfig();
return urls.map((url) async {
return (await http.get(
final response = await http.get(
Uri.parse("${config.backendUrl}$url"),
headers: getSessionOrThrow(),
)).bodyBytes;
);
if (response.statusCode == HttpStatus.unauthorized) {
throw UserUnauthorized();
}
return response.bodyBytes;
}).toList();
} catch (e, st) {
debugPrint("An error occured:");

View File

@ -1,80 +1,86 @@
import 'package:flutter/material.dart';
import 'package:hl_lieferservice/model/delivery.dart' show DeliveryState;
import 'package:hl_lieferservice/model/tour.dart';
import 'package:intl/intl.dart';
class DeliveryInfo extends StatelessWidget {
final Tour tour;
final int? selectedCarId;
const DeliveryInfo({super.key, required this.tour});
const DeliveryInfo({super.key, required this.tour, this.selectedCarId});
@override
Widget build(BuildContext context) {
String date = DateFormat("dd.MM.yyyy").format(tour.date);
String amountDeliveries = tour.deliveries.length.toString();
final String date = DateFormat("dd.MM.yyyy").format(tour.date);
final relevantDeliveries = selectedCarId != null
? tour.deliveries.where((d) => d.carId == selectedCarId).toList()
: tour.deliveries;
final total = relevantDeliveries.length;
final done = relevantDeliveries
.where((d) => d.state == DeliveryState.finished)
.length;
final progress = total > 0 ? done / total : 0.0;
final allDone = total > 0 && done == total;
return Padding(
padding: const EdgeInsets.all(10),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(bottom: 10),
child: Text(
"Informationen",
style: Theme.of(context).textTheme.headlineSmall,
),
),
SizedBox(
width: double.infinity,
child: Card(
color: Theme.of(context).colorScheme.onSecondary,
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 15),
child: SizedBox(
width: double.infinity,
child: Card(
color: Theme.of(context).colorScheme.onSecondary,
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(Icons.calendar_month),
Padding(
padding: const EdgeInsets.only(left: 5),
child: Text("Datum"),
),
],
const Icon(Icons.calendar_month),
const Padding(
padding: EdgeInsets.only(left: 5),
child: Text("Datum"),
),
Text(date),
],
),
Padding(
padding: const EdgeInsets.only(top: 15),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(Icons.local_shipping_outlined),
Padding(
padding: const EdgeInsets.only(left: 5),
child: Text("Lieferungen"),
),
],
),
Text(amountDeliveries),
],
),
),
Text(date),
],
),
),
const SizedBox(height: 15),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
const Icon(Icons.local_shipping_outlined),
const Padding(
padding: EdgeInsets.only(left: 5),
child: Text("Lieferungen"),
),
],
),
Text("$done / $total"),
],
),
const SizedBox(height: 10),
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: progress,
minHeight: 6,
backgroundColor:
Theme.of(context).colorScheme.surfaceContainerHighest,
valueColor: AlwaysStoppedAnimation<Color>(
allDone ? Colors.green : Theme.of(context).primaryColor,
),
),
),
],
),
),
],
),
),
);
}

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hl_lieferservice/feature/authentication/bloc/auth_bloc.dart';
import 'package:hl_lieferservice/model/delivery.dart';
import 'package:hl_lieferservice/feature/delivery/detail/presentation/delivery_detail_page.dart';
@ -18,60 +19,132 @@ class DeliveryListItem extends StatelessWidget {
required this.distance,
});
Widget _leading(BuildContext context) {
if (delivery.state == DeliveryState.finished) {
return Icon(Icons.check_circle, color: Colors.green);
}
if (delivery.state == DeliveryState.canceled) {
return Icon(Icons.cancel_rounded, color: Colors.red);
}
if (delivery.state == DeliveryState.onhold) {
return Icon(Icons.pause_circle, color: Colors.orange);
}
return Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Icon(Icons.location_on, color: Theme.of(context).primaryColor),
Text("${distance.toStringAsFixed(2)}km"),
],
);
}
void _goToDelivery(BuildContext context) {
Navigator.of(context).push(
MaterialPageRoute(
builder:
(context) => BlocProvider(
create:
(context) => NoteBloc(
deliveryId: delivery.id,
opBloc: context.read<OperationBloc>(),
repository: NoteRepository(
service: NoteService(),
),
),
child: DeliveryDetail(deliveryId: delivery.id),
),
builder: (context) => BlocProvider(
create: (context) => NoteBloc(
deliveryId: delivery.id,
opBloc: context.read<OperationBloc>(),
authBloc: context.read<AuthBloc>(),
repository: NoteRepository(service: NoteService()),
),
child: DeliveryDetail(deliveryId: delivery.id),
),
),
);
}
(Color, Color, IconData, String) _stateStyle(BuildContext context) {
switch (delivery.state) {
case DeliveryState.finished:
return (
Colors.green.withValues(alpha: 0.07),
Colors.green.withValues(alpha: 0.35),
Icons.check_circle_rounded,
"Abgeschlossen",
);
case DeliveryState.canceled:
return (
Colors.red.withValues(alpha: 0.07),
Colors.red.withValues(alpha: 0.35),
Icons.cancel_rounded,
"Storniert",
);
case DeliveryState.onhold:
return (
Colors.orange.withValues(alpha: 0.07),
Colors.orange.withValues(alpha: 0.35),
Icons.pause_circle_rounded,
"Pausiert",
);
case DeliveryState.ongoing:
return (
Theme.of(context).colorScheme.surfaceContainerLow,
Colors.transparent,
Icons.local_shipping_outlined,
"${distance.toStringAsFixed(1)} km",
);
}
}
@override
Widget build(BuildContext context) {
return ListTile(
title: Text(
delivery.customer.name,
style: Theme.of(context).textTheme.titleMedium,
final (cardColor, borderColor, icon, statusLabel) = _stateStyle(context);
final isOngoing = delivery.state == DeliveryState.ongoing;
final iconColor = switch (delivery.state) {
DeliveryState.finished => Colors.green,
DeliveryState.canceled => Colors.red,
DeliveryState.onhold => Colors.orange,
DeliveryState.ongoing => Theme.of(context).primaryColor,
};
return Card(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
elevation: 0,
color: cardColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(color: borderColor),
),
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () => _goToDelivery(context),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
Icon(icon, color: iconColor, size: 28),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
delivery.customer.name,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
color: isOngoing ? null : iconColor,
),
),
const SizedBox(height: 2),
Text(
delivery.customer.address.toString(),
style: TextStyle(
fontSize: 13,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
),
const SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
statusLabel,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: isOngoing
? Theme.of(context).colorScheme.onSurfaceVariant
: iconColor,
),
),
const SizedBox(height: 4),
Icon(
Icons.arrow_forward_ios,
size: 14,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
],
),
],
),
),
),
leading: _leading(context),
tileColor: Theme.of(context).colorScheme.surfaceContainerHigh,
subtitle: Text(delivery.customer.address.toString()),
trailing: Icon(Icons.arrow_forward_ios),
onTap: () => _goToDelivery(context),
);
}
}

View File

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
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/delivery/overview/model/sorting_information.dart';
import 'package:hl_lieferservice/feature/delivery/overview/presentation/delivery_overview.dart';
import 'package:hl_lieferservice/model/delivery.dart';
@ -56,79 +56,66 @@ class _DeliveryListState extends State<DeliveryList> {
builder: (context, state) {
final currentState = state;
if (currentState is TourLoaded) {
List<Delivery> deliveries =
currentState.tour.deliveries
.where(
(delivery) =>
delivery.carId == widget.selectedCarId &&
delivery.allArticlesScanned() &&
delivery.state != DeliveryState.finished,
)
.toList();
if (widget.sortType == SortType.custom) {
return _showCustomSortedList(
currentState.tour.deliveries,
currentState.sortingInformation[widget.selectedCarId.toString()] ?? [],
currentState.distances ?? {},
);
}
List<Delivery> finishedDeliveries =
currentState.tour.deliveries
.where(
(delivery) =>
delivery.state == DeliveryState.finished &&
delivery.carId == widget.selectedCarId,
)
.toList();
final allDeliveries = currentState.tour.deliveries
.where((d) => d.carId == widget.selectedCarId)
.toList();
if (deliveries.isEmpty) {
if (allDeliveries.isEmpty) {
return ListView(
physics: NeverScrollableScrollPhysics(),
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
children: [
Center(child: const Text("Keine Auslieferungen gefunden")),
children: const [
Center(child: Text("Keine Auslieferungen gefunden")),
],
);
}
final ongoing = allDeliveries
.where((d) => d.state == DeliveryState.ongoing)
.toList();
final nonOngoing = allDeliveries
.where((d) => d.state != DeliveryState.ongoing)
.toList();
int Function(Delivery, Delivery) comparator;
switch (widget.sortType) {
case SortType.custom:
return _showCustomSortedList(
currentState.tour.deliveries,
currentState.sortingInformation[widget.selectedCarId.toString()] ?? [],
currentState.distances ?? {},
);
case SortType.nameAsc:
deliveries.sort(
(a, b) => a.customer.name.compareTo(b.customer.name),
);
comparator = (a, b) => a.customer.name.compareTo(b.customer.name);
break;
case SortType.nameDesc:
deliveries.sort(
(a, b) => b.customer.name.compareTo(a.customer.name),
);
comparator = (a, b) => b.customer.name.compareTo(a.customer.name);
break;
case SortType.distance:
deliveries.sort(
(a, b) => (currentState.distances![a.id] ?? 0.0).compareTo(
currentState.distances![b.id] ?? 0.0,
),
);
comparator = (a, b) =>
(currentState.distances?[a.id] ?? 0.0)
.compareTo(currentState.distances?[b.id] ?? 0.0);
break;
default:
comparator = (a, b) => a.customer.name.compareTo(b.customer.name);
}
//deliveries.addAll(finishedDeliveries);
ongoing.sort(comparator);
nonOngoing.sort(comparator);
return ListView.separated(
separatorBuilder: (context, index) => const Divider(height: 0),
final sorted = [...ongoing, ...nonOngoing];
return ListView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
Delivery delivery = deliveries[index];
return DeliveryListItem(
delivery: delivery,
distance: currentState.distances?[delivery.id] ?? 0.0,
);
},
itemCount: deliveries.length,
physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.only(bottom: 8),
itemCount: sorted.length,
itemBuilder: (context, index) => DeliveryListItem(
delivery: sorted[index],
distance: currentState.distances?[sorted[index].id] ?? 0.0,
),
);
}

View File

@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
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/delivery/bloc/tour_bloc.dart';
import 'package:hl_lieferservice/feature/delivery/bloc/tour_event.dart';
import 'package:hl_lieferservice/feature/delivery/overview/presentation/delivery_info.dart';
@ -34,8 +36,14 @@ class _DeliveryOverviewState extends State<DeliveryOverview> {
void initState() {
super.initState();
// Select the first car for initialization
_selectedCarId = widget.tour.driver.cars.firstOrNull?.id;
// Pre-select today's car from the daily car selection.
// Falls back to the first available car if no selection exists.
final carSelectState = context.read<CarSelectBloc>().state;
if (carSelectState is CarSelectComplete) {
_selectedCarId = carSelectState.selectedCar.id;
} else {
_selectedCarId = widget.tour.driver.cars.firstOrNull?.id;
}
_sortType = SortType.nameAsc;
}
@ -44,54 +52,6 @@ class _DeliveryOverviewState extends State<DeliveryOverview> {
context.read<TourBloc>().add(LoadTour(teamId: state.user.number));
}
Widget _carSelection() {
return SizedBox(
width: double.infinity,
height: 50,
child: ListView(
scrollDirection: Axis.horizontal,
children:
widget.tour.driver.cars.map((car) {
Color? backgroundColor;
Color? iconColor = Theme.of(context).primaryColor;
Color? textColor;
if (_selectedCarId == car.id) {
backgroundColor = Theme.of(context).primaryColor;
textColor = Theme.of(context).colorScheme.onSecondary;
iconColor = Theme.of(context).colorScheme.onSecondary;
}
return Padding(
padding: const EdgeInsets.only(right: 8),
child: GestureDetector(
onTap: () {
setState(() {
_selectedCarId = car.id;
});
},
child: Chip(
backgroundColor: backgroundColor,
label: Row(
children: [
Icon(Icons.local_shipping, color: iconColor, size: 20),
Padding(
padding: const EdgeInsets.only(left: 5),
child: Text(
car.plate,
style: TextStyle(color: textColor, fontSize: 12),
),
),
],
),
),
),
);
}).toList(),
),
);
}
/// Highlight the text of the active sorting type.
TextStyle? _popupItemTextStyle() {
return TextStyle(color: Theme.of(context).primaryColor, fontWeight: FontWeight.bold);
@ -99,17 +59,23 @@ class _DeliveryOverviewState extends State<DeliveryOverview> {
@override
Widget build(BuildContext context) {
return RefreshIndicator(
return BlocListener<CarSelectBloc, CarSelectState>(
listener: (context, carState) {
if (carState is CarSelectComplete) {
setState(() => _selectedCarId = carState.selectedCar.id);
}
},
child: RefreshIndicator(
onRefresh: _loadTour,
child: ListView(
//crossAxisAlignment: CrossAxisAlignment.start,
children: [
DeliveryInfo(tour: widget.tour),
DeliveryInfo(tour: widget.tour, selectedCarId: _selectedCarId),
Padding(
padding: const EdgeInsets.only(
left: 10,
right: 10,
top: 15,
top: 0,
bottom: 10,
),
child: Row(
@ -191,16 +157,13 @@ class _DeliveryOverviewState extends State<DeliveryOverview> {
],
),
),
Padding(
padding: const EdgeInsets.only(left: 10, right: 10, bottom: 20),
child: _carSelection(),
),
DeliveryList(
selectedCarId: _selectedCarId,
sortType: _sortType,
),
],
),
),
);
}
}

View File

@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
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/delivery/overview/presentation/delivery_fail_page.dart';
import 'package:hl_lieferservice/feature/delivery/overview/presentation/delivery_overview.dart';
@ -16,25 +18,55 @@ class DeliveryOverviewPage extends StatefulWidget {
class _DeliveryOverviewPageState extends State<DeliveryOverviewPage> {
@override
Widget build(BuildContext context) {
return BlocBuilder<TourBloc, TourState>(
builder: (context, state) {
if (state is TourLoaded) {
final currentState = state;
final carState = context.watch<CarSelectBloc>().state;
return Center(
child: DeliveryOverview(
tour: currentState.tour,
distances: currentState.distances ?? {},
return Scaffold(
appBar: AppBar(
title: const Text("Auslieferung"),
centerTitle: false,
backgroundColor: Theme.of(context).primaryColor,
foregroundColor: Theme.of(context).colorScheme.onSecondary,
actions: [
if (carState is CarSelectComplete)
Padding(
padding: const EdgeInsets.only(right: 16),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.local_shipping,
color: Theme.of(context).colorScheme.onSecondary,
size: 20,
),
const SizedBox(width: 6),
Text(
carState.selectedCar.plate,
style: TextStyle(
color: Theme.of(context).colorScheme.onSecondary,
fontWeight: FontWeight.bold,
),
),
],
),
),
);
}
],
),
body: BlocBuilder<TourBloc, TourState>(
builder: (context, state) {
if (state is TourLoaded) {
return DeliveryOverview(
tour: state.tour,
distances: state.distances ?? {},
);
}
if (state is TourLoadingFailed) {
return DeliveryLoadingFailedPage();
}
if (state is TourLoadingFailed) {
return DeliveryLoadingFailedPage();
}
return Container();
},
return const Center(child: CircularProgressIndicator());
},
),
);
}
}

View File

@ -80,6 +80,8 @@ class TourRepository {
if (article.scannedAmount < article.amount) {
article.scannedAmount += 1;
delivery.carId = int.tryParse(carId) ?? delivery.carId;
await service.assignCar(deliveryId, carId);
_tourStream.add(tour);
return ScanResult.scanned;
} else {

View File

@ -2,6 +2,7 @@ import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:hl_lieferservice/dto/delivery_response.dart';
import 'package:hl_lieferservice/dto/delivery_update.dart';
import 'package:hl_lieferservice/dto/delivery_update_response.dart';
@ -271,10 +272,24 @@ class TourService {
Future<BasicResponseDTO> finishDelivery(String deliveryId) async {
try {
// ISO-8601 mit T-Separator: sprachunabhaengig fuer SQL-Server datetime.
// ('yyyy-MM-dd HH:mm:ss' OHNE T wird unter DATEFORMAT=DMY (DE) als YDM
// geparst und schlaegt fuer Tag > 12 fehl.)
// ERPframe arbeitet mit lokaler Zeit -> bewusst keine UTC-Konvertierung.
final String deliveredAt = DateFormat(
"yyyy-MM-dd'T'HH:mm:ss",
).format(DateTime.now());
var headers = {"Content-Type": "application/json"};
headers.addAll(getSessionOrThrow());
var response = await post(
urlBuilder("_web_finishDelivery"),
headers: getSessionOrThrow(),
body: {"delivery_id": deliveryId},
headers: headers,
body: jsonEncode({
"delivery_id": deliveryId,
"delivered_at": deliveredAt,
}),
);
if (response.statusCode == HttpStatus.unauthorized) {