fix(phasen): ohne Tour nur erster Schritt aktiv - Stepper sperrt Beladen/Ausliefern

An einem Tag ohne Tour konnte man in der AppBar durch alle Stepper-Schritte
navigieren. Ursache: an leeren Tagen gilt der GETEILTE 'no-tour'-Persistenz-
Eimer; einmal dort aufgeschlossener Fortschritt (hohes Max) machte an JEDEM
leeren Tag Beladen/Ausliefern anklickbar.

- Stepper: ohne geladene Tour (TourEmpty/loading) ist maxReached hart der
  erste sichtbare Schritt -> weitere Schritte nicht anklickbar, keine Haekchen
- PhaseBloc._load: ohne Tour ausschliesslich Eintrittsphase, Persistenz wird
  weder gelesen noch geschrieben (kein Fortschritt im 'no-tour'-Eimer)
- PhaseBloc._set: ohne Tour jeden Vorwaerts-Wechsel verwerfen, nichts
  persistieren - unabhaengig vom ausloesenden Button/Tap

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dennis Nemec
2026-09-09 01:32:14 +02:00
parent 3add6b498e
commit a027c93c62
2 changed files with 35 additions and 1 deletions

View File

@ -66,6 +66,14 @@ class PhaseBloc extends Bloc<PhaseEvent, PhaseState> {
/// bedeutungslos.
String _token() => tourTokenResolver?.call() ?? 'no-tour';
/// `true`, wenn aktuell eine Tour geladen ist (der Resolver liefert einen
/// Token). Ohne Resolver (z. B. in Tests) gilt „Tour vorhanden", damit
/// nichts unnötig gesperrt wird.
bool _hasTour() {
final resolve = tourTokenResolver;
return resolve == null || resolve() != null;
}
PhaseReady _ensureReady() {
final current = state;
return current is PhaseReady
@ -106,6 +114,17 @@ class PhaseBloc extends Bloc<PhaseEvent, PhaseState> {
emit(current.withLoaded(event.carId, entry, entry));
}
// Keine Tour geladen (Tag ohne Tour): ausschließlich die Eintrittsphase,
// ohne Persistenz zu lesen oder zu schreiben. Der geteilte
// 'no-tour'-Eimer darf keinen Fortschritt tragen — sonst wären an jedem
// leeren Tag Beladen/Ausliefern anklickbar.
if (!_hasTour()) {
final entry = _entryPhase();
_loadedTokenByCar[event.carId] = token;
add(PhaseLoaded(carId: event.carId, phase: entry, maxPhase: entry));
return;
}
try {
final persisted = await phaseService.load(event.carId, token);
final persistedMax = await phaseService.loadMax(event.carId, token);
@ -156,9 +175,17 @@ class PhaseBloc extends Bloc<PhaseEvent, PhaseState> {
}
Future<void> _set(PhaseSet event, Emitter<PhaseState> emit) async {
final hasTour = _hasTour();
// Ohne geladene Tour gibt es nichts zu beladen/auszuliefern: nur die
// Eintrittsphase ist erlaubt, jeder Vorwärts-Wechsel wird verworfen —
// egal, welcher Button oder Stepper-Tap ihn auslöst.
if (!hasTour && event.phase.index > _entryPhase().index) return;
final current = _ensureReady();
final next = current.withPhase(event.carId, event.phase);
emit(next);
// Ohne Tour nichts persistieren — der 'no-tour'-Eimer darf keinen
// Fortschritt tragen (s. _load).
if (!hasTour) return;
try {
final token = _token();
await phaseService.save(event.carId, token, event.phase);