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.
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user