import 'package:flutter/material.dart'; import 'package:hl_lieferservice/feature/loading/widget/article_row.dart'; import 'package:hl_lieferservice/model/article.dart'; import 'package:hl_lieferservice/model/component.dart'; /// Eine einzelne, im Hold-Dialog auswählbare Position. Aufrufer erhalten /// nach Bestätigung die ausgewählten Items zurück. /// /// Genau eines von [article] / [component] ist gesetzt — beide kombiniert /// ergeben einen Komponenten-Eintrag (component != null mit zugehörigem /// Parent-Artikel in [article]). class HoldSelectionItem { HoldSelectionItem.article(this.article) : component = null, key = HoldKey.article(article); HoldSelectionItem.component(this.article, Component this.component) : key = HoldKey.component(article, component); /// Artikel — bei Komponenten der zugehörige Parent. final Article article; /// Komponente (nur gesetzt, wenn es sich um eine Stücklisten-Position /// handelt). final Component? component; /// Eindeutiger Schlüssel zur Hold-State-Verwaltung. Identisch mit den /// Keys, die [HoldKey] erzeugt — so kann ein Aufrufer ohne Umweg den /// internen Hold-Set füllen. final String key; String get _displayName => component?.name ?? article.name; String get _articleNumber => component?.articleNumber ?? article.articleNumber; } /// Auswahl-Dialog für den Teilabbruch ("Artikel heute nicht liefern"). /// /// Liefert nach Bestätigung per `Navigator.pop` die Liste der ausgewählten /// [HoldSelectionItem]s. Bei Abbruch ist das Ergebnis `null`. Items, die /// im Set [alreadyHeld] enthalten sind, werden ausgegraut dargestellt und /// sind nicht erneut wählbar. class HoldSelectionDialog extends StatefulWidget { const HoldSelectionDialog({ super.key, required this.customerName, required this.articles, required this.alreadyHeld, }); /// Anzeigename des Kunden — wird im Dialog-Header gezeigt. final String customerName; /// Scannbare Artikel der Lieferung (also bereits vorgefiltert). final List
articles; /// Set bereits gehaltener Keys — diese erscheinen ausgegraut & disabled. final Set alreadyHeld; static Future?> show( BuildContext context, { required String customerName, required List
articles, required Set alreadyHeld, }) { return showDialog>( context: context, builder: (_) => HoldSelectionDialog( customerName: customerName, articles: articles, alreadyHeld: alreadyHeld, ), ); } @override State createState() => _HoldSelectionDialogState(); } class _HoldSelectionDialogState extends State { final Set _selectedKeys = {}; late final List _items; @override void initState() { super.initState(); _items = _buildItems(widget.articles); } /// Erzeugt aus den Artikeln die selektierbaren Einträge. Parent-Artikel /// werden nicht selbst zum Eintrag — ihre Komponenten sind die wählbaren /// Einheiten. Für die Anzeige der Header-Zeile werden Parents über das /// Build-Verfahren (siehe build) separat eingestreut. List _buildItems(List
articles) { final result = []; for (final a in articles) { if (a.isParent && a.components.isNotEmpty) { for (final c in a.components) { result.add(HoldSelectionItem.component(a, c)); } } else { result.add(HoldSelectionItem.article(a)); } } return result; } void _toggle(String key) { setState(() { if (_selectedKeys.contains(key)) { _selectedKeys.remove(key); } else { _selectedKeys.add(key); } }); } void _confirm() { final selected = _items.where((i) => _selectedKeys.contains(i.key)).toList(); Navigator.of(context).pop(selected); } @override Widget build(BuildContext context) { final theme = Theme.of(context); return AlertDialog( title: const Text("Artikel zurückhalten"), content: SizedBox( width: double.maxFinite, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( widget.customerName, style: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 8), const Text( "Markiere die Positionen, die heute nicht ausgeliefert werden:", style: TextStyle(fontSize: 13), ), const SizedBox(height: 8), Flexible( child: ListView( shrinkWrap: true, children: _buildList(theme), ), ), ], ), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text("Abbrechen"), ), FilledButton( onPressed: _selectedKeys.isEmpty ? null : _confirm, child: const Text("Weiter"), ), ], ); } /// Baut die ListView-Inhalte mit Header-Zeilen für Parent-Artikel. /// Parent-Header sind bewusst nicht klickbar — sie dienen nur zur /// Strukturierung. List _buildList(ThemeData theme) { final widgets = []; for (final a in widget.articles) { if (a.isParent && a.components.isNotEmpty) { widgets.add( Padding( padding: const EdgeInsets.only(top: 8, bottom: 2, left: 4), child: Row( children: [ Icon(Icons.account_tree_outlined, size: 16, color: theme.colorScheme.primary), const SizedBox(width: 6), Expanded( child: Text( a.name, style: const TextStyle( fontWeight: FontWeight.w600, fontSize: 13, ), ), ), ], ), ), ); for (final c in a.components) { final item = _items.firstWhere( (i) => i.component == c && i.article == a, ); widgets.add(_buildTile(item, indent: true)); } } else { final item = _items.firstWhere( (i) => i.article == a && i.component == null, ); widgets.add(_buildTile(item)); } } return widgets; } Widget _buildTile(HoldSelectionItem item, {bool indent = false}) { final alreadyHeld = widget.alreadyHeld.contains(item.key); final selected = _selectedKeys.contains(item.key); return Padding( padding: EdgeInsets.only(left: indent ? 16 : 0), child: Opacity( opacity: alreadyHeld ? 0.4 : 1.0, child: CheckboxListTile( contentPadding: EdgeInsets.zero, dense: true, value: alreadyHeld ? true : selected, onChanged: alreadyHeld ? null : (_) => _toggle(item.key), title: Text( item._displayName, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500), ), subtitle: Text( "Artikelnr. ${item._articleNumber}" "${alreadyHeld ? " · bereits zurückgehalten" : ""}", style: const TextStyle(fontSize: 11), ), ), ), ); } }