Final commit.

This commit is contained in:
Dennis Nemec
2026-06-01 17:12:28 +02:00
parent 3ecbc82885
commit a9bf8ecdd1
385 changed files with 29081 additions and 12089 deletions

View File

@ -13,8 +13,17 @@ import 'package:hl_lieferservice/feature/delivery/overview/service/phase_service
/// Default-Eintritt [DeliveryPhase.sortieren].
typedef CarCountResolver = int? Function();
/// Liefert einen Token, der die aktuell geladene Tour-Version identifiziert
/// (typischerweise aus `Tour.syncedAt`). Der [PhaseService] bindet die
/// persistierten Phasen an diesen Token — ein neuer ERP-Sync / Demo-Seed
/// erzeugt einen neuen Token und damit einen frischen Phasen-Stand.
///
/// Liefert `null`, wenn (noch) keine Tour geladen ist; der BLoC nutzt dann
/// einen neutralen Fallback-Token.
typedef TourTokenResolver = String? Function();
/// Zentraler State für die aktuelle Phase je Fahrzeug. Persistiert über
/// [PhaseService] auf datumsbezogene SharedPreferences-Keys (siehe Service).
/// [PhaseService] auf tour-token-bezogene SharedPreferences-Keys (siehe Service).
///
/// Eintrittsphase nach Fahrzeugauswahl:
/// * 1 Auto im Team → [DeliveryPhase.sortieren] (bisheriges Verhalten).
@ -30,9 +39,14 @@ class PhaseBloc extends Bloc<PhaseEvent, PhaseState> {
/// Provider so verdrahtet, dass sie aus dem [TourBloc] kommt.
final CarCountResolver? carCountResolver;
/// Liefert den Tour-Token (aus `Tour.syncedAt`). Bindet die persistierten
/// Phasen an die aktuelle Tour-Version.
final TourTokenResolver? tourTokenResolver;
PhaseBloc({
PhaseService? phaseService,
this.carCountResolver,
this.tourTokenResolver,
}) : phaseService = phaseService ?? PhaseService(),
super(PhaseInitial()) {
on<PhaseLoadForCar>(_load);
@ -40,6 +54,11 @@ class PhaseBloc extends Bloc<PhaseEvent, PhaseState> {
on<PhaseSet>(_set);
}
/// Aktueller Tour-Token oder neutraler Fallback, falls noch keine Tour
/// geladen ist (z. B. `TourEmpty`) — dort ist die Phase ohnehin
/// bedeutungslos.
String _token() => tourTokenResolver?.call() ?? 'no-tour';
PhaseReady _ensureReady() {
final current = state;
return current is PhaseReady
@ -62,8 +81,9 @@ class PhaseBloc extends Bloc<PhaseEvent, PhaseState> {
if (current.phaseByCar.containsKey(event.carId)) return;
try {
final persisted = await phaseService.load(event.carId);
final persistedMax = await phaseService.loadMax(event.carId);
final token = _token();
final persisted = await phaseService.load(event.carId, token);
final persistedMax = await phaseService.loadMax(event.carId, token);
final phase = persisted ?? _entryPhase();
// Max ist mindestens die aktuelle Phase. Falls in der Persistenz ein
// höherer Wert steht (Rücksprung), den nehmen.
@ -75,11 +95,11 @@ class PhaseBloc extends Bloc<PhaseEvent, PhaseState> {
if (persisted == null) {
// Erste Phase nach Fahrzeugauswahl direkt persistieren, damit
// ein Resume nach App-Neustart die Phase kennt.
await phaseService.save(event.carId, phase);
await phaseService.saveMax(event.carId, maxPhase);
await phaseService.save(event.carId, token, phase);
await phaseService.saveMax(event.carId, token, maxPhase);
} else if (persistedMax == null) {
// Migration: alte Tage ohne Max-Tracking → einmalig nachziehen.
await phaseService.saveMax(event.carId, maxPhase);
await phaseService.saveMax(event.carId, token, maxPhase);
}
add(PhaseLoaded(
@ -109,12 +129,13 @@ class PhaseBloc extends Bloc<PhaseEvent, PhaseState> {
final next = current.withPhase(event.carId, event.phase);
emit(next);
try {
await phaseService.save(event.carId, event.phase);
final token = _token();
await phaseService.save(event.carId, token, event.phase);
// withPhase hat das Max ggf. hochgezogen — persistieren, damit ein
// Neustart die "höchste erreichte Phase" kennt.
final newMax = next.maxPhaseFor(event.carId);
if (newMax != null) {
await phaseService.saveMax(event.carId, newMax);
await phaseService.saveMax(event.carId, token, newMax);
}
} catch (e, st) {
debugPrint("PhaseBloc._set: $e $st");

File diff suppressed because it is too large Load Diff

View File

@ -1,277 +1,341 @@
import 'dart:typed_data';
import 'package:hl_lieferservice/model/car.dart';
import 'package:hl_lieferservice/model/tour.dart';
import '../../../../model/delivery.dart';
abstract class TourEvent {}
/// Events für den [`TourBloc`].
///
/// Bewusst minimaler Scope für Phase C+D-2: Lese-Pfad, Reorder,
/// Car-Assign. Scan-, Hold-, Cancel-, Complete-, Discount- und
/// Notiz-Events kommen in C+D-3 / C+D-4 zurück.
sealed class TourEvent {
const TourEvent();
}
/// Initial-Load der heutigen Tour des angemeldeten Fahrers.
/// Account-Filter sitzt im JWT — kein zusätzliches Argument nötig.
class LoadTour extends TourEvent {
String teamId;
LoadTour({required this.teamId});
const LoadTour();
}
class RequestDeliveryDistanceEvent extends TourEvent {
Tour tour;
RequestDeliveryDistanceEvent({required this.tour});
/// Pull-to-refresh / manueller Reload aus der Übersicht. Vorhandener
/// `TourLoaded`-State bleibt sichtbar, bis der neue Snapshot da ist;
/// `TourLoading` wird nur emittiert, wenn vorher kein State existierte.
class RefreshTour extends TourEvent {
const RefreshTour();
}
class RequestSortingInformationEvent extends TourEvent {
Tour tour;
List<Payment> payments;
/// 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.
class ReorderDeliveries extends TourEvent {
const ReorderDeliveries({required this.orderedDeliveryIds});
RequestSortingInformationEvent({
required this.tour,
required this.payments,
});
final List<String> orderedDeliveryIds;
}
class ReorderDeliveryEvent extends TourEvent {
int newPosition;
int oldPosition;
String carId;
/// Weist einer Lieferung ein Fahrzeug zu — oder hebt die Zuweisung auf
/// (`carId == null`).
class AssignCarToDelivery extends TourEvent {
const AssignCarToDelivery({required this.deliveryId, required this.carId});
ReorderDeliveryEvent({required this.newPosition, required this.oldPosition, required this.carId});
}
/// Ersetzt die komplette Sortier-Information (z. B. beim Zurücksetzen auf
/// die Default-Reihenfolge in der Sortier-Page). Persistiert lokal über
/// den ReorderService, kein Backend-Call.
class ReplaceSortingEvent extends TourEvent {
final String carId;
final Map<String, List<String>> newSortingInformation;
ReplaceSortingEvent({
required this.carId,
required this.newSortingInformation,
});
}
/// Bestätigung der Sortierung durch den Fahrer. Löst den (aktuell ge-
/// stubbten) Backend-Call zur Persistierung der Reihenfolge aus und
/// schaltet bei Erfolg in Phase [DeliveryPhase.beladen].
class ConfirmSortingEvent extends TourEvent {
final String carId;
ConfirmSortingEvent({required this.carId});
}
/// Stellt sicher, dass der Sortier-Bucket für [carId] alle aktuell in der
/// Tour vorhandenen Lieferungen enthält — und zwar UNABHÄNGIG von
/// `delivery.carId`. Hintergrund: zum Sortier-Zeitpunkt sind die Lieferungen
/// dem Fahrzeug oft noch nicht zugeordnet (das passiert erst beim Scannen
/// während des Beladens). Der Fahrer hat aber bereits sein Tagesfahrzeug
/// gewählt, also gehören alle Tour-Lieferungen in diesen Bucket. Bestehende
/// Reihenfolge bleibt erhalten, fehlende IDs werden hinten angehängt.
class EnsureSortingForCarEvent extends TourEvent {
final String carId;
EnsureSortingForCarEvent({required this.carId});
}
class TourUpdated extends TourEvent {
Tour tour;
List<Payment> payments;
TourUpdated({required this.tour, required this.payments});
}
class PaymentOptionsUpdated extends TourEvent {
List<Payment> options;
PaymentOptionsUpdated({required this.options});
}
class UpdateTour extends TourEvent {
Tour tour;
UpdateTour({required this.tour});
}
class AssignCarEvent extends TourEvent {
String deliveryId;
String carId;
AssignCarEvent({required this.deliveryId, required this.carId});
}
/// Hebt die Fahrzeug-Zuordnung einer Lieferung wieder auf. Wird im
/// Auswahl-Schritt ausgelöst, wenn der Fahrer eine eigene Lieferung
/// "freigibt", damit ein Kollege sie übernehmen kann. Aktuell ruft der
/// Handler nur den ProcessRepository-Stub auf — ein echtes Update der
/// Tour erfolgt erst mit dem realen Backend-Endpoint (siehe Repository).
class UnassignDeliveryEvent extends TourEvent {
final String deliveryId;
UnassignDeliveryEvent({required this.deliveryId});
final String? carId;
}
class IncrementArticleScanAmount extends TourEvent {
String internalArticleId;
String deliveryId;
String carId;
IncrementArticleScanAmount({
required this.internalArticleId,
required this.deliveryId,
/// Bulk-Variante: weist mehreren Lieferungen in einer atomaren Aktion das
/// gleiche Fahrzeug zu. Notwendig, weil flutter_bloc Events standardmäßig
/// **concurrent** verarbeitet — N parallel laufende `AssignCarToDelivery`-
/// Handler lesen alle den Initial-State und überschreiben sich gegenseitig
/// beim `emit`, sodass nur die letzte Zuweisung sichtbar bleibt. Der
/// Bulk-Handler verarbeitet die Liste sequenziell und emittiert genau
/// einen Zwischen-State am Ende.
///
/// Backend-seitig macht der Handler trotzdem N einzelne HTTP-Calls, weil
/// der `PATCH /deliveries/{id}/assigned-car`-Endpoint keine Liste kennt.
class AssignCarToDeliveries extends TourEvent {
const AssignCarToDeliveries({
required this.deliveryIds,
required this.carId,
});
final List<String> deliveryIds;
final String? carId;
}
class ScanArticleEvent extends TourEvent {
ScanArticleEvent({
required this.articleNumber,
required this.carId,
required this.deliveryId,
/// Scan-Trigger aus der Loading-Phase. Der Bloc inkrementiert lokal
/// **optimistisch** die Scan-Quantität, ruft anschließend `POST /scans`
/// und rollt im `rejected`-Fall lokal zurück. `duplicate` ist still
/// (lokal schon hochgezählt, Server bestätigt nicht erneut).
///
/// `actorCarId` ist die UUID des aktuell gewählten Fahrzeugs — das
/// Backend nutzt das nur als Audit-Spur; an der Lieferung selbst
/// passiert dadurch nichts.
class ScanItem extends TourEvent {
const ScanItem({
required this.deliveryItemId,
required this.actorCarId,
this.manual = false,
});
String articleNumber;
String deliveryId;
String carId;
final String deliveryItemId;
final String actorCarId;
/// `true` = manuelle Zeilen-Bestätigung (Fallback ohne Barcode): die
/// **gesamte Restmenge** wird auf einmal als geladen verbucht und im
/// Backend als `manual` protokolliert. `false` = regulärer Einzel-Scan (+1).
final bool manual;
}
/// Scan a single BOM component. The server call for the parent article is
/// deferred until *all* components are fully scanned.
class ScanComponentEvent extends TourEvent {
ScanComponentEvent({
required this.componentArticleNumber,
required this.carId,
required this.deliveryId,
});
String componentArticleNumber;
String deliveryId;
String carId;
}
class CancelDeliveryEvent extends TourEvent {
String deliveryId;
CancelDeliveryEvent({required this.deliveryId});
}
class HoldDeliveryEvent extends TourEvent {
String deliveryId;
HoldDeliveryEvent({required this.deliveryId});
}
class ReactivateDeliveryEvent extends TourEvent {
String deliveryId;
ReactivateDeliveryEvent({required this.deliveryId});
}
class LoadDeliveryEvent extends TourEvent {
LoadDeliveryEvent({required this.delivery});
Delivery delivery;
}
class UnscanArticleEvent extends TourEvent {
UnscanArticleEvent({
required this.articleId,
required this.newAmount,
required this.reason,
required this.deliveryId,
});
String articleId;
String deliveryId;
String reason;
int newAmount;
}
class ResetScanAmountEvent extends TourEvent {
ResetScanAmountEvent({required this.articleId, required this.deliveryId});
String articleId;
String deliveryId;
}
class AddDiscountEvent extends TourEvent {
AddDiscountEvent({
required this.deliveryId,
required this.value,
/// Umkehrung eines Scans (z. B. „falsch gescannt"). `reason` ist vom
/// Backend für `unscan` erforderlich.
class UnscanItem extends TourEvent {
const UnscanItem({
required this.deliveryItemId,
required this.actorCarId,
required this.reason,
});
String deliveryId;
String reason;
int value;
final String deliveryItemId;
final String actorCarId;
final String reason;
}
class RemoveDiscountEvent extends TourEvent {
RemoveDiscountEvent({required this.deliveryId});
String deliveryId;
}
class UpdateDiscountEvent extends TourEvent {
UpdateDiscountEvent({
required this.deliveryId,
required this.value,
/// Pausiert ein Item (`scan_status=held`). Reversibel über [UnholdItem].
/// `reason` Pflicht.
class HoldItem extends TourEvent {
const HoldItem({
required this.deliveryItemId,
required this.actorCarId,
required this.reason,
});
String deliveryId;
String? reason;
int? value;
final String deliveryItemId;
final String actorCarId;
final String reason;
}
class CarsLoadedEvent extends TourEvent {
List<Car> cars;
CarsLoadedEvent({required this.cars});
}
class UpdateDeliveryOptionEvent extends TourEvent {
UpdateDeliveryOptionEvent({
required this.key,
required this.value,
required this.deliveryId,
/// Setzt ein pausiertes Item zurück auf `in_progress`. Kein Reason
/// nötig — der Server löscht beim Unhold den `held_reason`.
class UnholdItem extends TourEvent {
const UnholdItem({
required this.deliveryItemId,
required this.actorCarId,
});
String deliveryId;
String key;
dynamic value;
final String deliveryItemId;
final String actorCarId;
}
class UpdateSelectedPaymentMethodEvent extends TourEvent {
UpdateSelectedPaymentMethodEvent({
required this.payment,
required this.deliveryId,
/// Entfernt ein Item aus der Lieferung (`scan_status=removed`).
/// `reason` Pflicht. Reversibel über [UnremoveItem] — die Historie
/// bleibt im Audit-Log vollständig erhalten.
class RemoveItem extends TourEvent {
const RemoveItem({
required this.deliveryItemId,
required this.actorCarId,
required this.reason,
this.quantity,
this.saveReasonAsNote = false,
});
Payment payment;
String deliveryId;
final String deliveryItemId;
final String actorCarId;
final String reason;
/// Mengen-Gutschrift: wie viele Stück gutgeschrieben werden. `null` =
/// ganze Restmenge (= klassisches „ganze Zeile entfernen").
final int? quantity;
/// Wenn `true`, wird der `reason` nach erfolgreichem Entfernen zusätzlich
/// als Lieferungs-Notiz gespeichert. Gesetzt aus dem Gutschrift-Flow
/// (Step „Artikel & Gutschriften"); im Beladen-Flow `false`, dort wäre
/// eine Notiz pro Abbuchung nur Rauschen.
final bool saveReasonAsNote;
}
class FinishDeliveryEvent extends TourEvent {
FinishDeliveryEvent({
required this.deliveryId,
required this.driverSignature,
required this.customerSignature,
/// Stellt ein entferntes Item wieder her — geht nur, wenn der aktuelle
/// Status `removed` ist. Kein Reason nötig (Backend-Konvention wie bei
/// `unscan` und `unhold`). Audit-Log behält den ursprünglichen
/// `remove`-Eintrag und fügt einen neuen `unremove`-Eintrag hinzu.
class UnremoveItem extends TourEvent {
const UnremoveItem({
required this.deliveryItemId,
required this.actorCarId,
this.quantity,
});
String deliveryId;
Uint8List customerSignature;
Uint8List driverSignature;
final String deliveryItemId;
final String actorCarId;
/// Mengen-Gutschrift zurücknehmen: wie viele Stück wiederhergestellt
/// werden. `null` = gesamte Gutschrift zurück.
final int? quantity;
}
class SetArticleAmountEvent extends TourEvent {
// ─── Delivery-Lifecycle ───────────────────────────────────────────────
/// Bricht eine Lieferung endgültig ab. `reason` ist Pflicht.
class CancelDelivery extends TourEvent {
const CancelDelivery({required this.deliveryId, required this.reason});
final String deliveryId;
final String articleId;
final String? reason;
final int amount;
final String reason;
}
SetArticleAmountEvent({
/// Pausiert eine Lieferung. `reason` ist Pflicht. Reversibel über
/// [ResumeDelivery].
class HoldDelivery extends TourEvent {
const HoldDelivery({required this.deliveryId, required this.reason});
final String deliveryId;
final String reason;
}
/// Setzt eine pausierte Lieferung zurück auf `active`.
class ResumeDelivery extends TourEvent {
const ResumeDelivery({required this.deliveryId});
final String deliveryId;
}
/// Schließt eine Lieferung ab: lädt beide Unterschriften hoch, dokumentiert
/// die Bestätigungen des Kunden und setzt die Lieferung auf `completed`.
class CompleteDelivery extends TourEvent {
const CompleteDelivery({
required this.deliveryId,
required this.articleId,
required this.amount,
this.reason,
required this.customerSignaturePng,
required this.driverSignaturePng,
required this.receiptConfirmed,
required this.notesAcknowledged,
required this.acknowledgedNoteIds,
this.paymentMethodId,
this.actorCarId,
this.paymentCollected = false,
});
}
final String deliveryId;
final List<int> customerSignaturePng;
final List<int> driverSignaturePng;
final bool receiptConfirmed;
final bool notesAcknowledged;
final List<String> acknowledgedNoteIds;
/// Vom Fahrer im Summary gewählte Zahlungsmethode (Override). `null` =
/// die am Beleg hinterlegte Methode bleibt. Wird beim Abschluss persistiert.
final String? paymentMethodId;
final String? actorCarId;
/// Fahrer hat das Vor-Ort-Inkasso (Bar/EC) des offenen Betrags bestätigt.
/// `false`, wenn kein Inkasso anfiel (offen == 0 oder „Auf Rechnung").
final bool paymentCollected;
}
/// Legt eine neue (Text- oder Bild-)Notiz an einer Lieferung an. Aktuell
/// wird nur der Text-Pfad von der UI getriggert; `imageAttachment` ist als
/// Storage-Key (z. B. Pre-Signed-URL-Key) gedacht und wartet auf die
/// zukünftige Foto-Upload-Phase.
/// Setzt/ändert die Betrags-Gutschrift einer Lieferung (Geld-Nachlass).
class SetDeliveryCredit extends TourEvent {
const SetDeliveryCredit({
required this.deliveryId,
required this.amountCents,
required this.reason,
required this.actorCarId,
});
final String deliveryId;
final int amountCents;
final String reason;
final String actorCarId;
}
/// Entfernt die Betrags-Gutschrift einer Lieferung.
class RemoveDeliveryCredit extends TourEvent {
const RemoveDeliveryCredit({
required this.deliveryId,
required this.actorCarId,
});
final String deliveryId;
final String actorCarId;
}
/// Setzt/ändert den Wert eines Service für eine Lieferung (Checkbox/Zahl).
class SetDeliveryServiceValue extends TourEvent {
const SetDeliveryServiceValue({
required this.deliveryId,
required this.serviceId,
required this.actorCarId,
this.boolValue,
this.numericValue,
});
final String deliveryId;
final String serviceId;
final String actorCarId;
final bool? boolValue;
final int? numericValue;
}
/// Entfernt den Service-Wert einer Lieferung (Service „nicht gesetzt").
class RemoveDeliveryServiceValue extends TourEvent {
const RemoveDeliveryServiceValue({
required this.deliveryId,
required this.serviceId,
});
final String deliveryId;
final String serviceId;
}
class AddDeliveryNote extends TourEvent {
const AddDeliveryNote({
required this.deliveryId,
this.text,
this.imageAttachment,
this.creditDeliveryItemId,
});
final String deliveryId;
final String? text;
final String? imageAttachment;
/// Optionaler Gutschrift-Bezug (DeliveryItem-Id). Gesetzt, wenn die Notiz
/// einen Gutschrift-Grund dokumentiert — ermöglicht das Löschen beim
/// Unremove.
final String? creditDeliveryItemId;
}
/// Ändert Text/Bild einer bestehenden Notiz.
class UpdateDeliveryNote extends TourEvent {
const UpdateDeliveryNote({
required this.deliveryId,
required this.noteId,
this.text,
this.imageAttachment,
});
final String deliveryId;
final String noteId;
final String? text;
final String? imageAttachment;
}
/// Löscht eine Notiz.
class DeleteDeliveryNote extends TourEvent {
const DeleteDeliveryNote({required this.deliveryId, required this.noteId});
final String deliveryId;
final String noteId;
}
/// Lädt ein Bild als Notiz hoch (geht über DOCUframe).
class UploadDeliveryNoteImage extends TourEvent {
const UploadDeliveryNoteImage({
required this.deliveryId,
required this.filename,
required this.mime,
required this.bytes,
});
final String deliveryId;
final String filename;
final String mime;
final List<int> bytes;
}

View File

@ -1,63 +1,95 @@
import '../../../../model/tour.dart';
import 'package:hl_lieferservice/domain/entity/tour_details.dart';
abstract class TourState {}
/// 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();
}
class TourInitial extends TourState {}
/// App-Start, bevor irgendetwas geladen wurde.
class TourInitial extends TourState {
const TourInitial();
}
class TourLoading extends TourState {}
/// 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();
}
class TourLoadingFailed extends TourState {}
/// 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 {
Tour tour;
Map<String, double>? distances;
List<Payment> paymentOptions;
Map<String, List<String>> sortingInformation;
/// Number of scan-related server requests currently in flight. Drives the
/// inline indicator on the scanner widget. Using a counter (not bool) lets
/// rapid-fire scans coexist without one prematurely clearing the indicator.
int pendingScanRequests;
/// True während der Backend-Call zur Persistierung der Sortier-Reihenfolge
/// läuft. Wird vom Bestätigungs-Button in der Sortier-Page für Spinner
/// und Button-Disabled-State ausgewertet.
bool isPersistingSorting;
/// Letzte Fehlermeldung des Sortier-Persist-Calls. Wird nach Anzeige
/// durch das UI ge-leert (z. B. nach SnackBar).
String? sortingPersistError;
TourLoaded({
required this.tour,
this.distances,
required this.paymentOptions,
required this.sortingInformation,
this.pendingScanRequests = 0,
this.isPersistingSorting = false,
this.sortingPersistError,
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({
Tour? tour,
Map<String, double>? distances,
List<Payment>? paymentOptions,
Map<String, List<String>>? sortingInformation,
int? pendingScanRequests,
bool? isPersistingSorting,
String? sortingPersistError,
bool clearSortingPersistError = false,
TourDetails? details,
bool? isRefreshing,
bool? isPersistingReorder,
Object? refreshError = _sentinel,
Object? reorderError = _sentinel,
}) {
return TourLoaded(
tour: tour ?? this.tour,
distances: distances ?? this.distances,
paymentOptions: paymentOptions ?? this.paymentOptions,
sortingInformation: sortingInformation ?? this.sortingInformation,
pendingScanRequests: pendingScanRequests ?? this.pendingScanRequests,
isPersistingSorting: isPersistingSorting ?? this.isPersistingSorting,
sortingPersistError: clearSortingPersistError
? null
: (sortingPersistError ?? this.sortingPersistError),
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();
}