Finished custom sorting of deliveries

This commit is contained in:
Dennis Nemec
2026-01-07 15:19:34 +01:00
parent 9111dc92db
commit 622967e5c1
8 changed files with 369 additions and 41 deletions

View File

@ -4,8 +4,10 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hl_lieferservice/feature/delivery/overview/bloc/tour_event.dart'; import 'package:hl_lieferservice/feature/delivery/overview/bloc/tour_event.dart';
import 'package:hl_lieferservice/feature/delivery/overview/bloc/tour_state.dart'; import 'package:hl_lieferservice/feature/delivery/overview/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/overview/repository/tour_repository.dart';
import 'package:hl_lieferservice/feature/delivery/overview/service/distance_service.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'; import 'package:hl_lieferservice/model/tour.dart';
import 'package:hl_lieferservice/widget/operations/bloc/operation_bloc.dart'; import 'package:hl_lieferservice/widget/operations/bloc/operation_bloc.dart';
import 'package:hl_lieferservice/widget/operations/bloc/operation_event.dart'; import 'package:hl_lieferservice/widget/operations/bloc/operation_event.dart';
@ -50,6 +52,8 @@ class TourBloc extends Bloc<TourEvent, TourState> {
on<FinishDeliveryEvent>(_finishDelivery); on<FinishDeliveryEvent>(_finishDelivery);
on<TourUpdated>(_updated); on<TourUpdated>(_updated);
on<RequestDeliveryDistanceEvent>(_calculateDistances); on<RequestDeliveryDistanceEvent>(_calculateDistances);
on<RequestSortingInformationEvent>(_requestSortingInformation);
on<ReorderDeliveryEvent>(_reorderDelivery);
} }
@override @override
@ -59,6 +63,36 @@ class TourBloc extends Bloc<TourEvent, TourState> {
return super.close(); return super.close();
} }
void _reorderDelivery(
ReorderDeliveryEvent event,
Emitter<TourState> emit,
) 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
.sortingInformation
.sorting
.firstWhere((info) => info.position == newPosition);
information.position = event.oldPosition;
informationOld.position = newPosition;
await ReorderService().saveSortingInformation(
currentState.sortingInformation,
);
emit(
currentState.copyWith(
sortingInformation: currentState.sortingInformation.copyWith(),
),
);
}
}
void _calculateDistances( void _calculateDistances(
RequestDeliveryDistanceEvent event, RequestDeliveryDistanceEvent event,
Emitter<TourState> emit, Emitter<TourState> emit,
@ -79,15 +113,104 @@ class TourBloc extends Bloc<TourEvent, TourState> {
debugPrint("Fehler beim Berechnen der Distanzen: $e"); debugPrint("Fehler beim Berechnen der Distanzen: $e");
opBloc.add(FailOperation(message: "Fehler beim Berechnen der Distanzen")); opBloc.add(FailOperation(message: "Fehler beim Berechnen der Distanzen"));
return; return;
} finally {
// Independent of error state fetch the sorting information
add(
RequestSortingInformationEvent(
tour: event.tour,
payments: event.payments,
distances: distances,
),
);
} }
}
emit( void _requestSortingInformation(
TourLoaded( RequestSortingInformationEvent event,
tour: event.tour, Emitter<TourState> emit,
paymentOptions: event.payments, ) async {
distances: distances, try {
), ReorderService service = ReorderService();
); SortingInformationContainer container = SortingInformationContainer(
sorting: [],
);
// Create empty default value if it does not exist yet
if (!service.orderInformationExist()) {
await service.initializeTour(event.tour);
}
// Populate the container with information. If the file did not exist then it
// now contains the standard values.
container = await service.loadSortingInformation();
bool inconsistent = false;
for (final delivery in event.tour.deliveries) {
int info = container.sorting.indexWhere(
(info) => info.deliveryId == delivery.id,
);
int max = container.sorting.fold(0, (acc, element) {
if (element.position > acc) {
return element.position;
}
return acc;
});
// not found, so add it to the list
if (info == -1) {
inconsistent = true;
container.sorting.add(
SortingInformation(
deliveryId: delivery.id,
position: container.sorting.isEmpty ? 0 : max + 1,
),
);
}
}
// if new deliveries were added then save the information with the newly
// populated container
if (inconsistent) {
await service.saveSortingInformation(container);
}
emit(
TourLoaded(
tour: event.tour,
paymentOptions: event.payments,
sortingInformation: container,
distances: event.distances,
),
);
} catch (e, st) {
debugPrint("Fehler beim Lesen der Datei: $e");
debugPrint("$st");
opBloc.add(
FailOperation(
message:
"Fehler beim Laden der Sortierung. Es wird ohne Sortierung fortgefahren",
),
);
SortingInformationContainer container = SortingInformationContainer(
sorting: [],
);
for (final (index, delivery) in event.tour.deliveries.indexed) {
container.sorting.add(
SortingInformation(deliveryId: delivery.id, position: index),
);
}
emit(
TourLoaded(
tour: event.tour,
paymentOptions: event.payments,
sortingInformation: container,
distances: event.distances,
),
);
}
} }
void _updated(TourUpdated event, Emitter<TourState> emit) { void _updated(TourUpdated event, Emitter<TourState> emit) {
@ -97,16 +220,17 @@ class TourBloc extends Bloc<TourEvent, TourState> {
event.payments.map((payment) => payment.copyWith()).toList(); event.payments.map((payment) => payment.copyWith()).toList();
if (currentState is TourLoaded) { if (currentState is TourLoaded) {
debugPrint("TEST UPDATE");
emit( emit(
TourLoaded( TourLoaded(
tour: tour, tour: tour,
paymentOptions: payments, paymentOptions: payments,
distances: Map<String, double>.from(currentState.distances ?? {}), distances: Map<String, double>.from(currentState.distances ?? {}),
sortingInformation: currentState.sortingInformation,
), ),
); );
} }
// 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), RequestDeliveryDistanceEvent(tour: tour.copyWith(), payments: payments),

View File

@ -19,6 +19,21 @@ class RequestDeliveryDistanceEvent extends TourEvent {
RequestDeliveryDistanceEvent({required this.tour, required this.payments}); RequestDeliveryDistanceEvent({required this.tour, required this.payments});
} }
class RequestSortingInformationEvent extends TourEvent {
Tour tour;
List<Payment> payments;
Map<String, double>? distances;
RequestSortingInformationEvent({required this.tour, required this.payments, this.distances});
}
class ReorderDeliveryEvent extends TourEvent {
int newPosition;
int oldPosition;
ReorderDeliveryEvent({required this.newPosition, required this.oldPosition});
}
class TourUpdated extends TourEvent { class TourUpdated extends TourEvent {
Tour tour; Tour tour;
List<Payment> payments; List<Payment> payments;

View File

@ -1,3 +1,5 @@
import 'package:hl_lieferservice/feature/delivery/overview/model/sorting_information.dart';
import '../../../../model/tour.dart'; import '../../../../model/tour.dart';
abstract class TourState {} abstract class TourState {}
@ -13,26 +15,42 @@ class TourRequestingDistances extends TourState {
TourRequestingDistances({required this.tour, required this.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;
SortingInformationContainer sortingInformation;
TourLoaded({ TourLoaded({
required this.tour, required this.tour,
this.distances, this.distances,
required this.paymentOptions, required this.paymentOptions,
required this.sortingInformation
}); });
TourLoaded copyWith({ TourLoaded copyWith({
Tour? tour, Tour? tour,
Map<String, double>? distances, Map<String, double>? distances,
List<Payment>? paymentOptions, List<Payment>? paymentOptions,
SortingInformationContainer? sortingInformation
}) { }) {
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
); );
} }
} }

View File

@ -0,0 +1,54 @@
import 'package:flutter/cupertino.dart';
class SortingInformation {
String deliveryId;
int position;
SortingInformation({required this.deliveryId, required this.position});
static Map<String, dynamic> toJson(SortingInformation info) {
return {"delivery_id": info.deliveryId, "position": info.position};
}
static SortingInformation fromJson(Map<String, dynamic> json) {
return SortingInformation(
deliveryId: json["delivery_id"].toString(),
position: json["position"],
);
}
}
class SortingInformationContainer {
List<SortingInformation> sorting;
SortingInformationContainer({required this.sorting});
void sort() {
sorting.sort((a, b) => a.position.compareTo(b.position));
}
static SortingInformationContainer fromJson(Map<String, dynamic> json) {
SortingInformationContainer container = SortingInformationContainer(
sorting: [],
);
for (final info in json["sorting"]) {
container.sorting.add(SortingInformation.fromJson(info));
}
return container;
}
Map<String, dynamic> toJson() {
return {
"sorting":
sorting.map((info) => SortingInformation.toJson(info)).toList(),
};
}
SortingInformationContainer copyWith({List<SortingInformation>? sorting}) {
return SortingInformationContainer(
sorting: sorting ?? this.sorting,
);
}
}

View File

@ -1,4 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hl_lieferservice/feature/delivery/overview/bloc/tour_bloc.dart';
import 'package:hl_lieferservice/feature/delivery/overview/bloc/tour_state.dart';
import 'package:hl_lieferservice/feature/delivery/overview/model/sorting_information.dart';
import 'package:hl_lieferservice/model/delivery.dart'; import 'package:hl_lieferservice/model/delivery.dart';
import 'delivery_item.dart'; import 'delivery_item.dart';
@ -6,11 +10,13 @@ import 'delivery_item.dart';
class DeliveryList extends StatefulWidget { class DeliveryList extends StatefulWidget {
final List<Delivery> deliveries; final List<Delivery> deliveries;
final Map<String, double> distances; final Map<String, double> distances;
final List<SortingInformation> sortingInformation;
const DeliveryList({ const DeliveryList({
super.key, super.key,
required this.deliveries, required this.deliveries,
required this.distances, required this.distances,
required this.sortingInformation,
}); });
@override @override
@ -26,16 +32,32 @@ class _DeliveryListState extends State<DeliveryList> {
); );
} }
return ListView.separated( return BlocBuilder<TourBloc, TourState>(
separatorBuilder: (context, index) => const Divider(height: 0), builder: (context, state) {
itemBuilder: (context, index) { final currentState = state;
Delivery delivery = widget.deliveries[index]; if (currentState is TourLoaded) {
return DeliveryListItem( List<SortingInformation> sorted = [...currentState.sortingInformation.sorting];
delivery: delivery, sorted.sort((a, b) => a.position.compareTo(b.position));
distance: widget.distances[delivery.id] ?? 0.0,
); return ListView.separated(
separatorBuilder: (context, index) => const Divider(height: 0),
itemBuilder: (context, index) {
SortingInformation info = sorted[index];
Delivery delivery = currentState.tour.deliveries.firstWhere(
(delivery) => info.deliveryId == delivery.id,
);
return DeliveryListItem(
delivery: delivery,
distance: currentState.distances?[delivery.id] ?? 0.0,
);
},
itemCount: sorted.length,
);
}
return Center(child: CircularProgressIndicator());
}, },
itemCount: widget.deliveries.length,
); );
} }
} }

