Files
Holzleitner-Lieferservice-App/lib/feature/delivery/bloc/tour_state.dart
Dennis Nemec bc95fe9577 fix: Fehlerbehandlung beim Laden und Meldungen überarbeiten
Reklamationen der Fahrer: Meldungen verdeckten Buttons, und nach dem
Abschluss stand „Lieferung nicht in der Tour gefunden" ohne Ausweg da.

Meldungen:
- Erfolgs-SnackBars entfernt, bei Erfolg bleibt der Spinner
- einheitliche, schwebende Meldungen statt Stau über den Buttons
- Server-Rohtexte durch verständliche Texte ersetzt (api_failure_text)

Laden:
- fehlt eine geöffnete Lieferung, wird die Tour einmal neu geholt; die
  Meldung kommt nur nach erfolgreichem Abruf ohne die Lieferung
- Endlos-Spinner auf Lieferdetail, Filial-Abholung und Beladen ersetzt
  durch Kopfzeile, Fehlergrund und „Erneut versuchen"
- Tour wird nach einem Tageswechsel nachgeladen, offene Seiten bleiben
  dabei unberührt
- Abschluss und Zuweisung überschreiben kein zwischenzeitliches Neuladen,
  alte Antworten drehen einen Abschluss nicht mehr zurück
- leere Server-Antworten gelten als Fehler statt als „keine Daten"

Anmeldung:
- Token-Erneuerung wartet höchstens 12 s, der native Aufruf läuft weiter
- App-Start ohne Netz zeigt einen Hinweis mit „Erneut versuchen"
- Fehlerseite der Fahrzeugauswahl bietet „Abmelden"

Außerdem Podfile.lock an die Plugins angeglichen. Tests für die neuen
Fehlerpfade ergänzt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 20:28:24 +01:00

116 lines
4.0 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. Steuert nur den Ablauf der
/// Sortier-Page; angezeigt wird er über `FailOperation`.
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;
}
}