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>
115 lines
3.9 KiB
Dart
115 lines
3.9 KiB
Dart
import 'package:hl_lieferservice/domain/entity/tour_details.dart';
|
|
|
|
/// Lifecycle-States des `TourBloc`.
|
|
///
|
|
/// Bewusst eine sealed-Hierarchie: das UI kann via `switch` exhaustiv
|
|
/// alle Pfade abbilden und der Compiler meldet, wenn ein neuer Pfad
|
|
/// dazukommt.
|
|
sealed class TourState {
|
|
const TourState();
|
|
}
|
|
|
|
/// App-Start, bevor irgendetwas geladen wurde.
|
|
class TourInitial extends TourState {
|
|
const TourInitial();
|
|
}
|
|
|
|
/// Initial-Load läuft. Wird nur emittiert, wenn vorher kein Tour-State da
|
|
/// war — für Refresh siehe `TourLoaded.isRefreshing`.
|
|
class TourLoading extends TourState {
|
|
const TourLoading();
|
|
}
|
|
|
|
/// Initial-Load ist gescheitert. Refresh-Fehler hingegen werden in
|
|
/// `TourLoaded.refreshError` getragen, damit die alte Tour sichtbar bleibt.
|
|
class TourLoadFailed extends TourState {
|
|
const TourLoadFailed({required this.message});
|
|
|
|
final String message;
|
|
}
|
|
|
|
/// Erfolgreich geladen — beinhaltet das volle Tour-Aggregat sowie
|
|
/// UI-relevante Zusatzflags rund um Reorder- und Refresh-Operationen.
|
|
class TourLoaded extends TourState {
|
|
const TourLoaded({
|
|
required this.details,
|
|
this.isRefreshing = false,
|
|
this.isPersistingReorder = false,
|
|
this.refreshError,
|
|
this.reorderError,
|
|
});
|
|
|
|
final TourDetails details;
|
|
|
|
/// Hintergrund-Reload läuft (Pull-to-Refresh, Provider-Wakeup). UI darf
|
|
/// die alte Daten weiter zeigen und nur einen schmalen Indikator
|
|
/// einblenden.
|
|
final bool isRefreshing;
|
|
|
|
/// `PUT /tours/{id}/delivery-order` läuft. Sortier-Page nutzt das für
|
|
/// den Bestätigungs-Button.
|
|
final bool isPersistingReorder;
|
|
|
|
/// Fehler eines Hintergrund-Reloads — bleibt für eine einzelne Snackbar
|
|
/// hängen und wird beim nächsten Reload geleert.
|
|
final String? refreshError;
|
|
|
|
/// Fehler des letzten Reorder-Persist-Versuchs.
|
|
final String? reorderError;
|
|
|
|
TourLoaded copyWith({
|
|
TourDetails? details,
|
|
bool? isRefreshing,
|
|
bool? isPersistingReorder,
|
|
Object? refreshError = _sentinel,
|
|
Object? reorderError = _sentinel,
|
|
}) {
|
|
return TourLoaded(
|
|
details: details ?? this.details,
|
|
isRefreshing: isRefreshing ?? this.isRefreshing,
|
|
isPersistingReorder: isPersistingReorder ?? this.isPersistingReorder,
|
|
refreshError: identical(refreshError, _sentinel)
|
|
? this.refreshError
|
|
: refreshError as String?,
|
|
reorderError: identical(reorderError, _sentinel)
|
|
? this.reorderError
|
|
: reorderError as String?,
|
|
);
|
|
}
|
|
|
|
/// Spezialfall: Initial-Load ist erfolgreich, aber das Backend hat dem
|
|
/// angemeldeten Fahrer keine Tour für heute zugewiesen (kein ERP-Sync,
|
|
/// Urlaub, …). UI kann darauf einen freundlichen Hinweis statt einer
|
|
/// leeren Liste anzeigen.
|
|
bool get isEmpty => details.deliveries.isEmpty;
|
|
}
|
|
|
|
const Object _sentinel = Object();
|
|
|
|
/// Erfolgs-Spezialform für „heute keine Tour zugewiesen". Wir behandeln das
|
|
/// als eigenständigen State (statt als `TourLoaded` mit leeren Listen),
|
|
/// damit das UI im Routing klar trennen kann zwischen „Tour vorhanden,
|
|
/// gerade keine Lieferungen offen" und „gar keine Tour für heute".
|
|
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;
|
|
}
|
|
}
|