import 'package:flutter/material.dart'; /// Ergebnis des Reason-Pickers: getrimmter Grund + optionale Menge. class ReasonPickerResult { const ReasonPickerResult({required this.reason, this.quantity}); final String reason; /// Gewählte Menge (1…maxQuantity). `null`, wenn das Sheet **ohne** /// Mengen-Auswahl geöffnet wurde (`maxQuantity == null`, z. B. bei /// Lieferungs-Gründen wie Pausieren/Abbrechen) — dann gilt „ganze Zeile". final int? quantity; } /// Bottom-Sheet zur Auswahl einer Begründung. Zeigt eine Radio-Liste der /// vordefinierten Gründe + den Sonderfall „Anderer Grund", der ein /// Freitext-Feld einblendet. /// /// Optional — wenn [maxQuantity] gesetzt und > 1 — zusätzlich eine /// **Mengen-Auswahl** (Stepper 1…maxQuantity, Vorbelegung = maxQuantity) für /// die Teilmengen-Löschung/-Gutschrift. /// /// Rückgabe: [ReasonPickerResult] oder `null` bei Abbruch. /// /// Validierung: /// * Vordefinierter Grund → direkt OK /// * „Anderer Grund" mit leerem Freitext → Bestätigen disabled Future showReasonPickerSheet({ required BuildContext context, required String title, required List presets, String confirmLabel = 'Übernehmen', int? maxQuantity, }) { return showModalBottomSheet( context: context, isScrollControlled: true, showDragHandle: true, builder: (ctx) => _ReasonPickerSheet( title: title, presets: presets, confirmLabel: confirmLabel, maxQuantity: maxQuantity, ), ); } class _ReasonPickerSheet extends StatefulWidget { const _ReasonPickerSheet({ required this.title, required this.presets, required this.confirmLabel, required this.maxQuantity, }); final String title; final List presets; final String confirmLabel; final int? maxQuantity; @override State<_ReasonPickerSheet> createState() => _ReasonPickerSheetState(); } /// Sentinel-Wert für die „Anderer Grund"-Option. Eigene Klasse, damit der /// Vergleich nicht mit echten Reason-Strings kollidieren kann (z. B. /// jemand definiert tatsächlich „Anderer Grund" als Standard-Reason). const String _otherSentinel = ' other'; class _ReasonPickerSheetState extends State<_ReasonPickerSheet> { String? _selected; final _customController = TextEditingController(); final _customFocus = FocusNode(); late int _qty; @override void initState() { super.initState(); // Vorbelegung: ganze Restmenge. _qty = widget.maxQuantity ?? 1; } @override void dispose() { _customController.dispose(); _customFocus.dispose(); super.dispose(); } bool get _isOther => _selected == _otherSentinel; /// Mengen-Stepper nur zeigen, wenn überhaupt eine Auswahl Sinn ergibt. bool get _showQuantity => (widget.maxQuantity ?? 0) > 1; String? get _effectiveReason { if (_selected == null) return null; if (_isOther) { final v = _customController.text.trim(); return v.isEmpty ? null : v; } return _selected; } void _onConfirm() { final reason = _effectiveReason; if (reason == null) return; Navigator.of(context).pop( ReasonPickerResult( reason: reason, // Ohne Mengen-Kontext (maxQuantity == null) → null = ganze Zeile. quantity: widget.maxQuantity == null ? null : _qty, ), ); } @override Widget build(BuildContext context) { final theme = Theme.of(context); return SafeArea( child: Padding( padding: EdgeInsets.only( left: 16, right: 16, top: 8, // Tastatur-Höhe rein-clipping, damit das Freitext-Feld sichtbar bleibt. bottom: MediaQuery.of(context).viewInsets.bottom + 16, ), // Scrollbar, damit Mengen-Stepper + Gründe + Freitext bei wenig Platz // (offene Tastatur) nicht overflowen. child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Padding( padding: const EdgeInsets.only(bottom: 8), child: Text( widget.title, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), ), ), if (_showQuantity) ...[ Text( 'Wie viele entfernen? (1–${widget.maxQuantity})', style: theme.textTheme.bodySmall, ), const SizedBox(height: 4), Row( children: [ IconButton.filled( onPressed: _qty > 1 ? () => setState(() => _qty -= 1) : null, icon: const Icon(Icons.remove), ), Expanded( child: Text( '$_qty', textAlign: TextAlign.center, style: theme.textTheme.titleLarge?.copyWith( fontWeight: FontWeight.bold, ), ), ), IconButton.filled( onPressed: _qty < widget.maxQuantity! ? () => setState(() => _qty += 1) : null, icon: const Icon(Icons.add), ), ], ), const Divider(height: 24), ], for (final preset in widget.presets) RadioListTile( value: preset, groupValue: _selected, onChanged: (v) => setState(() => _selected = v), title: Text(preset), contentPadding: EdgeInsets.zero, dense: true, ), RadioListTile( value: _otherSentinel, groupValue: _selected, onChanged: (v) { setState(() => _selected = v); WidgetsBinding.instance.addPostFrameCallback((_) { _customFocus.requestFocus(); }); }, title: const Text('Anderer Grund'), contentPadding: EdgeInsets.zero, dense: true, ), if (_isOther) Padding( padding: const EdgeInsets.only(top: 8, bottom: 8), child: TextField( controller: _customController, focusNode: _customFocus, autocorrect: false, decoration: const InputDecoration( border: OutlineInputBorder(), hintText: 'Begründung', ), onChanged: (_) => setState(() {}), textInputAction: TextInputAction.done, onSubmitted: (_) => _onConfirm(), ), ), const SizedBox(height: 8), FilledButton( onPressed: _effectiveReason == null ? null : _onConfirm, child: Text(widget.confirmLabel), ), ], ), ), ), ); } }