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)
126 lines
4.8 KiB
Dart
126 lines
4.8 KiB
Dart
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.
|
|
}
|
|
}
|
|
}
|