View File

@ -10,6 +10,7 @@ import 'package:hl_lieferservice/model/tour.dart';
import '../../../../model/delivery.dart'; import '../../../../model/delivery.dart';
import '../../../authentication/bloc/auth_bloc.dart'; import '../../../authentication/bloc/auth_bloc.dart';
import '../../../authentication/bloc/auth_state.dart'; import '../../../authentication/bloc/auth_state.dart';
import '../bloc/tour_state.dart';
class DeliveryOverview extends StatefulWidget { class DeliveryOverview extends StatefulWidget {
const DeliveryOverview({ const DeliveryOverview({
@ -186,6 +187,10 @@ class _DeliveryOverviewState extends State<DeliveryOverview> {
Expanded( Expanded(
child: DeliveryList( child: DeliveryList(
distances: widget.distances, distances: widget.distances,
sortingInformation:
(context.read<TourBloc>().state as TourLoaded)
.sortingInformation
.sorting,
deliveries: deliveries:
_deliveries _deliveries
.where( .where(

View File

@ -1,7 +1,11 @@
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/delivery/overview/bloc/tour_bloc.dart'; import 'package:hl_lieferservice/feature/delivery/overview/bloc/tour_bloc.dart';
import 'package:hl_lieferservice/feature/delivery/overview/bloc/tour_event.dart';
import 'package:hl_lieferservice/feature/delivery/overview/bloc/tour_state.dart'; import 'package:hl_lieferservice/feature/delivery/overview/bloc/tour_state.dart';
import 'package:hl_lieferservice/model/delivery.dart';
import '../model/sorting_information.dart';
class CustomSortDialog extends StatefulWidget { class CustomSortDialog extends StatefulWidget {
const CustomSortDialog({super.key}); const CustomSortDialog({super.key});
@ -11,6 +15,21 @@ class CustomSortDialog extends StatefulWidget {
} }
class _CustomSortDialogState extends State<CustomSortDialog> { class _CustomSortDialogState extends State<CustomSortDialog> {
late List<SortingInformation> _localSortedList;
@override
void initState() {
super.initState();
final state = context.read<TourBloc>().state;
if (state is TourLoaded) {
_localSortedList = [...state.sortingInformation.sorting];
_localSortedList.sort((a, b) => a.position.compareTo(b.position));
} else {
_localSortedList = [];
}
}
Widget _information() { Widget _information() {
return Padding( return Padding(
padding: EdgeInsets.only(top: 15), padding: EdgeInsets.only(top: 15),
@ -47,33 +66,51 @@ class _CustomSortDialogState extends State<CustomSortDialog> {
if (currentState is TourLoaded) { if (currentState is TourLoaded) {
return Expanded( return Expanded(
child: ReorderableListView( child: ReorderableListView(
onReorder: (oldIndex, newIndex) {}, onReorder: (oldIndex, newIndex) {
children: currentState.tour.deliveries.indexed.fold([], ( setState(() {
acc, if (oldIndex < newIndex) {
current, newIndex -= 1;
) { }
final delivery = current.$2; final SortingInformation item = _localSortedList.removeAt(oldIndex);
final index = current.$1; _localSortedList.insert(newIndex, item);
});
acc.add( context.read<TourBloc>().add(
ListTile( ReorderDeliveryEvent(
leading: CircleAvatar( newPosition: newIndex,
backgroundColor: Theme.of(context).primaryColor, oldPosition: oldIndex,
child: Text(
"${index + 1}",
style: TextStyle(
color: Theme.of(context).colorScheme.onSecondary,
),
),
),
title: Text(delivery.customer.name),
subtitle: Text(delivery.customer.address.toString(), style: TextStyle(fontSize: 11),),
trailing: Icon(Icons.drag_handle),
key: Key("reorder-item-${delivery.id}"),
), ),
); );
return acc; },
}), children:
_localSortedList.map((info) {
Delivery delivery = currentState.tour.deliveries.firstWhere(
(delivery) => delivery.id == info.deliveryId,
);
SortingInformation information = currentState
.sortingInformation
.sorting
.firstWhere((info) => info.deliveryId == delivery.id);
return ListTile(
leading: CircleAvatar(
backgroundColor: Theme.of(context).primaryColor,
child: Text(
"${information.position + 1}",
style: TextStyle(
color: Theme.of(context).colorScheme.onSecondary,
),
),
),
title: Text(delivery.customer.name),
subtitle: Text(
delivery.customer.address.toString(),
style: TextStyle(fontSize: 11),
),
trailing: Icon(Icons.drag_handle),
key: Key("reorder-item-${delivery.id}"),
);
}).toList(),
), ),
); );
} }

View File

@ -0,0 +1,53 @@
import 'dart:convert';
import 'dart:io';
import 'package:hl_lieferservice/model/tour.dart';
import 'package:path_provider/path_provider.dart';
import '../model/sorting_information.dart';
class ReorderService {
get _path async {
final dir = await getApplicationDocumentsDirectory();
final date = DateTime.now();
final filename =
"custom_sort_${date.year}_${date.month}_${date.day}.json";
final path = "${dir.path}/$filename";
return path;
}
Future<File> get _file async {
final path = await _path;
final file = File(path);
return file;
}
Future<void> saveSortingInformation(SortingInformationContainer container) async {
(await _file).writeAsString(jsonEncode(container.toJson()));
}
Future<void> initializeTour(Tour tour) async {
(await _file).create();
SortingInformationContainer container = SortingInformationContainer(sorting: []);
for (final (index, delivery) in tour.deliveries.indexed) {
container.sorting.add(
SortingInformation(deliveryId: delivery.id, position: index),
);
}
(await _file).writeAsString(jsonEncode(container.toJson()));
}
bool orderInformationExist() {
return false;
}
Future<SortingInformationContainer> loadSortingInformation() async {
return SortingInformationContainer.fromJson(
jsonDecode(await (await _file).readAsString()),
);
}
}