Files
Holzleitner-Lieferservice-App/lib/feature/loading/model/loading_group.dart
Dennis Nemec 456fb59668 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)
2026-05-14 22:27:56 +02:00

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";
}