Compare commits
2 Commits
633b7631aa
...
7c6f99dda1
| Author | SHA1 | Date | |
|---|---|---|---|
| 7c6f99dda1 | |||
| c5219343a0 |
@ -43,6 +43,13 @@ class PhaseBloc extends Bloc<PhaseEvent, PhaseState> {
|
|||||||
/// Phasen an die aktuelle Tour-Version.
|
/// Phasen an die aktuelle Tour-Version.
|
||||||
final TourTokenResolver? tourTokenResolver;
|
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({
|
PhaseBloc({
|
||||||
PhaseService? phaseService,
|
PhaseService? phaseService,
|
||||||
this.carCountResolver,
|
this.carCountResolver,
|
||||||
@ -77,11 +84,18 @@ class PhaseBloc extends Bloc<PhaseEvent, PhaseState> {
|
|||||||
|
|
||||||
Future<void> _load(PhaseLoadForCar event, Emitter<PhaseState> emit) async {
|
Future<void> _load(PhaseLoadForCar event, Emitter<PhaseState> emit) async {
|
||||||
final current = _ensureReady();
|
final current = _ensureReady();
|
||||||
// Wenn bereits geladen, nichts tun — der Stepper-Tap entscheidet aktiv.
|
final token = _token();
|
||||||
if (current.phaseByCar.containsKey(event.carId)) return;
|
// 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 {
|
try {
|
||||||
final token = _token();
|
|
||||||
final persisted = await phaseService.load(event.carId, token);
|
final persisted = await phaseService.load(event.carId, token);
|
||||||
final persistedMax = await phaseService.loadMax(event.carId, token);
|
final persistedMax = await phaseService.loadMax(event.carId, token);
|
||||||
final phase = persisted ?? _entryPhase();
|
final phase = persisted ?? _entryPhase();
|
||||||
@ -102,6 +116,7 @@ class PhaseBloc extends Bloc<PhaseEvent, PhaseState> {
|
|||||||
await phaseService.saveMax(event.carId, token, maxPhase);
|
await phaseService.saveMax(event.carId, token, maxPhase);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_loadedTokenByCar[event.carId] = token;
|
||||||
add(PhaseLoaded(
|
add(PhaseLoaded(
|
||||||
carId: event.carId,
|
carId: event.carId,
|
||||||
phase: phase,
|
phase: phase,
|
||||||
@ -111,6 +126,7 @@ class PhaseBloc extends Bloc<PhaseEvent, PhaseState> {
|
|||||||
debugPrint("PhaseBloc._load: $e $st");
|
debugPrint("PhaseBloc._load: $e $st");
|
||||||
// Fail-soft: ohne Persistenz weiter, damit der Flow nicht hängen bleibt.
|
// Fail-soft: ohne Persistenz weiter, damit der Flow nicht hängen bleibt.
|
||||||
final fallback = _entryPhase();
|
final fallback = _entryPhase();
|
||||||
|
_loadedTokenByCar[event.carId] = token;
|
||||||
add(PhaseLoaded(
|
add(PhaseLoaded(
|
||||||
carId: event.carId,
|
carId: event.carId,
|
||||||
phase: fallback,
|
phase: fallback,
|
||||||
|
|||||||
@ -93,3 +93,22 @@ const Object _sentinel = Object();
|
|||||||
class TourEmpty extends TourState {
|
class TourEmpty extends TourState {
|
||||||
const TourEmpty();
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -67,11 +67,22 @@ class LoadingOverviewPage extends StatelessWidget {
|
|||||||
if (tourState is TourLoadFailed) {
|
if (tourState is TourLoadFailed) {
|
||||||
return const DeliveryLoadingFailedPage();
|
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) {
|
if (tourState is TourEmpty) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
drawer: const HomeAppDrawer(),
|
drawer: const HomeAppDrawer(),
|
||||||
appBar: AppBar(title: const Text('Beladung')),
|
appBar: stepperHeader,
|
||||||
body: const _EmptyOverview(),
|
body: const SafeArea(top: false, child: _EmptyOverview()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (tourState is! TourLoaded) {
|
if (tourState is! TourLoaded) {
|
||||||
@ -79,8 +90,11 @@ class LoadingOverviewPage extends StatelessWidget {
|
|||||||
// damit der Fahrer beim Hängen nicht ohne Logout dasitzt.
|
// damit der Fahrer beim Hängen nicht ohne Logout dasitzt.
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
drawer: const HomeAppDrawer(),
|
drawer: const HomeAppDrawer(),
|
||||||
appBar: AppBar(title: const Text('Beladung')),
|
appBar: stepperHeader,
|
||||||
body: const Center(child: CircularProgressIndicator()),
|
body: const SafeArea(
|
||||||
|
top: false,
|
||||||
|
child: Center(child: CircularProgressIndicator()),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,13 +117,7 @@ class LoadingOverviewPage extends StatelessWidget {
|
|||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
drawer: const HomeAppDrawer(),
|
drawer: const HomeAppDrawer(),
|
||||||
appBar: PreferredSize(
|
appBar: stepperHeader,
|
||||||
preferredSize: const Size.fromHeight(140),
|
|
||||||
child: PhaseStepper(
|
|
||||||
currentPhase: DeliveryPhase.beladen,
|
|
||||||
carId: carId,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
top: false,
|
top: false,
|
||||||
child: ordered.isEmpty
|
child: ordered.isEmpty
|
||||||
|
|||||||
@ -104,14 +104,10 @@ class _DeliveryAppState extends State<DeliveryApp> {
|
|||||||
// Tour-Version (Tour.syncedAt). Ein erneuter Sync/Seed
|
// Tour-Version (Tour.syncedAt). Ein erneuter Sync/Seed
|
||||||
// schreibt eine neue syncedAt → neuer Token → frische
|
// schreibt eine neue syncedAt → neuer Token → frische
|
||||||
// Phasen, ohne dass alte lokale Häkchen hängen bleiben.
|
// Phasen, ohne dass alte lokale Häkchen hängen bleiben.
|
||||||
tourTokenResolver: () {
|
// Ableitung zentral in `TourStatePhaseToken` (tour_state.dart),
|
||||||
final tourState = context.read<TourBloc>().state;
|
// dieselbe Quelle wie `Home._ensurePhaseLoaded`.
|
||||||
return tourState is TourLoaded
|
tourTokenResolver: () =>
|
||||||
? tourState.details.tour.syncedAt
|
context.read<TourBloc>().state.phaseToken,
|
||||||
.millisecondsSinceEpoch
|
|
||||||
.toString()
|
|
||||||
: null;
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
BlocProvider(
|
BlocProvider(
|
||||||
|
|||||||
@ -34,7 +34,12 @@ class Home extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _HomeState extends State<Home> {
|
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
|
/// Merkt, für welches Auto die automatische Lieferungs-Zuordnung bereits
|
||||||
/// Auto, für das zuletzt automatisch zugewiesen wurde (Ein-Auto-Teams).
|
/// 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
|
/// Stellt sicher, dass für das aktuell gewählte Auto eine Phase im
|
||||||
/// [PhaseBloc] existiert. Wird im build() reaktiv aufgerufen, daher mit
|
/// [PhaseBloc] existiert — und zwar für die **aktuell geladene Tour**.
|
||||||
/// `_initializedCarId` gegen mehrfache Loads gesichert.
|
/// 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
|
/// Wichtig: Wir feuern den Load erst, sobald sowohl Tour als auch Cars
|
||||||
/// einen geladen-Zustand haben — sonst fragt der `PhaseBloc` den
|
/// einen geladen-Zustand haben — sonst fragt der `PhaseBloc` den
|
||||||
/// `carCountResolver` ab, bekommt `null` und entscheidet für Mehr-Auto-
|
/// `carCountResolver` ab, bekommt `null` und entscheidet für Mehr-Auto-
|
||||||
/// Teams fälschlich auf `sortieren` statt `auswaehlen`.
|
/// Teams fälschlich auf `sortieren` statt `auswaehlen`.
|
||||||
void _ensurePhaseLoaded(String carId) {
|
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;
|
final carsState = context.read<CarsBloc>().state;
|
||||||
if (carsState is! CarsLoaded) return;
|
if (carsState is! CarsLoaded) return;
|
||||||
_initializedCarId = carId;
|
_initializedPhaseKey = key;
|
||||||
context.read<PhaseBloc>().add(PhaseLoadForCar(carId: carId));
|
context.read<PhaseBloc>().add(PhaseLoadForCar(carId: carId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user