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>
This commit is contained in:
@ -29,9 +29,12 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
||||
required this.tourRepository,
|
||||
required this.opBloc,
|
||||
required this.attachmentCache,
|
||||
}) : super(const TourInitial()) {
|
||||
DateTime Function()? clock,
|
||||
}) : _clock = clock ?? DateTime.now,
|
||||
super(const TourInitial()) {
|
||||
on<LoadTour>(_onLoad);
|
||||
on<RefreshTour>(_onRefresh);
|
||||
on<RefreshTourIfDayChanged>(_onRefreshIfDayChanged);
|
||||
on<ReorderDeliveries>(_onReorder);
|
||||
on<AssignCarToDelivery>(_onAssignCar);
|
||||
on<AssignCarToDeliveries>(_onAssignCarBulk);
|
||||
@ -67,6 +70,18 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
||||
|
||||
final Uuid _uuid = const Uuid();
|
||||
|
||||
final DateTime Function() _clock;
|
||||
|
||||
/// Kalendertag der letzten erfolgreichen Server-Antwort. Absichtlich nicht
|
||||
/// das Tour-Datum: Das darf vom Gerät abweichen (Server entscheidet über
|
||||
/// „heute") und würde dann bei jeder Rückkehr neu laden.
|
||||
DateTime? _loadedDay;
|
||||
|
||||
DateTime _today() {
|
||||
final now = _clock();
|
||||
return DateTime(now.year, now.month, now.day);
|
||||
}
|
||||
|
||||
/// Aktuell angefragtes Tour-Datum. `null` = „heute" (Server entscheidet).
|
||||
/// Wird vom [LoadTour]-Event gesetzt und von [RefreshTour] wiederverwendet,
|
||||
/// damit ein Pull-to-refresh denselben Tag neu lädt (nicht heute).
|
||||
@ -96,6 +111,7 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
||||
try {
|
||||
final details = await tourRepository.getMyTourDetails(date: _selectedDate);
|
||||
if (seq != _requestSeq) return; // überholt von neuerer Anfrage
|
||||
_loadedDay = _today();
|
||||
if (details == null) {
|
||||
emit(const TourEmpty());
|
||||
return;
|
||||
@ -127,6 +143,7 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
||||
try {
|
||||
final details = await tourRepository.getMyTourDetails(date: _selectedDate);
|
||||
if (seq != _requestSeq) return; // überholt von neuerer Anfrage
|
||||
_loadedDay = _today();
|
||||
if (details == null) {
|
||||
emit(const TourEmpty());
|
||||
return;
|
||||
@ -147,6 +164,20 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onRefreshIfDayChanged(
|
||||
RefreshTourIfDayChanged event,
|
||||
Emitter<TourState> emit,
|
||||
) async {
|
||||
final loadedDay = _loadedDay;
|
||||
if (loadedDay == null || _selectedDate != null) return;
|
||||
if (loadedDay == _today()) return;
|
||||
final current = state;
|
||||
// Läuft schon ein Laden, liefert es den neuen Tag ohnehin.
|
||||
if (current is TourLoading) return;
|
||||
if (current is TourLoaded && current.isRefreshing) return;
|
||||
await _onRefresh(const RefreshTour(), emit);
|
||||
}
|
||||
|
||||
// ─── ReorderDeliveries ───────────────────────────────────────────────
|
||||
|
||||
Future<void> _onReorder(
|
||||
@ -203,8 +234,7 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
||||
AssignCarToDelivery event,
|
||||
Emitter<TourState> emit,
|
||||
) async {
|
||||
final current = state;
|
||||
if (current is! TourLoaded) return;
|
||||
if (state is! TourLoaded) return;
|
||||
|
||||
opBloc.add(StartOperation());
|
||||
try {
|
||||
@ -217,15 +247,7 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
||||
// Wir bauen die lokale Delivery so um, dass alle Stamm-Felder vom
|
||||
// Server übernommen werden, Items und sortOrder aus dem lokalen
|
||||
// Aggregat erhalten bleiben.
|
||||
final localDelivery = current.details.deliveries
|
||||
.firstWhere((d) => d.id == event.deliveryId, orElse: () => remoteDelivery);
|
||||
|
||||
final merged = remoteDelivery.copyWith(
|
||||
sortOrder: localDelivery.sortOrder,
|
||||
items: localDelivery.items,
|
||||
);
|
||||
|
||||
emit(current.copyWith(details: current.details.replaceDelivery(merged)));
|
||||
_applyRemoteDelivery(emit, remoteDelivery);
|
||||
opBloc.add(FinishOperation());
|
||||
} catch (e, st) {
|
||||
debugPrint('TourBloc.AssignCarToDelivery fehlgeschlagen: $e\n$st');
|
||||
@ -657,7 +679,7 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
||||
details: latest.details.copyWith(paymentsByDeliveryId: next),
|
||||
));
|
||||
}
|
||||
opBloc.add(FinishOperation(message: 'Zahlung protokolliert'));
|
||||
opBloc.add(FinishOperation());
|
||||
} catch (e, st) {
|
||||
debugPrint('TourBloc.RecordDeliveryPayment fehlgeschlagen: $e\n$st');
|
||||
opBloc.add(FailOperation(
|
||||
@ -778,7 +800,7 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
||||
_replaceAmountCreditNotes(details, event.deliveryId, newNote);
|
||||
emit(latest.copyWith(details: details));
|
||||
}
|
||||
opBloc.add(FinishOperation(message: 'Gutschrift gespeichert'));
|
||||
opBloc.add(FinishOperation());
|
||||
} catch (e, st) {
|
||||
debugPrint('TourBloc.SetDeliveryCredit fehlgeschlagen: $e\n$st');
|
||||
final latest = state;
|
||||
@ -829,7 +851,7 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
||||
),
|
||||
));
|
||||
}
|
||||
opBloc.add(FinishOperation(message: 'Gutschrift entfernt'));
|
||||
opBloc.add(FinishOperation());
|
||||
} catch (e, st) {
|
||||
debugPrint('TourBloc.RemoveDeliveryCredit fehlgeschlagen: $e\n$st');
|
||||
final latest = state;
|
||||
@ -912,7 +934,7 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
||||
),
|
||||
));
|
||||
}
|
||||
opBloc.add(FinishOperation(message: 'Checkliste-Eintrag gespeichert'));
|
||||
opBloc.add(FinishOperation());
|
||||
} catch (e, st) {
|
||||
debugPrint('TourBloc.SetDeliveryServiceValue fehlgeschlagen: $e\n$st');
|
||||
final latest = state;
|
||||
@ -958,7 +980,7 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
||||
deliveryId: event.deliveryId,
|
||||
serviceId: event.serviceId,
|
||||
);
|
||||
opBloc.add(FinishOperation(message: 'Checkliste-Eintrag entfernt'));
|
||||
opBloc.add(FinishOperation());
|
||||
} catch (e, st) {
|
||||
debugPrint('TourBloc.RemoveDeliveryServiceValue fehlgeschlagen: $e\n$st');
|
||||
final latest = state;
|
||||
@ -1059,7 +1081,6 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
||||
) async {
|
||||
await _runLifecycle(
|
||||
emit: emit,
|
||||
deliveryId: event.deliveryId,
|
||||
operationLabel: 'Lieferung abbrechen',
|
||||
apply: () => tourRepository.cancelDelivery(
|
||||
deliveryId: event.deliveryId,
|
||||
@ -1074,7 +1095,6 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
||||
) async {
|
||||
await _runLifecycle(
|
||||
emit: emit,
|
||||
deliveryId: event.deliveryId,
|
||||
operationLabel: 'Lieferung pausieren',
|
||||
apply: () => tourRepository.holdDelivery(
|
||||
deliveryId: event.deliveryId,
|
||||
@ -1089,7 +1109,6 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
||||
) async {
|
||||
await _runLifecycle(
|
||||
emit: emit,
|
||||
deliveryId: event.deliveryId,
|
||||
operationLabel: 'Lieferung fortsetzen',
|
||||
apply: () =>
|
||||
tourRepository.resumeDelivery(deliveryId: event.deliveryId),
|
||||
@ -1102,7 +1121,6 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
||||
) async {
|
||||
await _runLifecycle(
|
||||
emit: emit,
|
||||
deliveryId: event.deliveryId,
|
||||
operationLabel: 'Lieferung abschließen',
|
||||
apply: () => tourRepository.completeDelivery(
|
||||
deliveryId: event.deliveryId,
|
||||
@ -1119,6 +1137,26 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
||||
);
|
||||
}
|
||||
|
||||
/// Mergt die Server-Antwort in den **aktuellen** Stand, nicht in den von vor
|
||||
/// dem Request: Sonst ginge ein währenddessen fertig gewordenes Neuladen
|
||||
/// verloren. Ein noch laufendes Neuladen wird verworfen, denn seine Antwort
|
||||
/// kennt diese Änderung eventuell noch nicht und würde sie zurückdrehen.
|
||||
void _applyRemoteDelivery(Emitter<TourState> emit, Delivery remote) {
|
||||
final latest = state;
|
||||
if (latest is! TourLoaded) return;
|
||||
++_requestSeq;
|
||||
final local = latest.details.deliveries
|
||||
.firstWhere((d) => d.id == remote.id, orElse: () => remote);
|
||||
final merged = remote.copyWith(
|
||||
sortOrder: local.sortOrder,
|
||||
items: local.items,
|
||||
);
|
||||
emit(latest.copyWith(
|
||||
isRefreshing: false,
|
||||
details: latest.details.replaceDelivery(merged),
|
||||
));
|
||||
}
|
||||
|
||||
/// Gemeinsamer Pfad für die drei Lifecycle-Operationen: globaler
|
||||
/// Loading-Overlay an, Server-Call, lokales Aggregat mit der neuen
|
||||
/// Stamm-Delivery mergen (Items/sortOrder aus dem aktuellen State
|
||||
@ -1127,25 +1165,14 @@ class TourBloc extends Bloc<TourEvent, TourState> {
|
||||
/// Reason-Dialog reagiert.
|
||||
Future<void> _runLifecycle({
|
||||
required Emitter<TourState> emit,
|
||||
required String deliveryId,
|
||||
required String operationLabel,
|
||||
required Future<Delivery> Function() apply,
|
||||
}) async {
|
||||
final current = state;
|
||||
if (current is! TourLoaded) return;
|
||||
if (state is! TourLoaded) return;
|
||||
|
||||
opBloc.add(StartOperation());
|
||||
try {
|
||||
final remote = await apply();
|
||||
final local = current.details.deliveries
|
||||
.firstWhere((d) => d.id == deliveryId, orElse: () => remote);
|
||||
final merged = remote.copyWith(
|
||||
sortOrder: local.sortOrder,
|
||||
items: local.items,
|
||||
);
|
||||
emit(current.copyWith(
|
||||
details: current.details.replaceDelivery(merged),
|
||||
));
|
||||
_applyRemoteDelivery(emit, await apply());
|
||||
opBloc.add(FinishOperation());
|
||||
} catch (e, st) {
|
||||
debugPrint('TourBloc.$operationLabel fehlgeschlagen: $e\n$st');
|
||||
|
||||
@ -38,6 +38,15 @@ class RefreshTour extends TourEvent {
|
||||
const RefreshTour();
|
||||
}
|
||||
|
||||
/// Lädt die Tour neu, wenn seit dem letzten Laden der Tag gewechselt hat und
|
||||
/// „heute" angezeigt wird. Eine bewusst im Kalender gewählte Tour bleibt.
|
||||
///
|
||||
/// Auslöser ist die Rückkehr in die Tour-Ansicht (siehe `TourDayWatcher`):
|
||||
/// Die App bleibt oft über Nacht geöffnet und zeigte sonst die Tour von gestern.
|
||||
class RefreshTourIfDayChanged extends TourEvent {
|
||||
const RefreshTourIfDayChanged();
|
||||
}
|
||||
|
||||
/// Schreibt die Sortier-Reihenfolge aller Lieferungen einer Tour an das
|
||||
/// Backend. Der Bloc fordert die UI nicht auf, die vollständige Reihenfolge
|
||||
/// zu kennen — sie wird aus der Liste übergeben.
|
||||
|
||||
@ -54,7 +54,8 @@ class TourLoaded extends TourState {
|
||||
/// hängen und wird beim nächsten Reload geleert.
|
||||
final String? refreshError;
|
||||
|
||||
/// Fehler des letzten Reorder-Persist-Versuchs.
|
||||
/// Fehler des letzten Reorder-Persist-Versuchs. Steuert nur den Ablauf der
|
||||
/// Sortier-Page; angezeigt wird er über `FailOperation`.
|
||||
final String? reorderError;
|
||||
|
||||
TourLoaded copyWith({
|
||||
|
||||
Reference in New Issue
Block a user