diff --git a/lib/feature/delivery/detail/presentation/delivery_detail_page.dart b/lib/feature/delivery/detail/presentation/delivery_detail_page.dart index e2d5e5d..eedd062 100644 --- a/lib/feature/delivery/detail/presentation/delivery_detail_page.dart +++ b/lib/feature/delivery/detail/presentation/delivery_detail_page.dart @@ -73,60 +73,100 @@ class DeliveryDetail extends StatelessWidget { } } -class _DeliveryDetailScaffold extends StatelessWidget { +class _DeliveryDetailScaffold extends StatefulWidget { const _DeliveryDetailScaffold({required this.deliveryId}); final String deliveryId; @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - return BlocBuilder( - builder: (context, tourState) { - if (tourState is! TourLoaded) { - return const Scaffold( - body: Center(child: CircularProgressIndicator()), - ); - } - final details = tourState.details; - final delivery = _findDelivery(details); - if (delivery == null) { - return Scaffold( - appBar: AppBar(title: const Text('Lieferung')), - body: Center( - child: Text('Lieferung $deliveryId nicht in der Tour gefunden.'), - ), - ); - } - final customer = details.customerOf(delivery); - return Scaffold( - appBar: AppBar( - backgroundColor: theme.primaryColor, - foregroundColor: theme.colorScheme.onPrimary, - title: Text(customer?.name ?? 'Lieferung'), - ), - body: Column( - children: [ - const _StepHeader(), - const Divider(height: 1), - Expanded( - child: _StepBody(delivery: delivery, details: details), - ), - const Divider(height: 1), - _BottomNav(delivery: delivery, details: details), - ], - ), - ); - }, - ); + State<_DeliveryDetailScaffold> createState() => + _DeliveryDetailScaffoldState(); +} + +class _DeliveryDetailScaffoldState extends State<_DeliveryDetailScaffold> { + /// „Gearmt" = die Lieferung war während dieser Page-Session aktiv. Nur dann + /// poppen wir bei `completed` automatisch zurück zur Übersicht. Öffnet der + /// Fahrer eine bereits abgeschlossene Lieferung, bleibt `_armed == false` + /// und die Page bleibt offen (kein ungewolltes Zurückspringen). + bool _armed = false; + bool _popped = false; + + @override + void initState() { + super.initState(); + final s = context.read().state; + if (s is TourLoaded && _findDelivery(s.details)?.state == DeliveryState.active) { + _armed = true; + } } Delivery? _findDelivery(TourDetails details) { for (final d in details.deliveries) { - if (d.id == deliveryId) return d; + if (d.id == widget.deliveryId) return d; } return null; } + + /// Nach erfolgreichem Abschluss (aktiv → completed) zurück zur Übersicht. + void _onTourState(BuildContext context, TourState state) { + if (_popped || state is! TourLoaded) return; + final d = _findDelivery(state.details); + if (d == null) return; + if (d.state == DeliveryState.active) { + _armed = true; + } else if (_armed && d.state == DeliveryState.completed) { + _popped = true; + Navigator.of(context).pop(); + } + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return BlocListener( + listener: _onTourState, + child: BlocBuilder( + builder: (context, tourState) { + if (tourState is! TourLoaded) { + return const Scaffold( + body: Center(child: CircularProgressIndicator()), + ); + } + final details = tourState.details; + final delivery = _findDelivery(details); + if (delivery == null) { + return Scaffold( + appBar: AppBar(title: const Text('Lieferung')), + body: Center( + child: Text( + 'Lieferung ${widget.deliveryId} nicht in der Tour gefunden.', + ), + ), + ); + } + final customer = details.customerOf(delivery); + return Scaffold( + appBar: AppBar( + backgroundColor: theme.primaryColor, + foregroundColor: theme.colorScheme.onPrimary, + title: Text(customer?.name ?? 'Lieferung'), + ), + body: Column( + children: [ + const _StepHeader(), + const Divider(height: 1), + Expanded( + child: _StepBody(delivery: delivery, details: details), + ), + const Divider(height: 1), + _BottomNav(delivery: delivery, details: details), + ], + ), + ); + }, + ), + ); + } } // ─── Step-Header (Pills) ──────────────────────────────────────────────── diff --git a/lib/feature/delivery/detail/presentation/steps/step_articles.dart b/lib/feature/delivery/detail/presentation/steps/step_articles.dart index 843c68a..1eb59e1 100644 --- a/lib/feature/delivery/detail/presentation/steps/step_articles.dart +++ b/lib/feature/delivery/detail/presentation/steps/step_articles.dart @@ -49,6 +49,24 @@ class StepArticles extends StatelessWidget { return (a.komponentenArtikelNr ?? '') .compareTo(b.komponentenArtikelNr ?? ''); }); + + // Komponenten je Oberartikel (Artikelnummer des Parents → Komponenten). + // Grundlage für E: Komponenten sind einzeln NICHT entfernbar — nur der + // Oberartikel kann (ganzes Set) entfernt werden, was auf die Komponenten + // kaskadiert. + final componentsByParentNr = >{}; + for (final it in items) { + final pNr = it.parentArtikelNr; + if (it.isComponent && pNr != null) { + componentsByParentNr.putIfAbsent(pNr, () => []).add(it); + } + } + List componentsOf(DeliveryItem it) { + final nr = details.articleOf(it.articleId)?.articleNumber; + if (nr == null) return const []; + return componentsByParentNr[nr] ?? const []; + } + return ListView( padding: const EdgeInsets.fromLTRB(16, 16, 16, 24), children: [ @@ -73,6 +91,7 @@ class StepArticles extends StatelessWidget { details: details, deliveryId: delivery.id, deliveryActive: delivery.state == DeliveryState.active, + components: componentsOf(items[i]), ), if (i < items.length - 1) const Divider(height: 1, indent: 16, endIndent: 16), @@ -149,6 +168,7 @@ class _ArticleManagementRow extends StatelessWidget { required this.details, required this.deliveryId, required this.deliveryActive, + this.components = const [], }); final DeliveryItem item; @@ -156,6 +176,10 @@ class _ArticleManagementRow extends StatelessWidget { final String deliveryId; final bool deliveryActive; + /// Komponenten dieses Items, falls es ein Set-Oberartikel ist (sonst leer). + /// Entfernen/Wiederherstellen kaskadiert auf diese Komponenten. + final List components; + Future _openCreditDialog( BuildContext context, { required int remaining, @@ -193,6 +217,51 @@ class _ArticleManagementRow extends StatelessWidget { )); } + /// Entfernt das GANZE Set (Oberartikel + alle Komponenten) auf einmal — + /// „entweder Parent komplett oder gar nix". Kein Mengen-Stepper + /// (`maxQuantity: null` ⇒ jede Zeile komplett). + Future _openSetRemoveDialog(BuildContext context) async { + final tourBloc = context.read(); + final actorCarId = _actorCarId(context); + final result = await showReasonPickerSheet( + context: context, + title: 'Grund für das Entfernen (ganzes Set)', + presets: ReasonCatalog.itemRemove, + confirmLabel: 'Set entfernen', + maxQuantity: null, + ); + if (result == null) return; + // Oberartikel + jede Komponente komplett entfernen (quantity null). + tourBloc.add(RemoveItem( + deliveryItemId: item.id, + reason: result.reason, + actorCarId: actorCarId, + quantity: null, + saveReasonAsNote: true, + )); + for (final c in components) { + tourBloc.add(RemoveItem( + deliveryItemId: c.id, + reason: result.reason, + actorCarId: actorCarId, + quantity: null, + // Grund nur einmal (am Oberartikel) als Notiz festhalten. + saveReasonAsNote: false, + )); + } + } + + /// Stellt das ganze Set wieder her (Oberartikel + Komponenten). + void _restoreSet(BuildContext context) { + final tourBloc = context.read(); + final actorCarId = _actorCarId(context); + tourBloc.add(UnremoveItem(deliveryItemId: item.id, actorCarId: actorCarId)); + for (final c in components) { + tourBloc + .add(UnremoveItem(deliveryItemId: c.id, actorCarId: actorCarId)); + } + } + /// Holt die aktuelle Auto-ID aus dem CarSelectBloc — gleiche Konvention /// wie der Loading-Flow: das aktiv gewählte Fahrzeug ist der „Akteur" /// des Audit-Events. Falls (defensiv) noch keine Auswahl getroffen ist, @@ -216,11 +285,27 @@ class _ArticleManagementRow extends StatelessWidget { final fullyRemoved = item.isRemoved; // Status == removed (voll gutgeschr.) final partiallyCredited = credited > 0 && !fullyRemoved; + // E: Komponenten eines Sets sind einzeln NICHT entfernbar; nur der + // Oberartikel kann (ganzes Set) entfernt werden. + final isComponent = item.isComponent; + final hasComponents = components.isNotEmpty; + // Gate: scannbare Position muss `done` sein, sonst keine Gutschrift. final scannable = article?.scannable ?? false; final isDone = item.scanProgress.status == ScanStatus.done; final blockedByScan = scannable && !isDone && !fullyRemoved; - final canCredit = deliveryActive && !blockedByScan && remaining > 0; + // Beim Set zusätzlich: jede scanbare Komponente muss erst verladen sein, + // sonst würde das Backend deren Entfernen ablehnen. + final componentsBlocked = components.any((c) { + final cArticle = details.articleOf(c.articleId); + final cScannable = cArticle?.scannable ?? false; + final cDone = c.scanProgress.status == ScanStatus.done; + return cScannable && !cDone && !c.isRemoved; + }); + final canCredit = deliveryActive && + !blockedByScan && + !componentsBlocked && + remaining > 0; final Color avatarColor; final String avatarText; @@ -285,53 +370,83 @@ class _ArticleManagementRow extends StatelessWidget { text: 'Erst scannen/verladen — dann Gutschrift möglich', color: theme.colorScheme.onSurfaceVariant, ), - ], - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (credited > 0) - IconButton( - // Wiederherstellen nur bei aktiver Lieferung — bei - // abgeschlossener/abgebrochener Lieferung gesperrt (greift auch - // backend-seitig, hier zusätzlich in der UI). - tooltip: deliveryActive - ? 'Gutschrift zurücknehmen' - : 'Nur bei aktiver Lieferung', - icon: Icon( - Icons.restore, - color: deliveryActive - ? theme.colorScheme.primary - : theme.colorScheme.onSurfaceVariant, - ), - onPressed: deliveryActive ? () => _restoreAll(context) : null, + // E: Komponenten-Hinweis — einzeln nicht entfernbar. + if (isComponent && deliveryActive && !fullyRemoved) + _StatusLine( + text: 'Nur über den Oberartikel entfernbar', + color: theme.colorScheme.onSurfaceVariant, ), - if (!fullyRemoved) - IconButton.outlined( - tooltip: blockedByScan - ? 'Erst scannen/verladen' - : (!deliveryActive - ? 'Nur bei aktiver Lieferung' - : 'Gutschrift / entfernen'), - style: ButtonStyle( - backgroundColor: WidgetStatePropertyAll( - canCredit - ? Colors.redAccent - : theme.colorScheme.surfaceContainerHighest, - ), - ), - onPressed: canCredit - ? () => _openCreditDialog(context, remaining: remaining) - : null, - icon: Icon( - Icons.delete, - color: canCredit - ? theme.colorScheme.onPrimary - : theme.colorScheme.onSurfaceVariant, - ), + // Set-Oberartikel: blockiert, solange eine Komponente noch nicht + // verladen ist. + if (hasComponents && + componentsBlocked && + deliveryActive && + !fullyRemoved) + _StatusLine( + text: 'Erst alle Set-Teile verladen — dann ganzes Set entfernbar', + color: theme.colorScheme.onSurfaceVariant, ), ], ), + // E: Komponenten haben KEINE eigenen Aktionen — Entfernen/Wiederherstellen + // läuft ausschließlich über den Oberartikel (kaskadiert auf das Set). + trailing: isComponent + ? null + : Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (credited > 0) + IconButton( + // Wiederherstellen nur bei aktiver Lieferung — bei + // abgeschlossener/abgebrochener Lieferung gesperrt (greift + // auch backend-seitig, hier zusätzlich in der UI). + tooltip: deliveryActive + ? (hasComponents + ? 'Set wiederherstellen' + : 'Gutschrift zurücknehmen') + : 'Nur bei aktiver Lieferung', + icon: Icon( + Icons.restore, + color: deliveryActive + ? theme.colorScheme.primary + : theme.colorScheme.onSurfaceVariant, + ), + onPressed: deliveryActive + ? () => hasComponents + ? _restoreSet(context) + : _restoreAll(context) + : null, + ), + if (!fullyRemoved) + IconButton.outlined( + tooltip: (blockedByScan || componentsBlocked) + ? 'Erst scannen/verladen' + : (!deliveryActive + ? 'Nur bei aktiver Lieferung' + : (hasComponents + ? 'Ganzes Set entfernen' + : 'Gutschrift / entfernen')), + style: ButtonStyle( + backgroundColor: WidgetStatePropertyAll( + canCredit + ? Colors.redAccent + : theme.colorScheme.surfaceContainerHighest, + ), + ), + onPressed: canCredit + ? () => hasComponents + ? _openSetRemoveDialog(context) + : _openCreditDialog(context, remaining: remaining) + : null, + icon: Icon( + Icons.delete, + color: canCredit + ? theme.colorScheme.onPrimary + : theme.colorScheme.onSurfaceVariant, + ), + ), + ], + ), ); } } diff --git a/lib/feature/delivery/detail/presentation/steps/step_info.dart b/lib/feature/delivery/detail/presentation/steps/step_info.dart index d396ddb..c574dbf 100644 --- a/lib/feature/delivery/detail/presentation/steps/step_info.dart +++ b/lib/feature/delivery/detail/presentation/steps/step_info.dart @@ -9,6 +9,9 @@ import 'package:hl_lieferservice/domain/entity/delivery_item.dart'; import 'package:hl_lieferservice/domain/entity/tour_details.dart'; import 'package:hl_lieferservice/feature/delivery/bloc/tour_bloc.dart'; import 'package:hl_lieferservice/feature/delivery/bloc/tour_event.dart'; +import 'package:hl_lieferservice/feature/delivery/detail/bloc/workflow_bloc.dart'; +import 'package:hl_lieferservice/feature/delivery/detail/bloc/workflow_event.dart'; +import 'package:hl_lieferservice/feature/delivery/detail/bloc/workflow_state.dart'; /// Step 1 — Informationen zur Lieferung. /// @@ -716,16 +719,77 @@ class _ArticleList extends StatelessWidget { ); } - return Card( - margin: EdgeInsets.zero, - child: Column( - children: [ - for (int i = 0; i < items.length; i++) ...[ - _ArticleRow(item: items[i], details: details), - if (i < items.length - 1) - const Divider(height: 1, indent: 16, endIndent: 16), - ], + // Hat sich bei mindestens einem Artikel die tatsächliche Menge geändert + // (entfernt oder teilweise gutgeschrieben)? Dann ein Banner mit Verweis + // auf Step 3 „Artikel", wo die Änderungen verwaltet werden. + final anyChanged = items.any(_quantityChanged); + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (anyChanged) ...[ + const _QuantityChangedBanner(), + const SizedBox(height: 8), ], + Card( + margin: EdgeInsets.zero, + child: Column( + children: [ + for (int i = 0; i < items.length; i++) ...[ + _ArticleRow(item: items[i], details: details), + if (i < items.length - 1) + const Divider(height: 1, indent: 16, endIndent: 16), + ], + ], + ), + ), + ], + ); + } +} + +/// `true`, wenn die tatsächlich auszuliefernde Menge von der ursprünglich +/// bestellten abweicht — also die Position ganz entfernt oder teilweise +/// gutgeschrieben wurde. +bool _quantityChanged(DeliveryItem item) => + item.isRemoved || item.scanProgress.creditedQuantity > 0; + +/// Tappbares Banner über der Artikelliste: weist darauf hin, dass sich die +/// Menge mindestens eines Artikels geändert hat, und springt zu Step 3 +/// („Artikel"), wo die Änderungen sichtbar/verwaltbar sind. +class _QuantityChangedBanner extends StatelessWidget { + const _QuantityChangedBanner(); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final amber = Colors.amber.shade800; + return InkWell( + borderRadius: BorderRadius.circular(8), + onTap: () => context + .read() + .add(const WorkflowGoToStep(WorkflowStep.articles)), + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: amber.withValues(alpha: 0.10), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: amber.withValues(alpha: 0.4)), + ), + child: Row( + children: [ + Icon(Icons.edit_note, color: amber), + const SizedBox(width: 10), + Expanded( + child: Text( + 'Bei mindestens einem Artikel hat sich die Menge geändert. ' + 'Details unter Schritt 3 „Artikel".', + style: theme.textTheme.bodySmall?.copyWith(color: amber), + ), + ), + Icon(Icons.chevron_right, color: amber, size: 20), + ], + ), ), ); } @@ -744,6 +808,8 @@ class _ArticleRow extends StatelessWidget { final warehouse = details.warehouseOf(item.warehouseId); final isScannable = article?.scannable ?? false; final removed = item.isRemoved; + // Menge tatsächlich verändert (entfernt oder teilweise gutgeschrieben)? + final changed = _quantityChanged(item); return ListTile( // Komponenten um eine Stufe eingerückt (gehören zum Oberartikel darüber). @@ -756,6 +822,8 @@ class _ArticleRow extends StatelessWidget { foregroundColor: removed ? theme.colorScheme.onSurfaceVariant : theme.colorScheme.onPrimary, + // Bewusst die URSPRÜNGLICH bestellte Menge (requiredQuantity), nicht + // die reduzierte — Änderungen werden separat als Hinweis ausgewiesen. child: Text( '${item.requiredQuantity}×', style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold), @@ -769,19 +837,47 @@ class _ArticleRow extends StatelessWidget { color: removed ? theme.colorScheme.onSurfaceVariant : null, ), ), - subtitle: Text( - [ - article?.articleNumber ?? item.articleId, - if (warehouse != null) warehouse.name, - if (!isScannable) 'Dienstleistung', - if (item.unitPrice > 0) - '${item.unitPrice.toStringAsFixed(2)} € / Stück', - ].join(' · '), - style: TextStyle( - fontSize: 12, - color: theme.colorScheme.onSurfaceVariant, - decoration: removed ? TextDecoration.lineThrough : null, - ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + [ + article?.articleNumber ?? item.articleId, + if (warehouse != null) warehouse.name, + if (!isScannable) 'Dienstleistung', + if (item.unitPrice > 0) + '${item.unitPrice.toStringAsFixed(2)} € / Stück', + ].join(' · '), + style: TextStyle( + fontSize: 12, + color: theme.colorScheme.onSurfaceVariant, + decoration: removed ? TextDecoration.lineThrough : null, + ), + ), + if (changed) + Padding( + padding: const EdgeInsets.only(top: 2), + child: Row( + children: [ + Icon(Icons.edit_note, size: 14, color: Colors.amber.shade800), + const SizedBox(width: 4), + Flexible( + child: Text( + removed + ? 'Menge geändert: entfernt (Ursprung ${item.requiredQuantity}×)' + : 'Menge geändert: jetzt ${item.deliveredQuantity}× ' + '(Ursprung ${item.requiredQuantity}×)', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: Colors.amber.shade800, + ), + ), + ), + ], + ), + ), + ], ), trailing: isScannable ? Text( diff --git a/lib/feature/delivery/detail/presentation/steps/step_summary.dart b/lib/feature/delivery/detail/presentation/steps/step_summary.dart index cf60e12..6d5bbaa 100644 --- a/lib/feature/delivery/detail/presentation/steps/step_summary.dart +++ b/lib/feature/delivery/detail/presentation/steps/step_summary.dart @@ -471,8 +471,8 @@ class _PaymentMethodPicker extends StatelessWidget { ] else if (!hasOpenAmount) ...[ const SizedBox(height: 8), const _PickerHint( - text: 'Kein offener Betrag — Auswahl der Zahlungsweise ' - 'nicht erforderlich.', + text: 'Keine Zahlung mehr offen (bereits bezahlt) — ' + 'Auswahl der Zahlungsweise nicht erforderlich.', ), ], ], diff --git a/lib/feature/loading/presentation/loading_customer_page.dart b/lib/feature/loading/presentation/loading_customer_page.dart index 9a4c693..37727a3 100644 --- a/lib/feature/loading/presentation/loading_customer_page.dart +++ b/lib/feature/loading/presentation/loading_customer_page.dart @@ -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 = >{}; + final injectedParentIds = {}; + for (final group in groups) { + final groupParentNrs = group.items + .where((it) => it.isComponent) + .map((it) => it.parentArtikelNr) + .whereType() + .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