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:
125
lib/feature/delivery/bloc/phase_bloc.dart
Normal file
125
lib/feature/delivery/bloc/phase_bloc.dart
Normal file
@ -0,0 +1,125 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:hl_lieferservice/feature/delivery/bloc/phase_event.dart';
|
||||
import 'package:hl_lieferservice/feature/delivery/bloc/phase_state.dart';
|
||||
import 'package:hl_lieferservice/feature/delivery/model/delivery_phase.dart';
|
||||
import 'package:hl_lieferservice/feature/delivery/overview/service/phase_service.dart';
|
||||
|
||||
/// Liefert die Anzahl der dem aktuellen Fahrer-Team zugeordneten Fahrzeuge.
|
||||
/// Wird vom [PhaseBloc] bei der Ermittlung der Eintrittsphase aufgerufen.
|
||||
///
|
||||
/// Optional: ist die Tour noch nicht geladen oder die Anzahl unbekannt,
|
||||
/// liefert die Funktion `null` zurück — der BLoC verwendet dann den
|
||||
/// Default-Eintritt [DeliveryPhase.sortieren].
|
||||
typedef CarCountResolver = int? Function();
|
||||
|
||||
/// Zentraler State für die aktuelle Phase je Fahrzeug. Persistiert über
|
||||
/// [PhaseService] auf datumsbezogene SharedPreferences-Keys (siehe Service).
|
||||
///
|
||||
/// Eintrittsphase nach Fahrzeugauswahl:
|
||||
/// * 1 Auto im Team → [DeliveryPhase.sortieren] (bisheriges Verhalten).
|
||||
/// * ≥2 Autos → [DeliveryPhase.auswaehlen] (neuer Auswahl-Schritt).
|
||||
///
|
||||
/// Ist bereits eine Phase persistiert, wird diese verwendet (Resume nach
|
||||
/// Neustart der App). Die Eintrittslogik greift also nur beim "ersten Load
|
||||
/// des Tages" für ein Fahrzeug.
|
||||
class PhaseBloc extends Bloc<PhaseEvent, PhaseState> {
|
||||
final PhaseService phaseService;
|
||||
|
||||
/// Liefert die aktuelle Anzahl der Team-Fahrzeuge. Wird vom umgebenden
|
||||
/// Provider so verdrahtet, dass sie aus dem [TourBloc] kommt.
|
||||
final CarCountResolver? carCountResolver;
|
||||
|
||||
PhaseBloc({
|
||||
PhaseService? phaseService,
|
||||
this.carCountResolver,
|
||||
}) : phaseService = phaseService ?? PhaseService(),
|
||||
super(PhaseInitial()) {
|
||||
on<PhaseLoadForCar>(_load);
|
||||
on<PhaseLoaded>(_applyLoaded);
|
||||
on<PhaseSet>(_set);
|
||||
}
|
||||
|
||||
PhaseReady _ensureReady() {
|
||||
final current = state;
|
||||
return current is PhaseReady
|
||||
? current
|
||||
: PhaseReady(phaseByCar: const {});
|
||||
}
|
||||
|
||||
/// Bestimmt die initiale Phase für ein frisch ausgewähltes Fahrzeug.
|
||||
/// Sobald die Tour bekannt ist und ≥2 Fahrzeuge enthält, startet der
|
||||
/// Fahrer im Auswahl-Schritt; sonst direkt im Sortieren.
|
||||
DeliveryPhase _entryPhase() {
|
||||
final count = carCountResolver?.call();
|
||||
if (count != null && count >= 2) return DeliveryPhase.auswaehlen;
|
||||
return DeliveryPhase.sortieren;
|
||||
}
|
||||
|
||||
Future<void> _load(PhaseLoadForCar event, Emitter<PhaseState> emit) async {
|
||||
final current = _ensureReady();
|
||||
// Wenn bereits geladen, nichts tun — der Stepper-Tap entscheidet aktiv.
|
||||
if (current.phaseByCar.containsKey(event.carId)) return;
|
||||
|
||||
try {
|
||||
final persisted = await phaseService.load(event.carId);
|
||||
final persistedMax = await phaseService.loadMax(event.carId);
|
||||
final phase = persisted ?? _entryPhase();
|
||||
// Max ist mindestens die aktuelle Phase. Falls in der Persistenz ein
|
||||
// höherer Wert steht (Rücksprung), den nehmen.
|
||||
DeliveryPhase maxPhase = phase;
|
||||
if (persistedMax != null && persistedMax.index > maxPhase.index) {
|
||||
maxPhase = persistedMax;
|
||||
}
|
||||
|
||||
if (persisted == null) {
|
||||
// Erste Phase nach Fahrzeugauswahl direkt persistieren, damit
|
||||
// ein Resume nach App-Neustart die Phase kennt.
|
||||
await phaseService.save(event.carId, phase);
|
||||
await phaseService.saveMax(event.carId, maxPhase);
|
||||
} else if (persistedMax == null) {
|
||||
// Migration: alte Tage ohne Max-Tracking → einmalig nachziehen.
|
||||
await phaseService.saveMax(event.carId, maxPhase);
|
||||
}
|
||||
|
||||
add(PhaseLoaded(
|
||||
carId: event.carId,
|
||||
phase: phase,
|
||||
maxPhase: maxPhase,
|
||||
));
|
||||
} catch (e, st) {
|
||||
debugPrint("PhaseBloc._load: $e $st");
|
||||
// Fail-soft: ohne Persistenz weiter, damit der Flow nicht hängen bleibt.
|
||||
final fallback = _entryPhase();
|
||||
add(PhaseLoaded(
|
||||
carId: event.carId,
|
||||
phase: fallback,
|
||||
maxPhase: fallback,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
void _applyLoaded(PhaseLoaded event, Emitter<PhaseState> emit) {
|
||||
final current = _ensureReady();
|
||||
emit(current.withLoaded(event.carId, event.phase, event.maxPhase));
|
||||
}
|
||||
|
||||
Future<void> _set(PhaseSet event, Emitter<PhaseState> emit) async {
|
||||
final current = _ensureReady();
|
||||
final next = current.withPhase(event.carId, event.phase);
|
||||
emit(next);
|
||||
try {
|
||||
await phaseService.save(event.carId, event.phase);
|
||||
// withPhase hat das Max ggf. hochgezogen — persistieren, damit ein
|
||||
// Neustart die "höchste erreichte Phase" kennt.
|
||||
final newMax = next.maxPhaseFor(event.carId);
|
||||
if (newMax != null) {
|
||||
await phaseService.saveMax(event.carId, newMax);
|
||||
}
|
||||
} catch (e, st) {
|
||||
debugPrint("PhaseBloc._set: $e $st");
|
||||
// UI bleibt konsistent, Persistenz-Fehler ignorieren wir bewusst —
|
||||
// beim nächsten Setzen wird erneut versucht.
|
||||
}
|
||||
}
|
||||
}
|
||||
41
lib/feature/delivery/bloc/phase_event.dart
Normal file
41
lib/feature/delivery/bloc/phase_event.dart
Normal file
@ -0,0 +1,41 @@
|
||||
import 'package:hl_lieferservice/feature/delivery/model/delivery_phase.dart';
|
||||
|
||||
abstract class PhaseEvent {}
|
||||
|
||||
/// Lädt (falls noch nicht geschehen) die persistierte Phase für ein Fahrzeug
|
||||
/// und emittiert sie. Falls nichts persistiert ist, ermittelt der
|
||||
/// [PhaseBloc] die Eintrittsphase abhängig von der Anzahl der Team-
|
||||
/// Fahrzeuge:
|
||||
/// * 1 Auto → [DeliveryPhase.sortieren]
|
||||
/// * ≥2 Autos → [DeliveryPhase.auswaehlen]
|
||||
class PhaseLoadForCar extends PhaseEvent {
|
||||
final String carId;
|
||||
|
||||
PhaseLoadForCar({required this.carId});
|
||||
}
|
||||
|
||||
/// Explizites Setzen einer Phase für ein Fahrzeug — wird sowohl beim
|
||||
/// Sprung über den Stepper als auch nach automatischem Phasen-Wechsel
|
||||
/// (z. B. Sortierung bestätigt → beladen) aufgerufen. Persistiert.
|
||||
class PhaseSet extends PhaseEvent {
|
||||
final String carId;
|
||||
final DeliveryPhase phase;
|
||||
|
||||
PhaseSet({required this.carId, required this.phase});
|
||||
}
|
||||
|
||||
/// Setzt die Phase für ein Fahrzeug, ohne sie zu persistieren. Wird intern
|
||||
/// nach einem Load verwendet, da die Quelle bereits SharedPreferences ist.
|
||||
/// [maxPhase] ist die höchste am Tag erreichte Phase aus der Persistenz —
|
||||
/// fällt sie weg, wird die aktuelle [phase] als Max angenommen.
|
||||
class PhaseLoaded extends PhaseEvent {
|
||||
final String carId;
|
||||
final DeliveryPhase phase;
|
||||
final DeliveryPhase maxPhase;
|
||||
|
||||
PhaseLoaded({
|
||||
required this.carId,
|
||||
required this.phase,
|
||||
required this.maxPhase,
|
||||
});
|
||||
}
|
||||
58
lib/feature/delivery/bloc/phase_state.dart
Normal file
58
lib/feature/delivery/bloc/phase_state.dart
Normal file
@ -0,0 +1,58 @@
|
||||
import 'package:hl_lieferservice/feature/delivery/model/delivery_phase.dart';
|
||||
|
||||
abstract class PhaseState {}
|
||||
|
||||
class PhaseInitial extends PhaseState {}
|
||||
|
||||
/// Die aktive Phase pro Fahrzeug. Die Map ist pro Auto-ID separiert,
|
||||
/// weil zwei Lieferketten am selben Tag (selten, aber möglich) parallel
|
||||
/// laufen könnten und sich gegenseitig nicht überschreiben dürfen.
|
||||
///
|
||||
/// Zusätzlich wird die **höchste je heute erreichte Phase** pro Fahrzeug
|
||||
/// gehalten ([maxPhaseByCar]). Damit kann der Stepper Vorwärts-Sprünge
|
||||
/// auf bereits besuchte Phasen erlauben, auch wenn der Fahrer vorher
|
||||
/// zurückgesprungen ist.
|
||||
class PhaseReady extends PhaseState {
|
||||
final Map<String, DeliveryPhase> phaseByCar;
|
||||
final Map<String, DeliveryPhase> maxPhaseByCar;
|
||||
|
||||
PhaseReady({
|
||||
required this.phaseByCar,
|
||||
this.maxPhaseByCar = const {},
|
||||
});
|
||||
|
||||
DeliveryPhase? phaseFor(String carId) => phaseByCar[carId];
|
||||
|
||||
/// Höchste am Tag erreichte Phase für [carId]. Fallback: aktuelle Phase
|
||||
/// — wenn nichts gemerkt ist, gilt die aktuelle Phase als "höchste".
|
||||
DeliveryPhase? maxPhaseFor(String carId) =>
|
||||
maxPhaseByCar[carId] ?? phaseByCar[carId];
|
||||
|
||||
/// Setzt eine neue Phase und bumpt das Max-Tracking, falls die neue Phase
|
||||
/// in der Enum-Reihenfolge höher ist als das bisherige Max.
|
||||
PhaseReady withPhase(String carId, DeliveryPhase phase) {
|
||||
final next = Map<String, DeliveryPhase>.from(phaseByCar);
|
||||
next[carId] = phase;
|
||||
|
||||
final nextMax = Map<String, DeliveryPhase>.from(maxPhaseByCar);
|
||||
final currentMax = nextMax[carId];
|
||||
if (currentMax == null || phase.index > currentMax.index) {
|
||||
nextMax[carId] = phase;
|
||||
}
|
||||
return PhaseReady(phaseByCar: next, maxPhaseByCar: nextMax);
|
||||
}
|
||||
|
||||
/// Wird nach dem Laden aus dem [PhaseService] verwendet, damit die echte
|
||||
/// historische Max-Phase übernommen wird (nicht automatisch geboostet).
|
||||
PhaseReady withLoaded(
|
||||
String carId,
|
||||
DeliveryPhase phase,
|
||||
DeliveryPhase maxPhase,
|
||||
) {
|
||||
final next = Map<String, DeliveryPhase>.from(phaseByCar);
|
||||
next[carId] = phase;
|
||||
final nextMax = Map<String, DeliveryPhase>.from(maxPhaseByCar);
|
||||
nextMax[carId] = maxPhase;
|
||||
return PhaseReady(phaseByCar: next, maxPhaseByCar: nextMax);
|
||||
}
|
||||
}
|
||||
@ -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());
|
||||
|
||||
@ -37,6 +37,41 @@ class ReorderDeliveryEvent extends TourEvent {
|
||||
ReorderDeliveryEvent({required this.newPosition, required this.oldPosition, required this.carId});
|
||||
}
|
||||
|
||||
/// Ersetzt die komplette Sortier-Information (z. B. beim Zurücksetzen auf
|
||||
/// die Default-Reihenfolge in der Sortier-Page). Persistiert lokal über
|
||||
/// den ReorderService, kein Backend-Call.
|
||||
class ReplaceSortingEvent extends TourEvent {
|
||||
final String carId;
|
||||
final Map<String, List<String>> newSortingInformation;
|
||||
|
||||
ReplaceSortingEvent({
|
||||
required this.carId,
|
||||
required this.newSortingInformation,
|
||||
});
|
||||
}
|
||||
|
||||
/// Bestätigung der Sortierung durch den Fahrer. Löst den (aktuell ge-
|
||||
/// stubbten) Backend-Call zur Persistierung der Reihenfolge aus und
|
||||
/// schaltet bei Erfolg in Phase [DeliveryPhase.beladen].
|
||||
class ConfirmSortingEvent extends TourEvent {
|
||||
final String carId;
|
||||
|
||||
ConfirmSortingEvent({required this.carId});
|
||||
}
|
||||
|
||||
/// Stellt sicher, dass der Sortier-Bucket für [carId] alle aktuell in der
|
||||
/// Tour vorhandenen Lieferungen enthält — und zwar UNABHÄNGIG von
|
||||
/// `delivery.carId`. Hintergrund: zum Sortier-Zeitpunkt sind die Lieferungen
|
||||
/// dem Fahrzeug oft noch nicht zugeordnet (das passiert erst beim Scannen
|
||||
/// während des Beladens). Der Fahrer hat aber bereits sein Tagesfahrzeug
|
||||
/// gewählt, also gehören alle Tour-Lieferungen in diesen Bucket. Bestehende
|
||||
/// Reihenfolge bleibt erhalten, fehlende IDs werden hinten angehängt.
|
||||
class EnsureSortingForCarEvent extends TourEvent {
|
||||
final String carId;
|
||||
|
||||
EnsureSortingForCarEvent({required this.carId});
|
||||
}
|
||||
|
||||
class TourUpdated extends TourEvent {
|
||||
Tour tour;
|
||||
List<Payment> payments;
|
||||
@ -63,6 +98,17 @@ class AssignCarEvent extends TourEvent {
|
||||
AssignCarEvent({required this.deliveryId, required this.carId});
|
||||
}
|
||||
|
||||
/// Hebt die Fahrzeug-Zuordnung einer Lieferung wieder auf. Wird im
|
||||
/// Auswahl-Schritt ausgelöst, wenn der Fahrer eine eigene Lieferung
|
||||
/// "freigibt", damit ein Kollege sie übernehmen kann. Aktuell ruft der
|
||||
/// Handler nur den ProcessRepository-Stub auf — ein echtes Update der
|
||||
/// Tour erfolgt erst mit dem realen Backend-Endpoint (siehe Repository).
|
||||
class UnassignDeliveryEvent extends TourEvent {
|
||||
final String deliveryId;
|
||||
|
||||
UnassignDeliveryEvent({required this.deliveryId});
|
||||
}
|
||||
|
||||
class IncrementArticleScanAmount extends TourEvent {
|
||||
String internalArticleId;
|
||||
String deliveryId;
|
||||
|
||||
@ -19,12 +19,23 @@ class TourLoaded extends TourState {
|
||||
/// rapid-fire scans coexist without one prematurely clearing the indicator.
|
||||
int pendingScanRequests;
|
||||
|
||||
/// True während der Backend-Call zur Persistierung der Sortier-Reihenfolge
|
||||
/// läuft. Wird vom Bestätigungs-Button in der Sortier-Page für Spinner
|
||||
/// und Button-Disabled-State ausgewertet.
|
||||
bool isPersistingSorting;
|
||||
|
||||
/// Letzte Fehlermeldung des Sortier-Persist-Calls. Wird nach Anzeige
|
||||
/// durch das UI ge-leert (z. B. nach SnackBar).
|
||||
String? sortingPersistError;
|
||||
|
||||
TourLoaded({
|
||||
required this.tour,
|
||||
this.distances,
|
||||
required this.paymentOptions,
|
||||
required this.sortingInformation,
|
||||
this.pendingScanRequests = 0,
|
||||
this.isPersistingSorting = false,
|
||||
this.sortingPersistError,
|
||||
});
|
||||
|
||||
TourLoaded copyWith({
|
||||
@ -33,6 +44,9 @@ class TourLoaded extends TourState {
|
||||
List<Payment>? paymentOptions,
|
||||
Map<String, List<String>>? sortingInformation,
|
||||
int? pendingScanRequests,
|
||||
bool? isPersistingSorting,
|
||||
String? sortingPersistError,
|
||||
bool clearSortingPersistError = false,
|
||||
}) {
|
||||
return TourLoaded(
|
||||
tour: tour ?? this.tour,
|
||||
@ -40,6 +54,10 @@ class TourLoaded extends TourState {
|
||||
paymentOptions: paymentOptions ?? this.paymentOptions,
|
||||
sortingInformation: sortingInformation ?? this.sortingInformation,
|
||||
pendingScanRequests: pendingScanRequests ?? this.pendingScanRequests,
|
||||
isPersistingSorting: isPersistingSorting ?? this.isPersistingSorting,
|
||||
sortingPersistError: clearSortingPersistError
|
||||
? null
|
||||
: (sortingPersistError ?? this.sortingPersistError),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user