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(); }