Compare commits

..

2 Commits

Author SHA1 Message Date
7c6f99dda1 fix(phasen): Tageswechsel laedt Phasen-Stand der neuen Tour (Reset), alter Tag bleibt
Beim Datumswechsel im Kopf-Kalender blieben Beladen/Ausliefern des alten
Tags 'erledigt': PhaseBloc._load brach bei bereits geladenem Auto ab und
Home stiess den Load nur einmal pro Auto an - der In-Memory-Stand wurde
beim Tour-/Token-Wechsel nie invalidiert. Die Persistenz ist bereits pro
Tour-Token (syncedAt) getrennt, nur das Nachladen fehlte.

- PhaseBloc: Guard tokenbewusst (_loadedTokenByCar); anderer Token laedt den
  persistierten Stand der neuen Tour nach (neuer Tag -> Eintrittsphase,
  Rueckkehr -> alter Stand wiederhergestellt)
- Home._ensurePhaseLoaded: Guard auf carId|tourToken statt nur carId
- Token-Ableitung zentral als TourState.phaseToken (tour_state.dart), genutzt
  von tourTokenResolver (app.dart) UND Home - eine Quelle statt Duplikat

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-09 01:14:56 +02:00
c5219343a0 fix(beladen): Phasen-Header auch ohne/vor geladener Tour anzeigen
Bei TourEmpty/TourLoading rendert die Beladen-Seite jetzt denselben
PhaseStepper-Header wie im geladenen Zustand statt einer nackten
'Beladung'-AppBar. Damit bleiben Kopf-Kalender (Datumswechsel), Menue und
Reload an einem Tag ohne Tour erreichbar.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-09 01:14:56 +02:00
5 changed files with 75 additions and 27 deletions

View File

