import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:hl_lieferservice/domain/entity/delivery.dart'; import 'package:hl_lieferservice/domain/entity/delivery_credit.dart'; import 'package:hl_lieferservice/domain/entity/delivery_item.dart'; import 'package:hl_lieferservice/domain/entity/tour_details.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'; import 'package:hl_lieferservice/feature/payment_methods/bloc/payment_methods_cubit.dart'; /// Step 5 — Übersicht & Abschluss. /// /// Listet alle Artikel mit der **tatsächlich auszuliefernden Menge** auf /// (Original-Soll minus lokaler Partial-Remove-Drafts minus /// Komplett-Removes). Dazu Anzahlung-Anzeige, optionale Gutschrift, /// Zahlungsmethoden-Dropdown. /// /// Der „Unterschreiben"-Button lebt in der Bottom-Navigation des /// Page-Wrappers; hier zeigen wir den Resümee-Block, der direkt vor der /// Unterschrift steht. class StepSummary extends StatelessWidget { const StepSummary({ super.key, required this.delivery, required this.details, }); final Delivery delivery; final TourDetails details; @override Widget build(BuildContext context) { return BlocBuilder( builder: (context, wfState) { return ListView( padding: const EdgeInsets.fromLTRB(16, 16, 16, 24), children: [ _SectionHeader(text: 'Ausgelieferte Artikel'), const SizedBox(height: 8), _DeliveredItems( delivery: delivery, details: details, ), const SizedBox(height: 24), _SectionHeader(text: 'Zahlung'), const SizedBox(height: 8), _PaymentSummary( delivery: delivery, credit: details.creditOf(delivery.id), ), const SizedBox(height: 24), _SectionHeader(text: 'Zahlungsmethode'), const SizedBox(height: 8), _PaymentMethodPicker( delivery: delivery, overrideId: wfState.paymentMethodOverrideId, credit: details.creditOf(delivery.id), ), const SizedBox(height: 16), const _SignHint(), ], ); }, ); } } class _SectionHeader extends StatelessWidget { const _SectionHeader({required this.text}); final String text; @override Widget build(BuildContext context) { return Text( text, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w700, ), ); } } class _DeliveredItems extends StatelessWidget { const _DeliveredItems({ required this.delivery, required this.details, }); final Delivery delivery; final TourDetails details; @override Widget build(BuildContext context) { final theme = Theme.of(context); // Innerhalb einer Belegzeile: Oberartikel vor seinen Komponenten, damit // Komponenten direkt darunter eingerückt erscheinen. final items = List.of(delivery.items) ..sort((a, b) { final byLine = a.belegzeilenNr.compareTo(b.belegzeilenNr); if (byLine != 0) return byLine; final byParent = (a.isComponent ? 1 : 0).compareTo(b.isComponent ? 1 : 0); if (byParent != 0) return byParent; return (a.komponentenArtikelNr ?? '') .compareTo(b.komponentenArtikelNr ?? ''); }); if (items.isEmpty) { return Card( margin: EdgeInsets.zero, child: Padding( padding: const EdgeInsets.all(16), child: Text( 'Keine Artikel hinterlegt.', style: TextStyle( color: theme.colorScheme.onSurfaceVariant, fontStyle: FontStyle.italic, ), ), ), ); } return Card( margin: EdgeInsets.zero, child: Column( children: [ for (int i = 0; i < items.length; i++) ...[ _DeliveredRow( item: items[i], details: details, ), if (i < items.length - 1) const Divider(height: 1, indent: 16, endIndent: 16), ], ], ), ); } } class _DeliveredRow extends StatelessWidget { const _DeliveredRow({ required this.item, required this.details, }); final DeliveryItem item; final TourDetails details; @override Widget build(BuildContext context) { final theme = Theme.of(context); final article = details.articleOf(item.articleId); // Ausgeliefert = Soll − Gutschrift (vom Backend). Voll gutgeschrieben // (status removed) ⇒ credited == required ⇒ delivered 0. final credited = item.scanProgress.creditedQuantity; final delivered = (item.requiredQuantity - credited).clamp( 0, item.requiredQuantity, ); final Color avatarColor; if (delivered == 0) { avatarColor = Colors.red.shade400; } else if (delivered < item.requiredQuantity) { avatarColor = Colors.amber.shade700; } else { avatarColor = Colors.green.shade600; } return ListTile( // Komponenten um eine Stufe eingerückt (gehören zum Oberartikel darüber). contentPadding: EdgeInsets.only(left: item.isComponent ? 40 : 16, right: 16), leading: CircleAvatar( backgroundColor: avatarColor, foregroundColor: theme.colorScheme.onPrimary, child: Text( '$delivered×', style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold), ), ), title: Text( '${item.isComponent ? '↳ ' : ''}${article?.name ?? '⟨Unbekannter Artikel⟩'}', style: TextStyle( fontWeight: FontWeight.w600, decoration: delivered == 0 ? TextDecoration.lineThrough : null, color: delivered == 0 ? theme.colorScheme.onSurfaceVariant : null, ), ), subtitle: Text( [ if (delivered < item.requiredQuantity) 'von ${item.requiredQuantity} bestellt · Gutschrift: $credited' else 'Artikelnr. ${article?.articleNumber ?? item.articleId}', '${item.unitPrice.toStringAsFixed(2)} € / Stück', ].join(' · '), style: TextStyle( fontSize: 12, color: theme.colorScheme.onSurfaceVariant, ), ), trailing: Text( '${item.lineTotal.toStringAsFixed(2)} €', style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w700, decoration: delivered == 0 ? TextDecoration.lineThrough : null, color: delivered == 0 ? theme.colorScheme.onSurfaceVariant : null, ), ), ); } } /// Offener Betrag der Lieferung in Euro: Warenwert (Σ Stückpreis × gelieferte /// Menge) − Anzahlung − Gutschrift, nie negativ. Einzige Quelle dieser Formel — /// genutzt von der Zahlungs-Übersicht UND der Zahlungsmethoden-Auswahl. double _openAmount(Delivery delivery, DeliveryCredit? credit) { final creditEuros = (credit?.amountCents ?? 0) / 100.0; final warenwert = delivery.items.fold(0, (acc, item) => acc + item.lineTotal); return (warenwert - delivery.prepaidAmount - creditEuros) .clamp(0.0, double.infinity); } class _PaymentSummary extends StatelessWidget { const _PaymentSummary({required this.delivery, required this.credit}); final Delivery delivery; final DeliveryCredit? credit; @override Widget build(BuildContext context) { final theme = Theme.of(context); // Warenwert = Σ Stückpreis × ausgelieferte Menge (entfernte/teil-entfernte // Positionen fallen automatisch raus). final warenwert = delivery.items .fold(0, (acc, item) => acc + item.lineTotal); // Offener Betrag über den gemeinsamen Helper (gleiche Formel wie die // Zahlungsmethoden-Auswahl). final open = _openAmount(delivery, credit); return Card( margin: EdgeInsets.zero, child: Padding( padding: const EdgeInsets.all(16), child: Column( children: [ _SummaryRow( icon: Icons.receipt_long_outlined, label: 'Warenwert', valueText: '${warenwert.toStringAsFixed(2)} €', valueColor: theme.colorScheme.onSurface, ), const SizedBox(height: 12), _SummaryRow( icon: Icons.savings_outlined, label: 'Bei Bestellung bezahlt', valueText: '− ${delivery.prepaidAmount.toStringAsFixed(2)} €', valueColor: delivery.prepaidAmount > 0 ? Colors.green.shade700 : theme.colorScheme.onSurfaceVariant, ), if (credit != null) ...[ const SizedBox(height: 12), _SummaryRow( icon: Icons.card_giftcard_outlined, label: 'Gutschrift', valueText: '− ${(credit!.amountCents / 100).toStringAsFixed(2)} €', valueColor: Colors.amber.shade800, subtitle: credit!.reason, ), ], const Divider(height: 24), _SummaryRow( icon: Icons.account_balance_wallet_outlined, label: 'Offener Betrag', valueText: '${open.toStringAsFixed(2)} €', valueColor: open > 0 ? theme.colorScheme.primary : Colors.green.shade700, emphasize: true, ), ], ), ), ); } } class _SummaryRow extends StatelessWidget { const _SummaryRow({ required this.icon, required this.label, required this.valueText, required this.valueColor, this.subtitle, this.emphasize = false, }); final IconData icon; final String label; final String valueText; final Color valueColor; final String? subtitle; /// Hebt Label + Wert hervor (für den „Offener Betrag"-Abschluss). final bool emphasize; @override Widget build(BuildContext context) { final theme = Theme.of(context); return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.only(top: 2), child: Icon(icon, color: theme.colorScheme.primary), ), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: emphasize ? const TextStyle(fontWeight: FontWeight.w700) : null, ), if (subtitle != null) Text( subtitle!, style: TextStyle( fontSize: 12, color: theme.colorScheme.onSurfaceVariant, ), ), ], ), ), Text( valueText, style: (emphasize ? theme.textTheme.titleLarge : theme.textTheme.titleMedium) ?.copyWith( fontWeight: FontWeight.w700, color: valueColor, ), ), ], ); } } class _PaymentMethodPicker extends StatelessWidget { const _PaymentMethodPicker({ required this.delivery, required this.overrideId, required this.credit, }); final Delivery delivery; final String? overrideId; final DeliveryCredit? credit; @override Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { if (state is PaymentMethodsLoading || state is PaymentMethodsInitial) { return const Card( margin: EdgeInsets.zero, child: Padding( padding: EdgeInsets.all(16), child: Row( children: [ SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ), SizedBox(width: 12), Text('Zahlungsmethoden laden …'), ], ), ), ); } if (state is PaymentMethodsFailed) { return Card( margin: EdgeInsets.zero, color: Theme.of(context).colorScheme.errorContainer, child: Padding( padding: const EdgeInsets.all(16), child: Text( state.message, style: TextStyle( color: Theme.of(context).colorScheme.onErrorContainer, ), ), ), ); } final loaded = state as PaymentMethodsLoaded; // Ausschließlich die Backend-Methoden — keine frontend-seitige // Fabrikation/Hardcodierung. Es werden genau die angezeigt, die im // Backend (Postgres `payment_methods`, aktiv) hinterlegt sind. final methods = loaded.methods; final selectedId = overrideId ?? delivery.paymentMethodId; // Als Dropdown-Value nur setzen, wenn die Methode tatsächlich in der // Backend-Liste ist (sonst würde Flutter asserten). Ist die zugewiesene // Methode zwischenzeitlich deaktiviert/entfernt, bleibt das Feld leer. final selectedValue = methods.any((m) => m.id == selectedId) ? selectedId : null; // Zahlungsmethode nur bei aktiver Lieferung änderbar. Bei // abgeschlossener/abgebrochener/pausierter Lieferung zeigt das // Dropdown den gewählten Stand, ist aber gesperrt. final active = delivery.state == DeliveryState.active; // Steht kein offener Betrag mehr aus (vollständig vorab bezahlt // oder per Gutschrift ausgeglichen), ist keine Zahlungsweise zu // wählen → Auswahl deaktivieren. final hasOpenAmount = _openAmount(delivery, credit) > 0; final editable = active && hasOpenAmount; return Card( margin: EdgeInsets.zero, child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ DropdownButtonFormField( initialValue: selectedValue, decoration: const InputDecoration( labelText: 'Zahlungsmethode', border: OutlineInputBorder(), ), items: [ for (final m in methods) DropdownMenuItem( value: m.id, child: Text(m.name), ), ], // `null` deaktiviert das Dropdown (Flutter-Konvention). onChanged: editable ? (newId) { if (newId == null) return; context.read().add( WorkflowOverridePaymentMethod( // Zurück auf die Original-Methode → Override // löschen, damit das Domain-Modell "no // override" kennt. paymentMethodId: newId == delivery.paymentMethodId ? null : newId, ), ); } : null, ), if (!active) ...[ const SizedBox(height: 8), const _PickerHint( text: 'Lieferung abgeschlossen — Zahlungsmethode nicht ' 'mehr änderbar.', ), ] else if (!hasOpenAmount) ...[ const SizedBox(height: 8), const _PickerHint( text: 'Kein offener Betrag — Auswahl der Zahlungsweise ' 'nicht erforderlich.', ), ], ], ), ), ); }, ); } } /// Dezenter Sperr-/Info-Hinweis unter dem Zahlungsmethoden-Dropdown /// (Schloss-Icon + Text in gedämpfter Farbe). class _PickerHint extends StatelessWidget { const _PickerHint({required this.text}); final String text; @override Widget build(BuildContext context) { final muted = Theme.of(context).colorScheme.onSurfaceVariant; return Row( children: [ Icon(Icons.lock_outline, size: 16, color: muted), const SizedBox(width: 8), Expanded( child: Text( text, style: Theme.of(context).textTheme.bodySmall?.copyWith(color: muted), ), ), ], ); } } class _SignHint extends StatelessWidget { const _SignHint(); @override Widget build(BuildContext context) { final theme = Theme.of(context); return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: theme.colorScheme.primary.withValues(alpha: 0.07), borderRadius: BorderRadius.circular(8), border: Border.all( color: theme.colorScheme.primary.withValues(alpha: 0.3), ), ), child: Row( children: [ Icon(Icons.draw_outlined, color: theme.colorScheme.primary), const SizedBox(width: 10), Expanded( child: Text( 'Mit „Unterschreiben" unten schließt der Kunde den Vorgang ab.', style: TextStyle( fontSize: 13, color: theme.colorScheme.primary, ), ), ), ], ), ); } }