Phasenbasierte Lieferübersicht + Beladen-Flow, plus Migrationsplan für Rust-Backend
UI-Restructuring: - TabBar in scan_page durch dedizierte Phasen ersetzt: Sortieren / Beladen / Ausliefern - PhaseBloc + PhaseService leiten Phase aus Tour-/Item-States ab - DeliverySelectionPage (ab 2 Autos) und DeliverySortPage als eigene Flows - LoadingOverviewPage / LoadingCustomerPage für die Beladephase - PhaseStepper-Widget im Home für Phasen-Anzeige - Lager-Differenzierung (Standardlager 0 vs. Außenlager) via WarehouseBadge Process-Stubs: - ProcessRepository für Hold/Cancel/Sort/Assign-Flows (stub, bereit für Backend-Anbindung) Doku: - docs/BACKEND_MIGRATION.md: Phasenplan für Umstellung auf das neue Rust-Backend (OpenAPI-Generator, Keycloak OIDC, Clean-Arch-Layering)
This commit is contained in:
@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
||||
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/repository/process_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';
|
||||
@ -20,10 +21,16 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
||||
OperationBloc opBloc;
|
||||
AuthBloc authBloc;
|
||||
TourRepository tourRepository;
|
||||
ProcessRepository processRepository;
|
||||
StreamSubscription? _combinedSubscription;
|
||||
|
||||
TourBloc({required this.opBloc, required this.authBloc, required this.tourRepository})
|
||||
: super(TourInitial()) {
|
||||
TourBloc({
|
||||
required this.opBloc,
|
||||
required this.authBloc,
|
||||
required this.tourRepository,
|
||||
ProcessRepository? processRepository,
|
||||
}) : processRepository = processRepository ?? ProcessRepository(),
|
||||
super(TourInitial()) {
|
||||
_combinedSubscription = CombineLatestStream.combine2(
|
||||
tourRepository.tour,
|
||||
tourRepository.paymentOptions,
|
||||
@ -41,6 +48,7 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
||||
|
||||
on<LoadTour>(_load);
|
||||
on<AssignCarEvent>(_assignCar);
|
||||
on<UnassignDeliveryEvent>(_unassignDelivery);
|
||||
on<IncrementArticleScanAmount>(_increment);
|
||||
on<ScanArticleEvent>(_scan);
|
||||
on<ScanComponentEvent>(_scanComponent);
|
||||
@ -59,6 +67,9 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
||||
on<RequestDeliveryDistanceEvent>(_calculateDistances);
|
||||
on<RequestSortingInformationEvent>(_requestSortingInformation);
|
||||
on<ReorderDeliveryEvent>(_reorderDelivery);
|
||||
on<ReplaceSortingEvent>(_replaceSorting);
|
||||
on<ConfirmSortingEvent>(_confirmSorting);
|
||||
on<EnsureSortingForCarEvent>(_ensureSortingForCar);
|
||||
on<CarsLoadedEvent>(_carsLoaded);
|
||||
on<SetArticleAmountEvent>(_setArticleAmount);
|
||||
}
|
||||
@ -128,6 +139,128 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _ensureSortingForCar(
|
||||
EnsureSortingForCarEvent event,
|
||||
Emitter<TourState> emit,
|
||||
) async {
|
||||
final currentState = state;
|
||||
if (currentState is! TourLoaded) return;
|
||||
|
||||
// Ein-Auto-Teams: alle Tour-Lieferungen sind die eigenen — die
|
||||
// Zuordnung erfolgt erst beim Scannen.
|
||||
// Mehr-Auto-Teams: nach dem Auswahl-Schritt sind die Lieferungen
|
||||
// bereits per `assignCar` mit der eigenen carId verknüpft; wir
|
||||
// sortieren dann ausschließlich die eigenen.
|
||||
final cars = currentState.tour.driver.cars;
|
||||
final allTourIds = cars.length >= 2
|
||||
? currentState.tour.deliveries
|
||||
.where((d) => d.carId?.toString() == event.carId)
|
||||
.map((d) => d.id)
|
||||
.toList(growable: false)
|
||||
: currentState.tour.deliveries
|
||||
.map((d) => d.id)
|
||||
.toList(growable: false);
|
||||
final existing =
|
||||
currentState.sortingInformation[event.carId] ?? const <String>[];
|
||||
|
||||
// Bestehende Reihenfolge beibehalten (nur Einträge, die noch in der Tour
|
||||
// sind), dann fehlende Tour-Lieferungen hinten anhängen.
|
||||
final allIdsSet = allTourIds.toSet();
|
||||
final seen = <String>{};
|
||||
final merged = <String>[];
|
||||
for (final id in existing) {
|
||||
if (allIdsSet.contains(id) && seen.add(id)) merged.add(id);
|
||||
}
|
||||
for (final id in allTourIds) {
|
||||
if (seen.add(id)) merged.add(id);
|
||||
}
|
||||
|
||||
if (merged.length == existing.length &&
|
||||
_listEquals(merged, existing.toList())) {
|
||||
// Nichts zu tun — Bucket ist bereits konsistent.
|
||||
return;
|
||||
}
|
||||
|
||||
final container = {...currentState.sortingInformation};
|
||||
container[event.carId] = merged;
|
||||
|
||||
await ReorderService().saveSortingInformation(container);
|
||||
emit(currentState.copyWith(sortingInformation: container));
|
||||
}
|
||||
|
||||
bool _listEquals(List<String> a, List<String> b) {
|
||||
if (a.length != b.length) return false;
|
||||
for (int i = 0; i < a.length; i++) {
|
||||
if (a[i] != b[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<void> _replaceSorting(
|
||||
ReplaceSortingEvent event,
|
||||
Emitter<TourState> emit,
|
||||
) async {
|
||||
final currentState = state;
|
||||
if (currentState is TourLoaded) {
|
||||
await ReorderService().saveSortingInformation(
|
||||
event.newSortingInformation,
|
||||
);
|
||||
emit(
|
||||
currentState.copyWith(
|
||||
sortingInformation: event.newSortingInformation,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _confirmSorting(
|
||||
ConfirmSortingEvent event,
|
||||
Emitter<TourState> emit,
|
||||
) async {
|
||||
final currentState = state;
|
||||
if (currentState is! TourLoaded) return;
|
||||
|
||||
emit(
|
||||
currentState.copyWith(
|
||||
isPersistingSorting: true,
|
||||
clearSortingPersistError: true,
|
||||
),
|
||||
);
|
||||
|
||||
final orderedIds =
|
||||
currentState.sortingInformation[event.carId] ?? const <String>[];
|
||||
|
||||
try {
|
||||
await processRepository.persistDeliveryOrder(
|
||||
carId: event.carId,
|
||||
orderedDeliveryIds: orderedIds,
|
||||
);
|
||||
|
||||
// Hinweis: Der eigentliche Phasen-Wechsel auf `beladen` läuft über
|
||||
// den [PhaseBloc], angestoßen vom UI-Listener in DeliverySortPage.
|
||||
// So bleibt der Phasen-State zentral und der TourBloc unabhängig.
|
||||
|
||||
// Re-read the latest state — earlier emits or upstream tour updates
|
||||
// may have replaced it while the await was in flight.
|
||||
final latest = state;
|
||||
if (latest is TourLoaded) {
|
||||
emit(latest.copyWith(isPersistingSorting: false));
|
||||
}
|
||||
} catch (e, st) {
|
||||
debugPrint("Fehler beim Persistieren der Sortierung: $e $st");
|
||||
final latest = state;
|
||||
if (latest is TourLoaded) {
|
||||
emit(
|
||||
latest.copyWith(
|
||||
isPersistingSorting: false,
|
||||
sortingPersistError:
|
||||
"Reihenfolge konnte nicht gespeichert werden. Bitte erneut versuchen.",
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _calculateDistances(
|
||||
RequestDeliveryDistanceEvent event,
|
||||
Emitter<TourState> emit,
|
||||
@ -230,6 +363,8 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
||||
distances: Map<String, double>.from(currentState.distances ?? {}),
|
||||
sortingInformation: currentState.sortingInformation,
|
||||
pendingScanRequests: currentState.pendingScanRequests,
|
||||
isPersistingSorting: currentState.isPersistingSorting,
|
||||
sortingPersistError: currentState.sortingPersistError,
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -409,6 +544,29 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _unassignDelivery(
|
||||
UnassignDeliveryEvent event,
|
||||
Emitter<TourState> emit,
|
||||
) async {
|
||||
final currentState = state;
|
||||
if (currentState is! TourLoaded) return;
|
||||
|
||||
opBloc.add(StartOperation());
|
||||
try {
|
||||
// Stub-Aufruf — solange kein echter Endpoint existiert, gibt es
|
||||
// hier kein Tour-Update; das lokale Modell behält den alten carId-
|
||||
// Wert. Mit dem echten Endpoint wird die Tour anschließend über
|
||||
// den tourRepository-Stream aktualisiert (analog zu assignCar).
|
||||
await processRepository.unassignDeliveryFromCar(
|
||||
deliveryId: event.deliveryId,
|
||||
);
|
||||
opBloc.add(FinishOperation());
|
||||
} catch (e, st) {
|
||||
debugPrint("$e $st");
|
||||
_handleError(e, "Fehler beim Freigeben der Lieferung");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _load(LoadTour event, Emitter<TourState> emit) async {
|
||||
try {
|
||||
emit(TourLoading());
|
||||
|
||||
Reference in New Issue
Block a user