Phasenbasierte Lieferübersicht + Beladen-Flow, plus Migrationsplan für Rust-Backend
UI-Restructuring: - TabBar in scan_page durch dedizierte Phasen ersetzt: Sortieren / Beladen / Ausliefern - PhaseBloc + PhaseService leiten Phase aus Tour-/Item-States ab - DeliverySelectionPage (ab 2 Autos) und DeliverySortPage als eigene Flows - LoadingOverviewPage / LoadingCustomerPage für die Beladephase - PhaseStepper-Widget im Home für Phasen-Anzeige - Lager-Differenzierung (Standardlager 0 vs. Außenlager) via WarehouseBadge Process-Stubs: - ProcessRepository für Hold/Cancel/Sort/Assign-Flows (stub, bereit für Backend-Anbindung) Doku: - docs/BACKEND_MIGRATION.md: Phasenplan für Umstellung auf das neue Rust-Backend (OpenAPI-Generator, Keycloak OIDC, Clean-Arch-Layering)
This commit is contained in:
115
lib/feature/loading/model/loading_group.dart
Normal file
115
lib/feature/loading/model/loading_group.dart
Normal file
@ -0,0 +1,115 @@
|
||||
import 'package:hl_lieferservice/model/article.dart';
|
||||
import 'package:hl_lieferservice/model/delivery.dart';
|
||||
|
||||
/// Aggregiert eine [Delivery] mit ihren scannbaren Artikeln zu einer
|
||||
/// Belade-Einheit. Bildet die Datenbasis für die Beladen-Phase (Vollbild-
|
||||
/// Kunde + Übersicht).
|
||||
///
|
||||
/// **Wichtige Geschäftslogik:** Für die Frage "ist diese Lieferung fertig
|
||||
/// beladen?" zählen nur Artikel aus dem **Standardlager** (warehouseNr
|
||||
/// `null` oder `"0"`). Außenlager-Artikel werden separat beim
|
||||
/// Kundenbesuch in der Ausliefer-Phase abgeholt — sie blockieren also
|
||||
/// nicht den Beladen-Abschluss. Konsequenz: alle Counter-Getter
|
||||
/// ([totalArticles], [completeArticles], [scannedUnits], [totalUnits],
|
||||
/// [isComplete], [isPartial], [hasAnyScanned]) ignorieren Außenlager-
|
||||
/// Artikel. Wer alle Artikel braucht, greift direkt auf [articles] zu.
|
||||
///
|
||||
/// Aufrufer füllen [articles] mit den scannbaren Artikeln dieser Lieferung
|
||||
/// (also nicht alle Artikel der Lieferung — vorgefiltert über
|
||||
/// `Article.scannable`).
|
||||
class LoadingGroup {
|
||||
/// Die zugrundeliegende Lieferung (inkl. Kunde, Adresse, State).
|
||||
final Delivery delivery;
|
||||
|
||||
/// Nummernschild des zugewiesenen Fahrzeugs zur Darstellung im Badge.
|
||||
/// `null` wenn die Lieferung noch keinem Auto zugeordnet ist.
|
||||
final String? carPlate;
|
||||
|
||||
/// Die scannbaren Artikel der Lieferung (bereits vorgefiltert).
|
||||
final List<Article> articles;
|
||||
|
||||
const LoadingGroup({
|
||||
required this.delivery,
|
||||
required this.articles,
|
||||
this.carPlate,
|
||||
});
|
||||
|
||||
/// Alle Standardlager-Artikel (Lager-Nummer `null` oder `"0"`). Bildet
|
||||
/// die Basis aller Beladen-Counter, weil Außenlager-Ware nicht in der
|
||||
/// Belade-Halle scannbar ist.
|
||||
List<Article> get _standardArticles => articles
|
||||
.where((a) => !_isExternalWarehouse(a.warehouseNr))
|
||||
.toList(growable: false);
|
||||
|
||||
/// Anzahl der scannbaren Standardlager-Artikel. Parent-Artikel zählen
|
||||
/// als 1 (nicht je Komponente).
|
||||
int get totalArticles => _standardArticles.length;
|
||||
|
||||
/// Anzahl der vollständig gescannten Standardlager-Artikel. Bei Parent-
|
||||
/// Artikeln gilt "vollständig" = alle Komponenten vollständig.
|
||||
int get completeArticles =>
|
||||
_standardArticles.where((a) => a.isFullyScanned).length;
|
||||
|
||||
/// Gesamtanzahl der erwarteten Einzelstücke aus dem Standardlager —
|
||||
/// bei Parent-Artikeln summiert über die Required-Amounts der
|
||||
/// Komponenten.
|
||||
int get totalUnits => _standardArticles.fold(0, (sum, a) {
|
||||
if (a.isParent && a.components.isNotEmpty) {
|
||||
return sum + a.components.fold(0, (s, c) => s + c.requiredAmount);
|
||||
}
|
||||
return sum + a.amount;
|
||||
});
|
||||
|
||||
/// Bereits gescannte Einzelstücke aus dem Standardlager — analog zu
|
||||
/// [totalUnits].
|
||||
int get scannedUnits => _standardArticles.fold(0, (sum, a) {
|
||||
if (a.isParent && a.components.isNotEmpty) {
|
||||
return sum + a.components.fold(0, (s, c) => s + c.scannedAmount);
|
||||
}
|
||||
return sum + a.scannedAmount + a.scannedRemovedAmount;
|
||||
});
|
||||
|
||||
/// `true`, wenn alle Standardlager-Artikel vollständig gescannt sind.
|
||||
///
|
||||
/// Edge-Case: Lieferung **ohne** Standardlager-Artikel (alle Artikel
|
||||
/// liegen in Außenlagern) → automatisch fertig, weil in der Beladen-
|
||||
/// Phase nichts zu tun ist.
|
||||
bool get isComplete {
|
||||
if (articles.isEmpty) return false;
|
||||
if (_standardArticles.isEmpty) return true;
|
||||
return completeArticles == totalArticles;
|
||||
}
|
||||
|
||||
/// `true`, wenn mindestens ein Stück gescannt wurde — egal ob Artikel
|
||||
/// vollständig oder nicht.
|
||||
bool get hasAnyScanned => scannedUnits > 0;
|
||||
|
||||
/// `true`, wenn die Lieferung angefangen, aber nicht abgeschlossen wurde.
|
||||
bool get isPartial => hasAnyScanned && !isComplete;
|
||||
|
||||
/// `true`, wenn mindestens ein Artikel der Lieferung NICHT aus dem
|
||||
/// Standard-Lager kommt. Standard-Lager hat die Nummer "0"; ein
|
||||
/// `warehouseNr == null` interpretieren wir als "nicht angegeben" und
|
||||
/// damit als Standard (kein False-Positive auf Datenlücken).
|
||||
bool get hasExternalWarehouseArticles =>
|
||||
articles.any((a) => _isExternalWarehouse(a.warehouseNr));
|
||||
|
||||
/// Eindeutige Liste der Außenlager-Namen, die in dieser Lieferung
|
||||
/// vorkommen — für Badges/Hinweise in der Übersicht. Wenn ein Artikel
|
||||
/// nur eine `warehouseNr` aber keinen Namen hat, wird die Nummer als
|
||||
/// Fallback genommen.
|
||||
List<String> get externalWarehouseLabels {
|
||||
final labels = <String>{};
|
||||
for (final a in articles) {
|
||||
if (!_isExternalWarehouse(a.warehouseNr)) continue;
|
||||
final label = (a.warehouseName?.isNotEmpty ?? false)
|
||||
? a.warehouseName!
|
||||
: "Lager ${a.warehouseNr}";
|
||||
labels.add(label);
|
||||
}
|
||||
return labels.toList(growable: false);
|
||||
}
|
||||
|
||||
static bool _isExternalWarehouse(String? nr) =>
|
||||
nr != null && nr.isNotEmpty && nr != "0";
|
||||
}
|
||||
Reference in New Issue
Block a user