Files
Holzleitner-Lieferservice-App/lib/feature/delivery/bloc/phase_bloc.dart
Dennis Nemec 456fb59668 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)
2026-05-14 22:27:56 +02:00

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.
}
}
}