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)
116 lines
4.7 KiB
Dart
116 lines
4.7 KiB
Dart
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";
|
|
}
|