Nach einem Tour-Wechsel (Tageswechsel im Kopf-Kalender, Resync) blieben bis zum asynchronen Nachladen aus den SharedPreferences noch Phase und Haekchen (maxReached) der vorherigen Tour sichtbar; zudem konnte ein persistiertes Max uebernommen werden, obwohl fuer die neue Tour-Version gar keine Phase persistiert war. - Bei Tour-Wechsel eines bereits geladenen Autos den In-Memory-Stand SOFORT synchron auf die Eintrittsphase setzen (nur Sortieren aktiv, keine Haekchen); ein persistierter Stand der neuen Tour folgt per PhaseLoaded - persistedMax nur uebernehmen, wenn fuer diese Tour-Version auch eine Phase persistiert ist (frische Tour-Version startet immer ohne ererbte Haekchen) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
178 lines
7.4 KiB
Dart
178 lines
7.4 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();
|
|
|
|
/// Liefert einen Token, der die aktuell geladene Tour-Version identifiziert
|
|
/// (typischerweise aus `Tour.syncedAt`). Der [PhaseService] bindet die
|
|
/// persistierten Phasen an diesen Token — ein neuer ERP-Sync / Demo-Seed
|
|
/// erzeugt einen neuen Token und damit einen frischen Phasen-Stand.
|
|
///
|
|
/// Liefert `null`, wenn (noch) keine Tour geladen ist; der BLoC nutzt dann
|
|
/// einen neutralen Fallback-Token.
|
|
typedef TourTokenResolver = String? Function();
|
|
|
|
/// Zentraler State für die aktuelle Phase je Fahrzeug. Persistiert über
|
|
/// [PhaseService] auf tour-token-bezogene 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;
|
|
|
|
/// Liefert den Tour-Token (aus `Tour.syncedAt`). Bindet die persistierten
|
|
/// Phasen an die aktuelle Tour-Version.
|
|
final TourTokenResolver? tourTokenResolver;
|
|
|
|
/// Merkt sich pro Fahrzeug, unter welchem Tour-Token der In-Memory-Stand
|
|
/// geladen wurde. Ändert sich der Token (Tageswechsel im Kopf-Kalender,
|
|
/// neuer Sync), lädt der nächste [PhaseLoadForCar] den persistierten Stand
|
|
/// der **neuen** Tour nach, statt den alten Tag weiterzuzeigen. Der alte
|
|
/// Tag bleibt persistiert und wird bei Rückkehr wiederhergestellt.
|
|
final Map<String, String> _loadedTokenByCar = {};
|
|
|
|
PhaseBloc({
|
|
PhaseService? phaseService,
|
|
this.carCountResolver,
|
|
this.tourTokenResolver,
|
|
}) : phaseService = phaseService ?? PhaseService(),
|
|
super(PhaseInitial()) {
|
|
on<PhaseLoadForCar>(_load);
|
|
on<PhaseLoaded>(_applyLoaded);
|
|
on<PhaseSet>(_set);
|
|
}
|
|
|
|
/// Aktueller Tour-Token oder neutraler Fallback, falls noch keine Tour
|
|
/// geladen ist (z. B. `TourEmpty`) — dort ist die Phase ohnehin
|
|
/// bedeutungslos.
|
|
String _token() => tourTokenResolver?.call() ?? 'no-tour';
|
|
|
|
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();
|
|
final token = _token();
|
|
// Bereits für GENAU diese Tour-Version geladen → nichts tun (der
|
|
// Stepper-Tap entscheidet aktiv). Ein anderer Token (Tageswechsel, neuer
|
|
// Sync) lädt den persistierten Stand der neuen Tour nach: ein neuer Tag
|
|
// startet so wieder in der Eintrittsphase (Beladen/Ausliefern zurück),
|
|
// der vorherige Tag behält seinen Stand für die Rückkehr.
|
|
if (current.phaseByCar.containsKey(event.carId) &&
|
|
_loadedTokenByCar[event.carId] == token) {
|
|
return;
|
|
}
|
|
|
|
// Tour-Wechsel bei bereits geladenem Auto: den alten In-Memory-Stand
|
|
// SOFORT synchron auf die Eintrittsphase zurücksetzen (nur „Sortieren"
|
|
// aktiv, keine Häkchen), damit während des asynchronen Nachladens aus
|
|
// den SharedPreferences nichts von der vorherigen Tour sichtbar bleibt.
|
|
// Ein evtl. persistierter Stand der neuen Tour (Rückkehr zu einem alten
|
|
// Tag) folgt direkt danach per PhaseLoaded.
|
|
if (current.phaseByCar.containsKey(event.carId)) {
|
|
final entry = _entryPhase();
|
|
emit(current.withLoaded(event.carId, entry, entry));
|
|
}
|
|
|
|
try {
|
|
final persisted = await phaseService.load(event.carId, token);
|
|
final persistedMax = await phaseService.loadMax(event.carId, token);
|
|
final phase = persisted ?? _entryPhase();
|
|
// Max ist mindestens die aktuelle Phase. Ein höheres persistiertes Max
|
|
// (Rücksprung am selben Tag) wird NUR übernommen, wenn für diese
|
|
// Tour-Version auch eine Phase persistiert ist. Eine frische
|
|
// Tour-Version (persisted == null) startet immer ohne ererbte Häkchen.
|
|
DeliveryPhase maxPhase = phase;
|
|
if (persisted != null &&
|
|
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, token, phase);
|
|
await phaseService.saveMax(event.carId, token, maxPhase);
|
|
} else if (persistedMax == null) {
|
|
// Migration: alte Tage ohne Max-Tracking → einmalig nachziehen.
|
|
await phaseService.saveMax(event.carId, token, maxPhase);
|
|
}
|
|
|
|
_loadedTokenByCar[event.carId] = token;
|
|
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();
|
|
_loadedTokenByCar[event.carId] = token;
|
|
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 {
|
|
final token = _token();
|
|
await phaseService.save(event.carId, token, 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, token, newMax);
|
|
}
|
|
} catch (e, st) {
|
|
debugPrint("PhaseBloc._set: $e $st");
|
|
// UI bleibt konsistent, Persistenz-Fehler ignorieren wir bewusst —
|
|
// beim nächsten Setzen wird erneut versucht.
|
|
}
|
|
}
|
|
}
|