import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:hl_lieferservice/domain/entity/payment_method.dart'; import 'package:hl_lieferservice/feature/delivery/detail/model/delivery_payment_status.dart'; import 'package:hl_lieferservice/feature/delivery/detail/presentation/widget/payment_method_icon.dart'; import 'package:hl_lieferservice/feature/delivery/detail/presentation/widget/reason_dialog.dart'; import 'package:hl_lieferservice/feature/payment_methods/bloc/payment_methods_cubit.dart'; /// Ergebnis des Zahlungs-Modals. `null` = abgebrochen, nichts passiert. sealed class PaymentSheetResult { const PaymentSheetResult(); } /// Der Fahrer hat bestätigt, dass die Zahlung per [method] erhalten wurde /// (bzw. bei „Rechnung": dass auf Rechnung abgewickelt wird). class PaymentConfirmed extends PaymentSheetResult { const PaymentConfirmed(this.method); final PaymentMethod method; } /// Zahlung ist nicht möglich — die ganze Lieferung soll abgebrochen werden. class DeliveryCancelRequested extends PaymentSheetResult { const DeliveryCancelRequested(this.reason); final String reason; } /// Öffnet das Zahlungs-Modal. /// /// Zeigt groß den offenen Betrag, die Zahlungsmethoden als Cards (dynamisch /// aus den Backend-Stammdaten) und bei EC-Karte groß die Kundennummer, die /// der Fahrer am EC-Gerät eingibt. Schließt sich nur über „Abbrechen", /// „Zahlung erhalten" oder „Lieferung abbrechen". Future showPaymentSheet( BuildContext context, { required int openCents, required int customerNumber, required String? preselectedMethodId, }) { return showModalBottomSheet( context: context, isScrollControlled: true, useSafeArea: true, // Kein versehentliches Wegwischen: Das Modal endet nur über die Buttons. isDismissible: false, enableDrag: false, showDragHandle: false, builder: (_) => _PaymentSheet( openCents: openCents, customerNumber: customerNumber, preselectedMethodId: preselectedMethodId, ), ); } class _PaymentSheet extends StatefulWidget { const _PaymentSheet({ required this.openCents, required this.customerNumber, required this.preselectedMethodId, }); final int openCents; final int customerNumber; final String? preselectedMethodId; @override State<_PaymentSheet> createState() => _PaymentSheetState(); } class _PaymentSheetState extends State<_PaymentSheet> { String? _selectedId; @override void initState() { super.initState(); // Vorauswahl = am Beleg hinterlegte Methode. Der Fahrer kann wechseln. _selectedId = widget.preselectedMethodId; } Future _cancelDelivery() async { final navigator = Navigator.of(context); final reason = await showReasonDialog(context, 'Lieferung abbrechen'); if (reason == null || !mounted) return; navigator.pop(DeliveryCancelRequested(reason)); } @override Widget build(BuildContext context) { final theme = Theme.of(context); return BlocBuilder( builder: (context, pmState) { final methods = pmState is PaymentMethodsLoaded ? pmState.methods.where((m) => m.active).toList() : const []; PaymentMethod? selected; for (final m in methods) { if (m.id == _selectedId) selected = m; } final isInvoice = selected?.code == 'invoice'; return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Flexible( child: ListView( shrinkWrap: true, padding: const EdgeInsets.fromLTRB(20, 20, 20, 8), children: [ Text( 'Zahlung abwickeln', style: theme.textTheme.titleLarge?.copyWith( fontWeight: FontWeight.w700, ), ), const SizedBox(height: 16), _AmountBanner(openCents: widget.openCents), const SizedBox(height: 20), Text( 'Zahlungsart', style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w700, ), ), const SizedBox(height: 8), ..._methodSection(context, pmState, methods), if (selected != null) ...[ const SizedBox(height: 8), _MethodHint( method: selected, customerNumber: widget.customerNumber, ), ], ], ), ), SafeArea( top: false, child: Padding( padding: const EdgeInsets.fromLTRB(20, 8, 20, 12), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ FilledButton.icon( style: FilledButton.styleFrom( minimumSize: const Size.fromHeight(60), textStyle: const TextStyle( fontSize: 18, fontWeight: FontWeight.w700, ), ), onPressed: selected == null ? null : () => Navigator.of( context, ).pop(PaymentConfirmed(selected!)), icon: const Icon(Icons.check_circle_outline), label: Text( isInvoice ? 'Auf Rechnung bestätigen' : 'Zahlung erhalten', ), ), const SizedBox(height: 8), Row( children: [ Expanded( flex: 3, child: OutlinedButton.icon( style: OutlinedButton.styleFrom( minimumSize: const Size.fromHeight(48), padding: const EdgeInsets.symmetric( horizontal: 12, ), foregroundColor: theme.colorScheme.error, side: BorderSide(color: theme.colorScheme.error), ), onPressed: _cancelDelivery, icon: const Icon(Icons.block), label: const Text('Lieferung abbrechen'), ), ), const SizedBox(width: 12), Expanded( flex: 2, child: TextButton( style: TextButton.styleFrom( minimumSize: const Size.fromHeight(48), ), onPressed: () => Navigator.of(context).pop(), child: const Text('Abbrechen'), ), ), ], ), ], ), ), ), ], ); }, ); } List _methodSection( BuildContext context, PaymentMethodsState state, List methods, ) { if (state is PaymentMethodsFailed) { return [ Card( margin: EdgeInsets.zero, color: Theme.of(context).colorScheme.errorContainer, child: ListTile( title: Text(state.message), trailing: TextButton.icon( onPressed: () => context.read().load(), icon: const Icon(Icons.refresh), label: const Text('Erneut laden'), ), ), ), ]; } if (state is! PaymentMethodsLoaded) { return const [ Padding( padding: EdgeInsets.all(16), child: Center(child: CircularProgressIndicator()), ), ]; } if (methods.isEmpty) { return const [Text('Keine Zahlungsmethoden hinterlegt.')]; } return [ for (final m in methods) _MethodCard( method: m, selected: m.id == _selectedId, onTap: () => setState(() => _selectedId = m.id), ), ]; } } /// Der zu zahlende Betrag, bewusst sehr groß. class _AmountBanner extends StatelessWidget { const _AmountBanner({required this.openCents}); final int openCents; @override Widget build(BuildContext context) { final theme = Theme.of(context); final primary = theme.colorScheme.primary; return Container( padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 16), decoration: BoxDecoration( color: primary.withValues(alpha: 0.08), borderRadius: BorderRadius.circular(16), border: Border.all(color: primary.withValues(alpha: 0.35)), ), child: Column( children: [ Text('Noch zu zahlen', style: theme.textTheme.titleSmall), const SizedBox(height: 4), FittedBox( fit: BoxFit.scaleDown, child: Text( formatEuroCents(openCents), style: theme.textTheme.displayMedium?.copyWith( fontWeight: FontWeight.w800, color: primary, ), ), ), ], ), ); } } class _MethodCard extends StatelessWidget { const _MethodCard({ required this.method, required this.selected, required this.onTap, }); final PaymentMethod method; final bool selected; final VoidCallback onTap; @override Widget build(BuildContext context) { final scheme = Theme.of(context).colorScheme; return Card( margin: const EdgeInsets.only(bottom: 10), clipBehavior: Clip.antiAlias, elevation: selected ? 2 : 0, color: selected ? scheme.primaryContainer : scheme.surface, surfaceTintColor: Colors.transparent, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(14), side: BorderSide( color: selected ? scheme.primary : scheme.outlineVariant, width: selected ? 2 : 1, ), ), child: InkWell( onTap: onTap, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 18), child: Row( children: [ Icon( paymentMethodIcon(method.code), size: 30, color: selected ? scheme.primary : scheme.onSurfaceVariant, ), const SizedBox(width: 16), Expanded( child: Text( method.name, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.w600, ), ), ), Icon( selected ? Icons.radio_button_checked : Icons.radio_button_off, color: selected ? scheme.primary : scheme.outline, ), ], ), ), ), ); } } /// Hinweis zur gewählten Methode. Bei EC-Karte groß die Kundennummer, die /// der Fahrer am EC-Gerät eingeben muss. class _MethodHint extends StatelessWidget { const _MethodHint({required this.method, required this.customerNumber}); final PaymentMethod method; final int customerNumber; @override Widget build(BuildContext context) { final theme = Theme.of(context); if (method.code == 'ec_card') { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: theme.colorScheme.secondaryContainer, borderRadius: BorderRadius.circular(16), ), child: Column( children: [ Text( 'Kundennummer am EC-Gerät eingeben', style: theme.textTheme.titleSmall?.copyWith( color: theme.colorScheme.onSecondaryContainer, ), ), const SizedBox(height: 6), FittedBox( fit: BoxFit.scaleDown, child: SelectableText( '$customerNumber', style: theme.textTheme.displayMedium?.copyWith( fontWeight: FontWeight.w800, letterSpacing: 4, fontFeatures: const [FontFeature.tabularFigures()], color: theme.colorScheme.onSecondaryContainer, ), ), ), ], ), ); } final text = switch (method.code) { 'cash' => 'Betrag bar entgegennehmen und danach bestätigen.', 'invoice' => 'Der Betrag wird per Rechnung beglichen. Es wird vor Ort ' 'nichts kassiert.', _ => 'Zahlung abwickeln und danach bestätigen.', }; return Text( text, style: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ); } }