Files
Holzleitner-Lieferservice-App/lib/feature/loading/widget/reason_picker_dialog.dart
Dennis Nemec 456fb59668 Phasenbasierte Lieferübersicht + Beladen-Flow, plus Migrationsplan für Rust-Backend
UI-Restructuring:
- TabBar in scan_page durch dedizierte Phasen ersetzt: Sortieren / Beladen / Ausliefern
- PhaseBloc + PhaseService leiten Phase aus Tour-/Item-States ab
- DeliverySelectionPage (ab 2 Autos) und DeliverySortPage als eigene Flows
- LoadingOverviewPage / LoadingCustomerPage für die Beladephase
- PhaseStepper-Widget im Home für Phasen-Anzeige
- Lager-Differenzierung (Standardlager 0 vs. Außenlager) via WarehouseBadge

Process-Stubs:
- ProcessRepository für Hold/Cancel/Sort/Assign-Flows (stub, bereit für Backend-Anbindung)

Doku:
- docs/BACKEND_MIGRATION.md: Phasenplan für Umstellung auf das neue
  Rust-Backend (OpenAPI-Generator, Keycloak OIDC, Clean-Arch-Layering)
2026-05-14 22:27:56 +02:00

141 lines
4.3 KiB
Dart

import 'package:flutter/material.dart';
/// Vordefinierte Gründe für Abbruch / Teilabbruch.
///
/// Die Liste ist absichtlich kurz und fahrernah gehalten — wir fragen keine
/// Mini-Romane ab, sondern erlauben das Wichtigste mit einem Tap. Für alles
/// Sonstige steht "Anderer Grund" mit Freitext zur Verfügung.
const List<String> _predefinedReasons = [
"Kunde nicht erreichbar",
"Adresse falsch",
"Ware beschädigt",
"Zugang nicht möglich",
"Anderer Grund",
];
/// Schlüssel-Konstante für die "Anderer Grund"-Option — damit Aufrufer den
/// Vergleich nicht über String-Literals führen müssen.
const String _otherReasonOption = "Anderer Grund";
/// Wiederverwendbarer Grund-Dialog für Beladen-Phase: sowohl der komplette
/// Lieferungs-Abbruch als auch das Zurückhalten einzelner Artikel /
/// Komponenten landen in diesem Picker.
///
/// Liefert per `showDialog<String>` den finalen Grundtext zurück — also
/// entweder einen der vordefinierten Strings oder den vom Fahrer
/// eingegebenen Freitext. Bei Abbruch des Dialogs ist das Ergebnis `null`.
class ReasonPickerDialog extends StatefulWidget {
const ReasonPickerDialog({
super.key,
required this.title,
this.subtitle,
});
/// Anzeigetitel des Dialogs (z. B. "Lieferung abbrechen").
final String title;
/// Optionaler erläuternder Untertitel (z. B. Name des Kunden).
final String? subtitle;
/// Komfort-Helfer: zeigt den Dialog und liefert das Ergebnis. Aufrufer
/// müssen so nicht mehr selbst `showDialog<String>` mit dem Builder
/// instanziieren.
static Future<String?> show(
BuildContext context, {
required String title,
String? subtitle,
}) {
return showDialog<String>(
context: context,
builder: (_) => ReasonPickerDialog(title: title, subtitle: subtitle),
);
}
@override
State<ReasonPickerDialog> createState() => _ReasonPickerDialogState();
}
class _ReasonPickerDialogState extends State<ReasonPickerDialog> {
String? _selected;
final TextEditingController _freeText = TextEditingController();
@override
void dispose() {
_freeText.dispose();
super.dispose();
}
bool get _isOther => _selected == _otherReasonOption;
bool get _canConfirm {
if (_selected == null) return false;
if (_isOther) return _freeText.text.trim().isNotEmpty;
return true;
}
void _confirm() {
if (!_canConfirm) return;
final reason = _isOther ? _freeText.text.trim() : _selected!;
Navigator.of(context).pop(reason);
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(widget.title),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (widget.subtitle != null) ...[
Text(
widget.subtitle!,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 12),
],
..._predefinedReasons.map((reason) {
return RadioListTile<String>(
contentPadding: EdgeInsets.zero,
dense: true,
title: Text(reason),
value: reason,
groupValue: _selected,
onChanged: (val) => setState(() => _selected = val),
);
}),
if (_isOther)
Padding(
padding: const EdgeInsets.only(top: 4),
child: TextField(
controller: _freeText,
autofocus: true,
maxLines: 3,
minLines: 2,
decoration: const InputDecoration(
labelText: "Bitte Grund angeben",
border: OutlineInputBorder(),
),
onChanged: (_) => setState(() {}),
),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text("Abbrechen"),
),
FilledButton(
onPressed: _canConfirm ? _confirm : null,
child: const Text("Bestätigen"),
),
],
);
}
}