Final commit.
This commit is contained in:
@ -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
@ -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;
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user