feat(delivery): Abschluss-Navigation, Mengen-Hinweis, Set-Handling

A) Nach erfolgreichem Abschluss (aktiv→completed) poppt die Detail-Page
   automatisch zurück zur Übersicht. Scaffold ist jetzt StatefulWidget
   mit BlocListener<TourBloc>; nur „gearmt", wenn die Lieferung beim
   Öffnen aktiv war → erneutes Öffnen einer fertigen Lieferung poppt nicht.

B) Step „Info": Artikelliste zeigt weiter die Ursprungsmenge
   (requiredQuantity). Bei entfernten/teilweise gutgeschriebenen Positionen
   erscheint pro Zeile ein „Menge geändert"-Hinweis + ein tappbares Banner,
   das zu Step 3 „Artikel" springt.

C) Beladen: nicht-scanbare Set-Köpfe (Parent-Komponenten) werden jetzt
   IMMER mit ihrem Set gezeigt — als Kopf in der Lagergruppe ihrer
   Komponenten statt isoliert unter „Dienstleistungen". _ItemRow leitet
   scanNotRequired aus der Artikel-Scanbarkeit ab.

D) Step „Übersicht": Wording der Zahlungsweise-Sperre bei offen==0
   präzisiert („Keine Zahlung mehr offen (bereits bezahlt)").

E) Step „Artikel": Komponenten eines Sets sind einzeln nicht mehr
   entfernbar (kein Button + Hinweis). Das Entfernen/Wiederherstellen läuft
   nur über den Oberartikel und kaskadiert auf das ganze Set (ganz oder
   gar nix). Set-Entfernen ist blockiert, solange eine Komponente noch
   nicht verladen ist.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dennis Nemec
2026-06-23 15:57:52 +02:00
parent 6d2f496700
commit 4c6bef6897
5 changed files with 413 additions and 126 deletions

View File

@ -535,6 +535,39 @@ class _CustomerBody extends StatelessWidget {
// erkennt. Liegen außerhalb von `groups` (die nur scanbare Items führen).
final serviceItems = details.nonScannableItems(delivery).toList();
// ── Set-Köpfe (Parent-Komponenten) immer mit ihrem Set anzeigen ──────
// Ein nicht-scanbarer Set-Kopf (Stücklisten-Oberartikel, z. B. als
// Pauschale geführt) soll NICHT isoliert unter „Dienstleistungen"
// erscheinen, sondern als Kopf über seinen (scanbaren) Komponenten in der
// jeweiligen Lagergruppe. Ein Item ist Set-Kopf, wenn seine Artikelnummer
// von einer Komponente als parentArtikelNr referenziert wird.
String? artNrOf(DeliveryItem it) =>
details.articleOf(it.articleId)?.articleNumber;
// Set-Köpfe je Lagergruppe (warehouseId → einzuhängende Köpfe) +
// gesammelte IDs, um sie aus der Dienstleistungs-Sektion zu entfernen.
final injectedParentsByWarehouseId = <String, List<DeliveryItem>>{};
final injectedParentIds = <String>{};
for (final group in groups) {
final groupParentNrs = group.items
.where((it) => it.isComponent)
.map((it) => it.parentArtikelNr)
.whereType<String>()
.toSet();
if (groupParentNrs.isEmpty) continue;
final parents = serviceItems
.where((s) => groupParentNrs.contains(artNrOf(s)))
.toList();
if (parents.isEmpty) continue;
injectedParentsByWarehouseId[group.warehouse.id] = parents;
injectedParentIds.addAll(parents.map((p) => p.id));
}
// Set-Köpfe, die in eine Gruppe eingehängt wurden, nicht doppelt unter
// „Dienstleistungen" zeigen. Set-Köpfe ohne scanbare Komponenten (kommen
// hier nicht vor) blieben weiterhin in der Liste.
final serviceItemsView = serviceItems
.where((s) => !injectedParentIds.contains(s.id))
.toList();
return Column(
children: [
// Header bekommt einen eigenen, leicht abgehobenen Hintergrund
@ -627,26 +660,30 @@ class _CustomerBody extends StatelessWidget {
warehouse: group.warehouse,
items: group.items,
),
for (final item in _parentFirst(group.items))
// Nicht-scanbare Set-Köpfe dieser Gruppe vor ihre Komponenten
// einhängen; `_parentFirst` ordnet Kopf vor Komponenten.
for (final item in _parentFirst([
...?injectedParentsByWarehouseId[group.warehouse.id],
...group.items,
]))
_ItemRow(
item: item,
details: details,
onAction: (action) => onItemAction(item, action),
),
],
// Gebuchte Dienstleistungen (nicht-scanbare Positionen): eigene
// Zwischenüberschrift „Dienstleistungen" (optisch wie
// Standardlager) und dieselbe Item-Card wie scanbare Artikel —
// einziger Unterschied ist der Hinweis, dass kein Scanvorgang
// nötig ist (`scanNotRequired`).
if (serviceItems.isNotEmpty) ...[
// Gebuchte Dienstleistungen (nicht-scanbare Positionen ohne
// eigene Set-Komponenten): eigene Zwischenüberschrift
// „Dienstleistungen" (optisch wie Standardlager) und dieselbe
// Item-Card wie scanbare Artikel — einziger Unterschied ist der
// Hinweis, dass kein Scanvorgang nötig ist.
if (serviceItemsView.isNotEmpty) ...[
const _ServiceSectionHeader(),
for (final item in _parentFirst(serviceItems))
for (final item in _parentFirst(serviceItemsView))
_ItemRow(
item: item,
details: details,
onAction: (action) => onItemAction(item, action),
scanNotRequired: true,
),
],
],
@ -1111,24 +1148,23 @@ class _ItemRow extends StatelessWidget {
required this.item,
required this.details,
required this.onAction,
this.scanNotRequired = false,
});
final DeliveryItem item;
final TourDetails details;
final void Function(_ItemAction action) onAction;
/// `true` für gebuchte Dienstleistungen (nicht-scanbare Positionen): die
/// Card wird GENAU SO wie ein Standardlager-Artikel gerendert, bekommt aber
/// unten den Hinweis „Kein Scanvorgang notwendig". Der Manuell-Button
/// entfällt bei nicht-scanbaren Positionen ohnehin (`canManualConfirm`).
final bool scanNotRequired;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final article = details.articleOf(item.articleId);
final warehouse = details.warehouseOf(item.warehouseId);
// `true` für nicht-scanbare Positionen (Dienstleistungen / Pauschalen /
// Set-Köpfe): die Card wird GENAU SO wie ein scanbarer Artikel gerendert,
// bekommt aber unten den Hinweis „Kein Scanvorgang notwendig" und keinen
// Mengen-Zähler. Aus dem Artikel abgeleitet, damit ein in eine Lagergruppe
// eingehängter nicht-scanbarer Set-Kopf automatisch korrekt rendert.
final scanNotRequired = !(article?.scannable ?? false);
final isExternalWarehouse = warehouse != null && !warehouse.isStandard;
// Manueller Fallback-Button: nur für scanbare, noch offene Positionen
// (nicht done/entfernt/pausiert) — analog dazu, was ein Barcode-Scan