From 622967e5c197d55d5be3542f7061289a166809aa Mon Sep 17 00:00:00 2001 From: Dennis Nemec Date: Wed, 7 Jan 2026 15:19:34 +0100 Subject: [PATCH] Finished custom sorting of deliveries --- .../delivery/overview/bloc/tour_bloc.dart | 140 +++++++++++++++++- .../delivery/overview/bloc/tour_event.dart | 15 ++ .../delivery/overview/bloc/tour_state.dart | 18 +++ .../overview/model/sorting_information.dart | 54 +++++++ .../overview/presentation/delivery_list.dart | 40 +++-- .../presentation/delivery_overview.dart | 5 + .../delivery_overview_custom_sort.dart | 85 ++++++++--- .../overview/service/reorder_service.dart | 53 +++++++ 8 files changed, 369 insertions(+), 41 deletions(-) create mode 100644 lib/feature/delivery/overview/model/sorting_information.dart create mode 100644 lib/feature/delivery/overview/service/reorder_service.dart diff --git a/lib/feature/delivery/overview/bloc/tour_bloc.dart b/lib/feature/delivery/overview/bloc/tour_bloc.dart index c43345d..543a686 100644 --- a/lib/feature/delivery/overview/bloc/tour_bloc.dart +++ b/lib/feature/delivery/overview/bloc/tour_bloc.dart @@ -4,8 +4,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:hl_lieferservice/feature/delivery/overview/bloc/tour_event.dart'; import 'package:hl_lieferservice/feature/delivery/overview/bloc/tour_state.dart'; +import 'package:hl_lieferservice/feature/delivery/overview/model/sorting_information.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/reorder_service.dart'; import 'package:hl_lieferservice/model/tour.dart'; import 'package:hl_lieferservice/widget/operations/bloc/operation_bloc.dart'; import 'package:hl_lieferservice/widget/operations/bloc/operation_event.dart'; @@ -50,6 +52,8 @@ class TourBloc extends Bloc { on(_finishDelivery); on(_updated); on(_calculateDistances); + on(_requestSortingInformation); + on(_reorderDelivery); } @override @@ -59,6 +63,36 @@ class TourBloc extends Bloc { return super.close(); } + void _reorderDelivery( + ReorderDeliveryEvent event, + Emitter 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( RequestDeliveryDistanceEvent event, Emitter emit, @@ -79,15 +113,104 @@ class TourBloc extends Bloc { debugPrint("Fehler beim Berechnen der Distanzen: $e"); opBloc.add(FailOperation(message: "Fehler beim Berechnen der Distanzen")); return; + } finally { + // Independent of error state fetch the sorting information + add( + RequestSortingInformationEvent( + tour: event.tour, + payments: event.payments, + distances: distances, + ), + ); } + } - emit( - TourLoaded( - tour: event.tour, - paymentOptions: event.payments, - distances: distances, - ), - ); + void _requestSortingInformation( + RequestSortingInformationEvent event, + Emitter emit, + ) async { + 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 emit) { @@ -97,16 +220,17 @@ class TourBloc extends Bloc { event.payments.map((payment) => payment.copyWith()).toList(); if (currentState is TourLoaded) { - debugPrint("TEST UPDATE"); emit( TourLoaded( tour: tour, paymentOptions: payments, distances: Map.from(currentState.distances ?? {}), + sortingInformation: currentState.sortingInformation, ), ); } + // Download distances if tour has previously fetched by API if (currentState is TourLoading) { add( RequestDeliveryDistanceEvent(tour: tour.copyWith(), payments: payments), diff --git a/lib/feature/delivery/overview/bloc/tour_event.dart b/lib/feature/delivery/overview/bloc/tour_event.dart index 446c43e..3cdb072 100644 --- a/lib/feature/delivery/overview/bloc/tour_event.dart +++ b/lib/feature/delivery/overview/bloc/tour_event.dart @@ -19,6 +19,21 @@ class RequestDeliveryDistanceEvent extends TourEvent { RequestDeliveryDistanceEvent({required this.tour, required this.payments}); } +class RequestSortingInformationEvent extends TourEvent { + Tour tour; + List payments; + Map? 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 { Tour tour; List payments; diff --git a/lib/feature/delivery/overview/bloc/tour_state.dart b/lib/feature/delivery/overview/bloc/tour_state.dart index dacbdd7..d81f4ad 100644 --- a/lib/feature/delivery/overview/bloc/tour_state.dart +++ b/lib/feature/delivery/overview/bloc/tour_state.dart @@ -1,3 +1,5 @@ +import 'package:hl_lieferservice/feature/delivery/overview/model/sorting_information.dart'; + import '../../../../model/tour.dart'; abstract class TourState {} @@ -13,26 +15,42 @@ class TourRequestingDistances extends TourState { TourRequestingDistances({required this.tour, required this.payments}); } +class TourRequestingSortingInformation extends TourState { + Tour tour; + Map? distances; + List paymentOptions; + + TourRequestingSortingInformation({ + required this.tour, + this.distances, + required this.paymentOptions, + }); +} + class TourLoaded extends TourState { Tour tour; Map? distances; List paymentOptions; + SortingInformationContainer sortingInformation; TourLoaded({ required this.tour, this.distances, required this.paymentOptions, + required this.sortingInformation }); TourLoaded copyWith({ Tour? tour, Map? distances, List? paymentOptions, + SortingInformationContainer? sortingInformation }) { return TourLoaded( tour: tour ?? this.tour, distances: distances ?? this.distances, paymentOptions: paymentOptions ?? this.paymentOptions, + sortingInformation: sortingInformation ?? this.sortingInformation ); } } diff --git a/lib/feature/delivery/overview/model/sorting_information.dart b/lib/feature/delivery/overview/model/sorting_information.dart new file mode 100644 index 0000000..934abb6 --- /dev/null +++ b/lib/feature/delivery/overview/model/sorting_information.dart @@ -0,0 +1,54 @@ +import 'package:flutter/cupertino.dart'; + +class SortingInformation { + String deliveryId; + int position; + + SortingInformation({required this.deliveryId, required this.position}); + + static Map toJson(SortingInformation info) { + return {"delivery_id": info.deliveryId, "position": info.position}; + } + + static SortingInformation fromJson(Map json) { + return SortingInformation( + deliveryId: json["delivery_id"].toString(), + position: json["position"], + ); + } +} + +class SortingInformationContainer { + List sorting; + + SortingInformationContainer({required this.sorting}); + + void sort() { + sorting.sort((a, b) => a.position.compareTo(b.position)); + } + + static SortingInformationContainer fromJson(Map json) { + SortingInformationContainer container = SortingInformationContainer( + sorting: [], + ); + + for (final info in json["sorting"]) { + container.sorting.add(SortingInformation.fromJson(info)); + } + + return container; + } + + Map toJson() { + return { + "sorting": + sorting.map((info) => SortingInformation.toJson(info)).toList(), + }; + } + + SortingInformationContainer copyWith({List? sorting}) { + return SortingInformationContainer( + sorting: sorting ?? this.sorting, + ); + } +} diff --git a/lib/feature/delivery/overview/presentation/delivery_list.dart b/lib/feature/delivery/overview/presentation/delivery_list.dart index cd5a6ad..fc7eee2 100644 --- a/lib/feature/delivery/overview/presentation/delivery_list.dart +++ b/lib/feature/delivery/overview/presentation/delivery_list.dart @@ -1,4 +1,8 @@ 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 'delivery_item.dart'; @@ -6,11 +10,13 @@ import 'delivery_item.dart'; class DeliveryList extends StatefulWidget { final List deliveries; final Map distances; + final List sortingInformation; const DeliveryList({ super.key, required this.deliveries, required this.distances, + required this.sortingInformation, }); @override @@ -26,16 +32,32 @@ class _DeliveryListState extends State { ); } - return ListView.separated( - separatorBuilder: (context, index) => const Divider(height: 0), - itemBuilder: (context, index) { - Delivery delivery = widget.deliveries[index]; - return DeliveryListItem( - delivery: delivery, - distance: widget.distances[delivery.id] ?? 0.0, - ); + return BlocBuilder( + builder: (context, state) { + final currentState = state; + if (currentState is TourLoaded) { + List sorted = [...currentState.sortingInformation.sorting]; + sorted.sort((a, b) => a.position.compareTo(b.position)); + + 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, ); } } diff --git a/lib/feature/delivery/overview/presentation/delivery_overview.dart b/lib/feature/delivery/overview/presentation/delivery_overview.dart index 0f0a085..e587a05 100644 --- a/lib/feature/delivery/overview/presentation/delivery_overview.dart +++ b/lib/feature/delivery/overview/presentation/delivery_overview.dart @@ -10,6 +10,7 @@ import 'package:hl_lieferservice/model/tour.dart'; import '../../../../model/delivery.dart'; import '../../../authentication/bloc/auth_bloc.dart'; import '../../../authentication/bloc/auth_state.dart'; +import '../bloc/tour_state.dart'; class DeliveryOverview extends StatefulWidget { const DeliveryOverview({ @@ -186,6 +187,10 @@ class _DeliveryOverviewState extends State { Expanded( child: DeliveryList( distances: widget.distances, + sortingInformation: + (context.read().state as TourLoaded) + .sortingInformation + .sorting, deliveries: _deliveries .where( diff --git a/lib/feature/delivery/overview/presentation/delivery_overview_custom_sort.dart b/lib/feature/delivery/overview/presentation/delivery_overview_custom_sort.dart index 680a1a9..0d02ddb 100644 --- a/lib/feature/delivery/overview/presentation/delivery_overview_custom_sort.dart +++ b/lib/feature/delivery/overview/presentation/delivery_overview_custom_sort.dart @@ -1,7 +1,11 @@ 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_event.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 { const CustomSortDialog({super.key}); @@ -11,6 +15,21 @@ class CustomSortDialog extends StatefulWidget { } class _CustomSortDialogState extends State { + late List _localSortedList; + + @override + void initState() { + super.initState(); + final state = context.read().state; + if (state is TourLoaded) { + _localSortedList = [...state.sortingInformation.sorting]; + _localSortedList.sort((a, b) => a.position.compareTo(b.position)); + } else { + _localSortedList = []; + } + } + + Widget _information() { return Padding( padding: EdgeInsets.only(top: 15), @@ -47,33 +66,51 @@ class _CustomSortDialogState extends State { if (currentState is TourLoaded) { return Expanded( child: ReorderableListView( - onReorder: (oldIndex, newIndex) {}, - children: currentState.tour.deliveries.indexed.fold([], ( - acc, - current, - ) { - final delivery = current.$2; - final index = current.$1; + onReorder: (oldIndex, newIndex) { + setState(() { + if (oldIndex < newIndex) { + newIndex -= 1; + } + final SortingInformation item = _localSortedList.removeAt(oldIndex); + _localSortedList.insert(newIndex, item); + }); - acc.add( - ListTile( - leading: CircleAvatar( - backgroundColor: Theme.of(context).primaryColor, - 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}"), + context.read().add( + ReorderDeliveryEvent( + newPosition: newIndex, + oldPosition: oldIndex, ), ); - 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(), ), ); } diff --git a/lib/feature/delivery/overview/service/reorder_service.dart b/lib/feature/delivery/overview/service/reorder_service.dart new file mode 100644 index 0000000..d03cb04 --- /dev/null +++ b/lib/feature/delivery/overview/service/reorder_service.dart @@ -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 get _file async { + final path = await _path; + final file = File(path); + + return file; + } + + Future saveSortingInformation(SortingInformationContainer container) async { + (await _file).writeAsString(jsonEncode(container.toJson())); + } + + Future 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 loadSortingInformation() async { + return SortingInformationContainer.fromJson( + jsonDecode(await (await _file).readAsString()), + ); + } +} \ No newline at end of file