100 lines
3.5 KiB
Dart
100 lines
3.5 KiB
Dart
import 'package:hl_lieferservice/domain/entity/delivery.dart';
|
|
import 'package:hl_lieferservice/domain/entity/delivery_item.dart';
|
|
import 'package:hl_lieferservice/domain/entity/tour_details.dart';
|
|
|
|
/// Ergebnis der Item-Auflösung beim Scan. Sealed, damit das UI per
|
|
/// `switch` exhaustiv pro Fall eine passende Meldung zeigt.
|
|
sealed class ItemMatch {
|
|
const ItemMatch();
|
|
const factory ItemMatch.ok(DeliveryItem item) = ItemMatchOk;
|
|
const factory ItemMatch.notInDelivery() = ItemMatchNotInDelivery;
|
|
const factory ItemMatch.notScannable() = ItemMatchNotScannable;
|
|
const factory ItemMatch.allDone() = ItemMatchAllDone;
|
|
const factory ItemMatch.allRemoved() = ItemMatchAllRemoved;
|
|
const factory ItemMatch.notOpen() = ItemMatchNotOpen;
|
|
}
|
|
|
|
class ItemMatchOk extends ItemMatch {
|
|
const ItemMatchOk(this.item);
|
|
final DeliveryItem item;
|
|
}
|
|
|
|
class ItemMatchNotInDelivery extends ItemMatch {
|
|
const ItemMatchNotInDelivery();
|
|
}
|
|
|
|
class ItemMatchNotScannable extends ItemMatch {
|
|
const ItemMatchNotScannable();
|
|
}
|
|
|
|
class ItemMatchAllDone extends ItemMatch {
|
|
const ItemMatchAllDone();
|
|
}
|
|
|
|
class ItemMatchAllRemoved extends ItemMatch {
|
|
const ItemMatchAllRemoved();
|
|
}
|
|
|
|
class ItemMatchNotOpen extends ItemMatch {
|
|
const ItemMatchNotOpen();
|
|
}
|
|
|
|
/// Findet in der Lieferung das erste **nicht fertig** gescannte Item mit der
|
|
/// gegebenen Article-Nummer. „Top-down"-Strategie: hat eine Lieferung zwei
|
|
/// Belegzeilen mit demselben Artikel (z. B. 20 + 10), wird zuerst die
|
|
/// niedrigere Belegzeile gefüllt. Erst wenn diese fertig ist, „rollt" der
|
|
/// nächste Scan auf die zweite Zeile weiter.
|
|
///
|
|
/// [itemFilter] schränkt die betrachteten Positionen ein — z. B. nur
|
|
/// Filial-Items in der Abhol-Phase. Ohne Filter werden alle Positionen
|
|
/// berücksichtigt (Beladen-Phase).
|
|
///
|
|
/// Klassifiziert den Misserfolgs-Grund (nicht scanbar / bereits fertig /
|
|
/// entfernt …), damit das UI dem Fahrer eine sinnvolle Meldung zeigt.
|
|
ItemMatch matchItem({
|
|
required Delivery delivery,
|
|
required TourDetails details,
|
|
required String articleNumber,
|
|
bool Function(DeliveryItem item)? itemFilter,
|
|
}) {
|
|
final normalized = articleNumber.trim();
|
|
final candidates = delivery.items
|
|
.where((it) => itemFilter?.call(it) ?? true)
|
|
.toList()
|
|
..sort((a, b) => a.belegzeilenNr.compareTo(b.belegzeilenNr));
|
|
final matchingArticle = candidates.where((it) {
|
|
final art = details.articleOf(it.articleId);
|
|
return art?.articleNumber == normalized;
|
|
}).toList();
|
|
|
|
if (matchingArticle.isEmpty) {
|
|
return const ItemMatch.notInDelivery();
|
|
}
|
|
|
|
// Erstes Item, das wir tatsächlich scannen können.
|
|
for (final item in matchingArticle) {
|
|
if (item.isDone || item.isRemoved) continue;
|
|
final article = details.articleOf(item.articleId);
|
|
if (article == null || !article.scannable) continue;
|
|
return ItemMatch.ok(item);
|
|
}
|
|
|
|
// Kein scannbares offenes Item — Grund anhand der Item-Verteilung
|
|
// klassifizieren, damit das UI eine sinnvolle Meldung zeigt.
|
|
final allNotScannable = matchingArticle.every((it) {
|
|
final art = details.articleOf(it.articleId);
|
|
return art == null || !art.scannable;
|
|
});
|
|
if (allNotScannable) return const ItemMatch.notScannable();
|
|
|
|
final allRemoved = matchingArticle.every((it) => it.isRemoved);
|
|
if (allRemoved) return const ItemMatch.allRemoved();
|
|
|
|
final allDone = matchingArticle.every((it) => it.isDone);
|
|
if (allDone) return const ItemMatch.allDone();
|
|
|
|
// Gemischte Konstellation (z. B. eine Zeile entfernt, eine fertig) —
|
|
// praktisch selten; konservativ als „nicht (mehr) offen" melden.
|
|
return const ItemMatch.notOpen();
|
|
}
|