@ -43,6 +43,13 @@ class PhaseBloc extends Bloc<PhaseEvent, PhaseState> {
/// 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,
@ -77,11 +84,18 @@ class PhaseBloc extends Bloc<PhaseEvent, PhaseState> {
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;
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;
}
try {
final token = _token();
final persisted = await phaseService.load(event.carId, token);
final persistedMax = await phaseService.loadMax(event.carId, token);
final phase = persisted ?? _entryPhase();
@ -102,6 +116,7 @@ class PhaseBloc extends Bloc<PhaseEvent, PhaseState> {
await phaseService.saveMax(event.carId, token, maxPhase);
}
_loadedTokenByCar[event.carId] = token;
add(PhaseLoaded(
carId: event.carId,
phase: phase,
@ -111,6 +126,7 @@ class PhaseBloc extends Bloc<PhaseEvent, PhaseState> {
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,

View File

@ -93,3 +93,22 @@ const Object _sentinel = Object();
class TourEmpty extends TourState {
const TourEmpty();
}
/// Token, der die aktuell geladene **Tour-Version** identifiziert (aus
/// `Tour.syncedAt`). Der `PhaseBloc` bindet die persistierten Phasen-Häkchen
/// daran: ein Tageswechsel im Kopf-Kalender oder ein neuer Sync ergibt einen
/// neuen Token und damit einen eigenen, getrennten Phasen-Stand. `null` =
/// keine Tour geladen (z. B. `TourEmpty`).
///
/// **Einzige Quelle** für diese Ableitung — wird vom `tourTokenResolver` in
/// `app.dart` UND von `Home._ensurePhaseLoaded` genutzt, damit beide Seiten
/// garantiert denselben Token sehen und ein Tour-Wechsel zuverlässig als
/// solcher erkannt wird.
extension TourStatePhaseToken on TourState {
String? get phaseToken {
final s = this;
return s is TourLoaded
? s.details.tour.syncedAt.millisecondsSinceEpoch.toString()
: null;
}
}

View File

@ -67,11 +67,22 @@ class LoadingOverviewPage extends StatelessWidget {
if (tourState is TourLoadFailed) {
return const DeliveryLoadingFailedPage();
}
// Auch ohne/vor geladener Tour den Phasen-Header zeigen — sonst
// verschwinden Kopf-Kalender, Menü und Reload hinter einer nackten
// 'Beladung'-AppBar und der Fahrer kommt an einem leeren Tag nicht
// mehr zum Datumswechsel.
final stepperHeader = PreferredSize(
preferredSize: const Size.fromHeight(140),
child: PhaseStepper(
currentPhase: DeliveryPhase.beladen,
carId: carId,
),
);
if (tourState is TourEmpty) {
return Scaffold(
drawer: const HomeAppDrawer(),
appBar: AppBar(title: const Text('Beladung')),
body: const _EmptyOverview(),
appBar: stepperHeader,
body: const SafeArea(top: false, child: _EmptyOverview()),
);
}
if (tourState is! TourLoaded) {
@ -79,8 +90,11 @@ class LoadingOverviewPage extends StatelessWidget {
// damit der Fahrer beim Hängen nicht ohne Logout dasitzt.
return Scaffold(
drawer: const HomeAppDrawer(),
appBar: AppBar(title: const Text('Beladung')),
body: const Center(child: CircularProgressIndicator()),
appBar: stepperHeader,
body: const SafeArea(
top: false,
child: Center(child: CircularProgressIndicator()),
),
);
}
@ -103,13 +117,7 @@ class LoadingOverviewPage extends StatelessWidget {
return Scaffold(
drawer: const HomeAppDrawer(),
appBar: PreferredSize(
preferredSize: const Size.fromHeight(140),
child: PhaseStepper(
currentPhase: DeliveryPhase.beladen,
carId: carId,
),
),
appBar: stepperHeader,
body: SafeArea(
top: false,
child: ordered.isEmpty

View File

@ -104,14 +104,10 @@ class _DeliveryAppState extends State<DeliveryApp> {
// Tour-Version (Tour.syncedAt). Ein erneuter Sync/Seed
// schreibt eine neue syncedAt → neuer Token → frische
// Phasen, ohne dass alte lokale Häkchen hängen bleiben.
tourTokenResolver: () {
final tourState = context.read<TourBloc>().state;
return tourState is TourLoaded
? tourState.details.tour.syncedAt
.millisecondsSinceEpoch
.toString()
: null;
},
// Ableitung zentral in `TourStatePhaseToken` (tour_state.dart),
// dieselbe Quelle wie `Home._ensurePhaseLoaded`.
tourTokenResolver: () =>
context.read<TourBloc>().state.phaseToken,
),
),
BlocProvider(

View File

@ -34,7 +34,12 @@ class Home extends StatefulWidget {
}
class _HomeState extends State<Home> {
String? _initializedCarId;
/// Key `carId|tourToken`, für den der [PhaseBloc] zuletzt zum Laden
/// angestoßen wurde. Ändert sich das Auto ODER die Tour-Version
/// (Tageswechsel im Kopf-Kalender, neuer Sync), wird erneut geladen — der
/// Bloc holt dann den persistierten Stand der **neuen** Tour, sodass ein
/// neuer Tag nicht die Beladen-/Ausliefern-Häkchen des alten Tags erbt.
String? _initializedPhaseKey;
/// Merkt, für welches Auto die automatische Lieferungs-Zuordnung bereits
/// Auto, für das zuletzt automatisch zugewiesen wurde (Ein-Auto-Teams).
@ -75,18 +80,22 @@ class _HomeState extends State<Home> {
}
/// Stellt sicher, dass für das aktuell gewählte Auto eine Phase im
/// [PhaseBloc] existiert. Wird im build() reaktiv aufgerufen, daher mit
/// `_initializedCarId` gegen mehrfache Loads gesichert.
/// [PhaseBloc] existiert — und zwar für die **aktuell geladene Tour**.
/// Wird im build() reaktiv aufgerufen, daher per [_initializedPhaseKey]
/// (`carId|tourToken`) gegen mehrfache Loads gesichert. Der Token kommt aus
/// [TourStatePhaseToken.phaseToken] (derselbe wie im `tourTokenResolver`);
/// `null` (keine Tour) zählt dabei als eigener Zustand.
///
/// Wichtig: Wir feuern den Load erst, sobald sowohl Tour als auch Cars
/// einen geladen-Zustand haben — sonst fragt der `PhaseBloc` den
/// `carCountResolver` ab, bekommt `null` und entscheidet für Mehr-Auto-
/// Teams fälschlich auf `sortieren` statt `auswaehlen`.
void _ensurePhaseLoaded(String carId) {
if (_initializedCarId == carId) return;
final key = '$carId|${context.read<TourBloc>().state.phaseToken}';
if (_initializedPhaseKey == key) return;
final carsState = context.read<CarsBloc>().state;
if (carsState is! CarsLoaded) return;
_initializedCarId = carId;
_initializedPhaseKey = key;
context.read<PhaseBloc>().add(PhaseLoadForCar(carId: carId));
}