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)
141 lines
4.3 KiB
Dart
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"),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|