import 'package:flutter/material.dart'; import 'package:hl_lieferservice/model/article.dart'; import 'package:hl_lieferservice/model/component.dart'; /// Identifier-Helpers für den Hold-State-Set: ein Artikel ohne Komponenten /// wird mit seiner [Article.internalId] referenziert, eine Komponente mit /// `:`. /// /// Wir nutzen bewusst ein einfaches String-Schema statt einer eigenen Klasse, /// weil der Set-Lookup in jedem Row-Rebuild stattfindet und Sets von /// einfachen Strings am preisgünstigsten sind. class HoldKey { /// Schlüssel für einen ganzen (Nicht-Parent-)Artikel. static String article(Article a) => "art:${a.internalId}"; /// Schlüssel für eine Komponente (Stücklisten-Position) unterhalb eines /// Parent-Artikels. static String component(Article parent, Component c) => "comp:${parent.internalId}:${c.articleNumber}"; } /// Visuelle Konstanten für die "Heute zurückgehalten"-Markierung. const _holdBadgeColor = Colors.deepOrange; /// Renderer für eine Artikelzeile innerhalb der Beladen-Phase. /// /// Unterscheidet automatisch zwischen Parent-Artikel (Stückliste) und /// regulärem Artikel — die Komponenten werden in einem [ParentArticleRow] /// inkl. Liste von [ComponentRow] aufgeklappt dargestellt. Außerhalb dieser /// Klasse sollte nur [ArticleRow] direkt verwendet werden; die anderen /// beiden Widgets sind als Subkomponenten exportiert, falls jemand sie /// gezielt ansteuern möchte. class ArticleRow extends StatelessWidget { const ArticleRow({ super.key, required this.article, required this.isHeld, required this.disabled, this.heldComponents = const {}, this.onTap, this.onLongPress, }); /// Der darzustellende Artikel. final Article article; /// `true`, wenn der Artikel als Ganzes für heute zurückgehalten ist. /// Bei Parent-Artikeln wird dies an die Komponenten weitergereicht. final bool isHeld; /// `true`, wenn die Lieferung selbst (z. B. wegen Abbruch) deaktiviert /// ist — die Zeile wird grundsätzlich ausgegraut, Tap deaktiviert. final bool disabled; /// Set der gehaltenen Komponenten-Schlüssel (siehe [HoldKey.component]). /// Wird nur ausgewertet, wenn der Artikel ein Parent ist. final Set heldComponents; /// Optional: Tap-Callback, z. B. um den Artikel "manuell" zu inkrementieren. /// Bleibt für die Beladen-Phase aktuell `null` — der Scan-Flow geht über /// den Scanner, nicht den Tap. Lässt aber Raum für spätere Komfort-Aktionen. final VoidCallback? onTap; /// Optional: Long-Press, z. B. für ein Kontext-Menü (Unscan). final VoidCallback? onLongPress; @override Widget build(BuildContext context) { if (article.isParent && article.components.isNotEmpty) { return ParentArticleRow( article: article, parentHeld: isHeld, disabled: disabled, heldComponents: heldComponents, ); } return _RegularArticleRow( article: article, isHeld: isHeld, disabled: disabled, onTap: onTap, onLongPress: onLongPress, ); } } /// Reguläre Artikel-Zeile (ohne Stückliste) als Card. class _RegularArticleRow extends StatelessWidget { const _RegularArticleRow({ required this.article, required this.isHeld, required this.disabled, this.onTap, this.onLongPress, }); final Article article; final bool isHeld; final bool disabled; final VoidCallback? onTap; final VoidCallback? onLongPress; @override Widget build(BuildContext context) { final entryDone = article.isFullyScanned; final theme = Theme.of(context); final scheme = theme.colorScheme; final effectiveDisabled = disabled || isHeld; // Card-Styling abhängig vom Status: gescannt = grünlicher Akzent, // zurückgehalten = orange-Akzent, sonst neutral. So sieht der Fahrer // beim Scrollen ohne Lesen, was schon erledigt ist. final Color cardColor; final Color borderColor; final IconData leadingIcon; final Color leadingColor; if (isHeld) { cardColor = _holdBadgeColor.withValues(alpha: 0.07); borderColor = _holdBadgeColor.withValues(alpha: 0.45); leadingIcon = Icons.pause_circle_outline; leadingColor = _holdBadgeColor; } else if (entryDone) { cardColor = Colors.green.withValues(alpha: 0.07); borderColor = Colors.green.withValues(alpha: 0.45); leadingIcon = Icons.check_circle; leadingColor = Colors.green.shade700; } else { cardColor = scheme.surfaceContainerLow; borderColor = scheme.outlineVariant.withValues(alpha: 0.4); leadingIcon = Icons.inventory_2_outlined; leadingColor = scheme.onSurfaceVariant; } return Opacity( opacity: effectiveDisabled ? 0.45 : 1.0, child: Card( margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), elevation: 0, color: cardColor, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), side: BorderSide(color: borderColor), ), child: InkWell( onTap: effectiveDisabled ? null : onTap, onLongPress: effectiveDisabled ? null : onLongPress, borderRadius: BorderRadius.circular(12), child: Padding( padding: const EdgeInsets.fromLTRB(12, 10, 12, 10), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Container( width: 36, height: 36, decoration: BoxDecoration( color: leadingColor.withValues(alpha: 0.15), shape: BoxShape.circle, ), child: Icon(leadingIcon, color: leadingColor, size: 20), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( article.name, style: TextStyle( fontSize: 15, fontWeight: FontWeight.w600, decoration: isHeld ? TextDecoration.lineThrough : TextDecoration.none, ), ), const SizedBox(height: 2), Text( "Artikelnr. ${article.articleNumber}", style: TextStyle( fontSize: 12, color: scheme.onSurfaceVariant, ), ), ], ), ), const SizedBox(width: 8), _ScanCountBadge( done: article.scannedAmount + article.scannedRemovedAmount, total: article.amount, isComplete: entryDone, ), ], ), if (isHeld) ...[ const SizedBox(height: 8), const _HeldBadge(), ], ], ), ), ), ), ); } } /// Parent-Artikel (Stückliste) — zeigt eine Header-Zeile und darunter die /// einzelnen Komponenten als [ComponentRow]. class ParentArticleRow extends StatelessWidget { const ParentArticleRow({ super.key, required this.article, required this.parentHeld, required this.disabled, this.heldComponents = const {}, }); /// Der Parent-Artikel (muss `isParent == true` und `components.isNotEmpty`). final Article article; /// `true`, wenn der gesamte Parent-Artikel zurückgehalten ist /// (vererbt sich auf alle Komponenten). final bool parentHeld; /// `true`, wenn die Lieferung deaktiviert ist (z. B. abgebrochen). final bool disabled; /// Set der gehaltenen Komponenten-Schlüssel (siehe [HoldKey.component]). final Set heldComponents; @override Widget build(BuildContext context) { final theme = Theme.of(context); final scheme = theme.colorScheme; final allDone = article.isFullyScanned; final scannedCount = article.components.where((c) => c.isFullyScanned).length; final effectiveDisabled = disabled || parentHeld; // Card-Styling für Stückliste — gleiche Logik wie reguläre Artikel, // aber mit Stücklisten-Icon und der Komponenten-Liste innerhalb derselben // Card (visuell gruppiert). final Color cardColor; final Color borderColor; final IconData headerIcon; final Color headerIconColor; if (parentHeld) { cardColor = _holdBadgeColor.withValues(alpha: 0.07); borderColor = _holdBadgeColor.withValues(alpha: 0.45); headerIcon = Icons.pause_circle_outline; headerIconColor = _holdBadgeColor; } else if (allDone) { cardColor = Colors.green.withValues(alpha: 0.07); borderColor = Colors.green.withValues(alpha: 0.45); headerIcon = Icons.check_circle; headerIconColor = Colors.green.shade700; } else { cardColor = scheme.surfaceContainerLow; borderColor = scheme.outlineVariant.withValues(alpha: 0.4); headerIcon = Icons.account_tree_outlined; headerIconColor = scheme.primary; } return Opacity( opacity: effectiveDisabled ? 0.45 : 1.0, child: Card( margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), elevation: 0, color: cardColor, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), side: BorderSide(color: borderColor), ), child: Padding( padding: const EdgeInsets.fromLTRB(12, 10, 12, 10), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Header-Reihe mit Icon, Name, Komponenten-Counter. Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Container( width: 36, height: 36, decoration: BoxDecoration( color: headerIconColor.withValues(alpha: 0.15), shape: BoxShape.circle, ), child: Icon(headerIcon, color: headerIconColor, size: 20), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( article.name, style: TextStyle( fontSize: 15, fontWeight: FontWeight.w600, decoration: parentHeld ? TextDecoration.lineThrough : TextDecoration.none, ), ), const SizedBox(height: 2), Text( "Stückliste · $scannedCount/${article.components.length} Komponenten", style: TextStyle( fontSize: 12, color: scheme.onSurfaceVariant, ), ), ], ), ), const SizedBox(width: 8), Icon( allDone ? Icons.check_circle : Icons.pending_outlined, color: allDone ? Colors.green : Colors.orange, size: 22, ), ], ), if (parentHeld) ...[ const SizedBox(height: 8), const _HeldBadge(), ], if (article.components.isNotEmpty) ...[ const SizedBox(height: 10), Divider( height: 1, color: scheme.outlineVariant.withValues(alpha: 0.6), ), const SizedBox(height: 6), ...article.components.map( (c) => ComponentRow( component: c, parentArticle: article, isHeld: parentHeld || heldComponents.contains(HoldKey.component(article, c)), disabled: disabled, ), ), ], ], ), ), ), ); } } /// Eine einzelne Komponenten-Zeile (Position einer Stückliste). class ComponentRow extends StatelessWidget { const ComponentRow({ super.key, required this.component, required this.parentArticle, required this.isHeld, required this.disabled, }); /// Die Komponente. final Component component; /// Parent-Artikel zur Auflösung des Hold-Keys & Anzeige-Kontextes. final Article parentArticle; /// `true`, wenn diese Komponente (oder der Parent) zurückgehalten ist. final bool isHeld; /// `true`, wenn die Lieferung deaktiviert ist. final bool disabled; @override Widget build(BuildContext context) { final scheme = Theme.of(context).colorScheme; final done = component.isFullyScanned; final effectiveDisabled = disabled || isHeld; // Component-Reihe sitzt INNERHALB der Parent-Card — daher kein eigener // Card-Wrapper. Stattdessen klare Einrückung + dezente Status-Markierung. final Color iconColor = done ? Colors.green.shade700 : (isHeld ? _holdBadgeColor : scheme.onSurfaceVariant); final IconData icon = isHeld ? Icons.pause_circle_outline : (done ? Icons.check_circle : Icons.radio_button_unchecked); return Opacity( opacity: effectiveDisabled ? 0.45 : 1.0, child: Padding( padding: const EdgeInsets.fromLTRB(48, 6, 4, 6), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row( children: [ Icon(icon, color: iconColor, size: 18), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( component.name, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w500, decoration: isHeld ? TextDecoration.lineThrough : TextDecoration.none, ), ), Text( "Artikelnr. ${component.articleNumber}", style: TextStyle( fontSize: 11, color: scheme.onSurfaceVariant, ), ), ], ), ), _ScanCountBadge( done: component.scannedAmount, total: component.requiredAmount, isComplete: done, compact: true, ), ], ), if (isHeld) ...[ const SizedBox(height: 4), const _HeldBadge(indented: true), ], ], ), ), ); } } /// Kompaktes Mengen-Badge `x / y×` für Artikel-/Komponenten-Karten. /// `compact: true` reduziert Padding und Schriftgröße für die Verwendung /// innerhalb der Parent-Card. class _ScanCountBadge extends StatelessWidget { const _ScanCountBadge({ required this.done, required this.total, required this.isComplete, this.compact = false, }); final int done; final int total; final bool isComplete; final bool compact; @override Widget build(BuildContext context) { final scheme = Theme.of(context).colorScheme; final color = isComplete ? Colors.green.shade700 : scheme.primary; return Container( padding: EdgeInsets.symmetric( horizontal: compact ? 8 : 10, vertical: compact ? 3 : 5, ), decoration: BoxDecoration( color: color.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(8), ), child: Text( "$done / $total×", style: TextStyle( fontSize: compact ? 11 : 13, fontWeight: FontWeight.bold, color: color, ), ), ); } } class _HeldBadge extends StatelessWidget { const _HeldBadge({this.indented = false}); /// Linke Einrückung — für Komponenten unter dem Parent-Header in der Card. final bool indented; @override Widget build(BuildContext context) { return Padding( padding: EdgeInsets.only(left: indented ? 28 : 0), child: Align( alignment: Alignment.centerLeft, child: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: _holdBadgeColor.withValues(alpha: 0.14), borderRadius: BorderRadius.circular(6), border: Border.all(color: _holdBadgeColor.withValues(alpha: 0.5)), ), child: Row( mainAxisSize: MainAxisSize.min, children: const [ Icon(Icons.pause_circle_outline, size: 12, color: _holdBadgeColor), SizedBox(width: 4), Text( "Heute zurückgehalten", style: TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: _holdBadgeColor, ), ), ], ), ), ), ); } }