Final commit.
This commit is contained in:
@ -1,16 +1,17 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:hl_lieferservice/feature/authentication/bloc/auth_bloc.dart';
|
||||
import 'package:hl_lieferservice/feature/authentication/bloc/auth_state.dart';
|
||||
import 'package:hl_lieferservice/feature/delivery/bloc/tour_bloc.dart';
|
||||
import 'package:hl_lieferservice/feature/delivery/bloc/tour_event.dart';
|
||||
|
||||
/// Fallback-Page, die der Übersichts- und Beladen-Pfad anzeigt, wenn der
|
||||
/// Initial-Tour-Load gescheitert ist. Tap auf "Erneut versuchen" feuert
|
||||
/// `LoadTour` erneut — Account-Filter sitzt jetzt im JWT, daher keine
|
||||
/// Personalnummer mehr nötig.
|
||||
class DeliveryLoadingFailedPage extends StatelessWidget {
|
||||
const DeliveryLoadingFailedPage({super.key});
|
||||
|
||||
void _onRetry(BuildContext context) {
|
||||
Authenticated state = context.read<AuthBloc>().state as Authenticated;
|
||||
context.read<TourBloc>().add(LoadTour(teamId: state.user.number));
|
||||
context.read<TourBloc>().add(const LoadTour());
|
||||
}
|
||||
|
||||
@override
|
||||
@ -21,18 +22,22 @@ class DeliveryLoadingFailedPage extends StatelessWidget {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.error_outline, size: 72, color: Theme.of(context).colorScheme.error,),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 30),
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 72,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(top: 30),
|
||||
child: Text(
|
||||
"Leider ist es beim Laden der Fahrten zu einem Fehler gekommen.",
|
||||
'Leider ist es beim Laden der Fahrten zu einem Fehler gekommen.',
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 30),
|
||||
child: FilledButton(
|
||||
onPressed: () => _onRetry(context),
|
||||
child: Text("Erneut versuchen"),
|
||||
child: const Text('Erneut versuchen'),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@ -1,23 +1,28 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hl_lieferservice/model/delivery.dart' show DeliveryState;
|
||||
import 'package:hl_lieferservice/model/tour.dart';
|
||||
import 'package:hl_lieferservice/domain/entity/delivery.dart';
|
||||
import 'package:hl_lieferservice/domain/entity/tour_details.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
/// Kopf-Karte der Auslieferungs-Übersicht. Zeigt Datum, Anzahl Lieferungen
|
||||
/// und Fortschrittsbalken — gefiltert auf das aktuell gewählte Fahrzeug,
|
||||
/// damit der Fahrer seine eigene Tagesleistung sieht.
|
||||
class DeliveryInfo extends StatelessWidget {
|
||||
final Tour tour;
|
||||
final TourDetails details;
|
||||
final String? selectedCarId;
|
||||
|
||||
const DeliveryInfo({super.key, required this.tour, this.selectedCarId});
|
||||
const DeliveryInfo({super.key, required this.details, this.selectedCarId});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final String date = DateFormat("dd.MM.yyyy").format(tour.date);
|
||||
final date = DateFormat('dd.MM.yyyy').format(details.tour.date);
|
||||
final relevantDeliveries = selectedCarId != null
|
||||
? tour.deliveries.where((d) => d.carId == selectedCarId).toList()
|
||||
: tour.deliveries;
|
||||
? details.deliveries
|
||||
.where((d) => d.assignedCarId == selectedCarId)
|
||||
.toList()
|
||||
: details.deliveries;
|
||||
final total = relevantDeliveries.length;
|
||||
final done = relevantDeliveries
|
||||
.where((d) => d.state == DeliveryState.finished)
|
||||
.where((d) => d.state == DeliveryState.completed)
|
||||
.length;
|
||||
final progress = total > 0 ? done / total : 0.0;
|
||||
final allDone = total > 0 && done == total;
|
||||
@ -37,11 +42,11 @@ class DeliveryInfo extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.calendar_month),
|
||||
const Padding(
|
||||
children: const [
|
||||
Icon(Icons.calendar_month),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: 5),
|
||||
child: Text("Datum"),
|
||||
child: Text('Datum'),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -53,15 +58,15 @@ class DeliveryInfo extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.local_shipping_outlined),
|
||||
const Padding(
|
||||
children: const [
|
||||
Icon(Icons.local_shipping_outlined),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: 5),
|
||||
child: Text("Lieferungen"),
|
||||
child: Text('Lieferungen'),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text("$done / $total"),
|
||||
Text('$done / $total'),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
@ -1,153 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:hl_lieferservice/feature/authentication/bloc/auth_bloc.dart';
|
||||
import 'package:hl_lieferservice/model/delivery.dart';
|
||||
import 'package:hl_lieferservice/feature/delivery/detail/presentation/delivery_detail_page.dart';
|
||||
|
||||
import '../../../../widget/operations/bloc/operation_bloc.dart';
|
||||
import '../../detail/bloc/note_bloc.dart';
|
||||
import '../../detail/repository/note_repository.dart';
|
||||
import '../../detail/service/notes_service.dart';
|
||||
|
||||
class DeliveryListItem extends StatelessWidget {
|
||||
final Delivery delivery;
|
||||
final double? distance;
|
||||
|
||||
const DeliveryListItem({
|
||||
super.key,
|
||||
required this.delivery,
|
||||
this.distance,
|
||||
});
|
||||
|
||||
void _goToDelivery(BuildContext context) {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => BlocProvider(
|
||||
create: (context) => NoteBloc(
|
||||
deliveryId: delivery.id,
|
||||
opBloc: context.read<OperationBloc>(),
|
||||
authBloc: context.read<AuthBloc>(),
|
||||
repository: NoteRepository(service: NoteService()),
|
||||
),
|
||||
child: DeliveryDetail(deliveryId: delivery.id),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
(Color, Color, IconData, String) _stateStyle(BuildContext context) {
|
||||
switch (delivery.state) {
|
||||
case DeliveryState.finished:
|
||||
return (
|
||||
Colors.green.withValues(alpha: 0.07),
|
||||
Colors.green.withValues(alpha: 0.35),
|
||||
Icons.check_circle_rounded,
|
||||
"Abgeschlossen",
|
||||
);
|
||||
case DeliveryState.canceled:
|
||||
return (
|
||||
Colors.red.withValues(alpha: 0.07),
|
||||
Colors.red.withValues(alpha: 0.35),
|
||||
Icons.cancel_rounded,
|
||||
"Storniert",
|
||||
);
|
||||
case DeliveryState.onhold:
|
||||
return (
|
||||
Colors.orange.withValues(alpha: 0.07),
|
||||
Colors.orange.withValues(alpha: 0.35),
|
||||
Icons.pause_circle_rounded,
|
||||
"Pausiert",
|
||||
);
|
||||
case DeliveryState.ongoing:
|
||||
final distanceLabel = distance != null && !distance!.isNaN
|
||||
? "${distance!.toStringAsFixed(1)} km"
|
||||
: "–";
|
||||
return (
|
||||
Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
Colors.transparent,
|
||||
Icons.local_shipping_outlined,
|
||||
distanceLabel,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final (cardColor, borderColor, icon, statusLabel) = _stateStyle(context);
|
||||
final isOngoing = delivery.state == DeliveryState.ongoing;
|
||||
|
||||
final iconColor = switch (delivery.state) {
|
||||
DeliveryState.finished => Colors.green,
|
||||
DeliveryState.canceled => Colors.red,
|
||||
DeliveryState.onhold => Colors.orange,
|
||||
DeliveryState.ongoing => Theme.of(context).primaryColor,
|
||||
};
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
elevation: 0,
|
||||
color: cardColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: BorderSide(color: borderColor),
|
||||
),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
onTap: () => _goToDelivery(context),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, color: iconColor, size: 28),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
delivery.customer.name,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: isOngoing ? null : iconColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
delivery.customer.address.toString(),
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
statusLabel,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: isOngoing
|
||||
? Theme.of(context).colorScheme.onSurfaceVariant
|
||||
: iconColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 14,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,126 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:hl_lieferservice/feature/delivery/bloc/tour_bloc.dart';
|
||||
import 'package:hl_lieferservice/feature/delivery/bloc/tour_state.dart';
|
||||
|
||||
import 'package:hl_lieferservice/feature/delivery/overview/presentation/delivery_overview.dart';
|
||||
import 'package:hl_lieferservice/model/delivery.dart';
|
||||
|
||||
import 'delivery_item.dart';
|
||||
|
||||
class DeliveryList extends StatefulWidget {
|
||||
final String? selectedCarId;
|
||||
final SortType sortType;
|
||||
|
||||
const DeliveryList({super.key, this.selectedCarId, required this.sortType});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _DeliveryListState();
|
||||
}
|
||||
|
||||
class _DeliveryListState extends State<DeliveryList> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
Widget _showCustomSortedList(
|
||||
List<Delivery> deliveries,
|
||||
List<String> sortingInformation,
|
||||
Map<String, double> distances,
|
||||
) {
|
||||
return ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
separatorBuilder: (context, index) => const Divider(height: 0),
|
||||
itemBuilder: (context, index) {
|
||||
String id = sortingInformation[index];
|
||||
Delivery delivery = deliveries.firstWhere(
|
||||
(delivery) =>
|
||||
id == delivery.id &&
|
||||
delivery.carId == widget.selectedCarId,
|
||||
);
|
||||
|
||||
return DeliveryListItem(
|
||||
delivery: delivery,
|
||||
distance: distances[delivery.id],
|
||||
);
|
||||
},
|
||||
itemCount: sortingInformation.length,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<TourBloc, TourState>(
|
||||
builder: (context, state) {
|
||||
final currentState = state;
|
||||
if (currentState is TourLoaded) {
|
||||
if (widget.sortType == SortType.custom) {
|
||||
return _showCustomSortedList(
|
||||
currentState.tour.deliveries,
|
||||
currentState.sortingInformation[widget.selectedCarId.toString()] ?? [],
|
||||
currentState.distances ?? {},
|
||||
);
|
||||
}
|
||||
|
||||
final allDeliveries = currentState.tour.deliveries
|
||||
.where((d) => d.carId == widget.selectedCarId)
|
||||
.toList();
|
||||
|
||||
if (allDeliveries.isEmpty) {
|
||||
return ListView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
children: const [
|
||||
Center(child: Text("Keine Auslieferungen gefunden")),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
final ongoing = allDeliveries
|
||||
.where((d) => d.state == DeliveryState.ongoing)
|
||||
.toList();
|
||||
final nonOngoing = allDeliveries
|
||||
.where((d) => d.state != DeliveryState.ongoing)
|
||||
.toList();
|
||||
|
||||
int Function(Delivery, Delivery) comparator;
|
||||
switch (widget.sortType) {
|
||||
case SortType.nameAsc:
|
||||
comparator = (a, b) => a.customer.name.compareTo(b.customer.name);
|
||||
break;
|
||||
case SortType.nameDesc:
|
||||
comparator = (a, b) => b.customer.name.compareTo(a.customer.name);
|
||||
break;
|
||||
case SortType.distance:
|
||||
comparator = (a, b) =>
|
||||
(currentState.distances?[a.id] ?? 0.0)
|
||||
.compareTo(currentState.distances?[b.id] ?? 0.0);
|
||||
break;
|
||||
default:
|
||||
comparator = (a, b) => a.customer.name.compareTo(b.customer.name);
|
||||
}
|
||||
|
||||
ongoing.sort(comparator);
|
||||
nonOngoing.sort(comparator);
|
||||
|
||||
final sorted = [...ongoing, ...nonOngoing];
|
||||
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
itemCount: sorted.length,
|
||||
itemBuilder: (context, index) => DeliveryListItem(
|
||||
delivery: sorted[index],
|
||||
distance: currentState.distances?[sorted[index].id],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Center(child: CircularProgressIndicator());
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,151 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:hl_lieferservice/feature/delivery/bloc/tour_bloc.dart';
|
||||
import 'package:hl_lieferservice/feature/delivery/bloc/tour_event.dart';
|
||||
import 'package:hl_lieferservice/feature/delivery/bloc/tour_state.dart';
|
||||
import 'package:hl_lieferservice/feature/delivery/util.dart';
|
||||
import 'package:hl_lieferservice/model/delivery.dart';
|
||||
|
||||
class CustomSortDialog extends StatefulWidget {
|
||||
const CustomSortDialog({super.key, this.selectedCarId});
|
||||
|
||||
final String? selectedCarId;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _CustomSortDialogState();
|
||||
}
|
||||
|
||||
class _CustomSortDialogState extends State<CustomSortDialog> {
|
||||
late List<String> _localSortedList;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final state = context.read<TourBloc>().state;
|
||||
if (state is TourLoaded) {
|
||||
_localSortedList = [
|
||||
...state.sortingInformation[widget.selectedCarId.toString()] ?? [],
|
||||
];
|
||||
} else {
|
||||
_localSortedList = [];
|
||||
}
|
||||
}
|
||||
|
||||
Widget _information() {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(top: 15),
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.all(15),
|
||||
child: Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.only(right: 15),
|
||||
child: Icon(Icons.info_outline, color: Colors.blueAccent),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
"Ziehen Sie die einzelnen Lieferungen mit dem Finger in die gewünschte Position.",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Divider(),
|
||||
_sortableList(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _sortableList() {
|
||||
return BlocBuilder<TourBloc, TourState>(
|
||||
builder: (context, state) {
|
||||
final currentState = state;
|
||||
|
||||
if (currentState is TourLoaded) {
|
||||
return Expanded(
|
||||
child: ReorderableListView(
|
||||
onReorder: (oldIndex, newIndex) {
|
||||
setState(() {
|
||||
_localSortedList = reorderList(
|
||||
_localSortedList,
|
||||
oldIndex,
|
||||
newIndex,
|
||||
);
|
||||
});
|
||||
|
||||
context.read<TourBloc>().add(
|
||||
ReorderDeliveryEvent(
|
||||
newPosition: newIndex,
|
||||
oldPosition: oldIndex,
|
||||
carId: widget.selectedCarId.toString(),
|
||||
),
|
||||
);
|
||||
},
|
||||
children:
|
||||
_localSortedList
|
||||
.map((id) {
|
||||
Delivery delivery = currentState.tour.deliveries
|
||||
.firstWhere((delivery) => delivery.id == id);
|
||||
int pos = _localSortedList.indexOf(id) + 1;
|
||||
|
||||
return ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
child: Text(
|
||||
"$pos",
|
||||
style: TextStyle(
|
||||
color:
|
||||
Theme.of(context).colorScheme.onSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
title: Text(delivery.customer.name),
|
||||
subtitle: Text(
|
||||
delivery.customer.address.toString(),
|
||||
style: TextStyle(fontSize: 11),
|
||||
),
|
||||
trailing: Icon(Icons.drag_handle),
|
||||
key: Key("reorder-item-${delivery.id}"),
|
||||
);
|
||||
})
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Center(child: CircularProgressIndicator());
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Dialog(
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 20, right: 10, top: 15),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Fahrten sortieren",
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
icon: const Icon(Icons.close),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Expanded(child: _information()),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,69 +1,32 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:hl_lieferservice/domain/entity/tour_details.dart';
|
||||
import 'package:hl_lieferservice/feature/car_selection/bloc/bloc.dart';
|
||||
import 'package:hl_lieferservice/feature/car_selection/bloc/state.dart';
|
||||
import 'package:hl_lieferservice/feature/delivery/bloc/tour_bloc.dart';
|
||||
import 'package:hl_lieferservice/feature/delivery/bloc/tour_event.dart';
|
||||
import 'package:hl_lieferservice/feature/delivery/bloc/tour_state.dart';
|
||||
import 'package:hl_lieferservice/feature/delivery/model/delivery_phase.dart';
|
||||
import 'package:hl_lieferservice/feature/delivery/overview/presentation/delivery_fail_page.dart';
|
||||
import 'package:hl_lieferservice/feature/delivery/overview/presentation/delivery_overview.dart';
|
||||
import 'package:hl_lieferservice/model/tour.dart';
|
||||
import 'package:hl_lieferservice/widget/home/presentation/home_drawer.dart';
|
||||
import 'package:hl_lieferservice/widget/phase_stepper/phase_stepper.dart';
|
||||
import '../../bloc/tour_bloc.dart';
|
||||
import '../../bloc/tour_state.dart';
|
||||
|
||||
/// Inhalt der Phase "Ausliefern". Sortieren und Beladen werden über eigene
|
||||
/// Pages und das Phasen-Routing in `Home` gerendert — diese Page übernimmt
|
||||
/// nur noch die letzte Phase. Der Phasen-Stepper bleibt sichtbar, damit der
|
||||
/// Fahrer bei Bedarf zurückspringen kann; das BottomNav der Auslieferung
|
||||
/// liegt im umgebenden `Home`-Scaffold.
|
||||
class DeliveryOverviewPage extends StatefulWidget {
|
||||
class DeliveryOverviewPage extends StatelessWidget {
|
||||
const DeliveryOverviewPage({super.key});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _DeliveryOverviewPageState();
|
||||
}
|
||||
|
||||
class _DeliveryOverviewPageState extends State<DeliveryOverviewPage> {
|
||||
Widget _buildOverviewWithBanner({
|
||||
required Tour tour,
|
||||
required String bannerText,
|
||||
}) {
|
||||
return Column(
|
||||
children: [
|
||||
Material(
|
||||
color: Colors.amber.shade100,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: Text(bannerText)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: DeliveryOverview(tour: tour),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final carState = context.watch<CarSelectBloc>().state;
|
||||
final carId = carState is CarSelectComplete
|
||||
? carState.selectedCar.id.toString()
|
||||
: "";
|
||||
final carId =
|
||||
carState is CarSelectComplete ? carState.selectedCar.id : '';
|
||||
|
||||
return Scaffold(
|
||||
// Drawer ist hier ebenfalls aktiv, damit der Menü-Button des Steppers
|
||||
// konsistent über alle Phasen funktioniert.
|
||||
drawer: const HomeAppDrawer(),
|
||||
appBar: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(140),
|
||||
@ -72,25 +35,79 @@ class _DeliveryOverviewPageState extends State<DeliveryOverviewPage> {
|
||||
carId: carId,
|
||||
),
|
||||
),
|
||||
body: BlocBuilder<TourBloc, TourState>(
|
||||
body: BlocConsumer<TourBloc, TourState>(
|
||||
listenWhen: (prev, next) =>
|
||||
next is TourLoaded && next.refreshError != null,
|
||||
listener: (context, state) {
|
||||
if (state is TourLoaded && state.refreshError != null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(state.refreshError!)),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
if (state is TourLoaded) {
|
||||
if (state.distances == null) {
|
||||
return _buildOverviewWithBanner(
|
||||
tour: state.tour,
|
||||
bannerText: "Berechne Distanzen…",
|
||||
);
|
||||
}
|
||||
return DeliveryOverview(tour: state.tour);
|
||||
switch (state) {
|
||||
case TourLoaded(:final details):
|
||||
return _OverviewBody(details: details);
|
||||
case TourEmpty():
|
||||
return const _EmptyTourBody();
|
||||
case TourLoadFailed():
|
||||
return const DeliveryLoadingFailedPage();
|
||||
case TourInitial():
|
||||
case TourLoading():
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (state is TourLoadingFailed) {
|
||||
return DeliveryLoadingFailedPage();
|
||||
}
|
||||
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _OverviewBody extends StatelessWidget {
|
||||
const _OverviewBody({required this.details});
|
||||
|
||||
final TourDetails details;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
context.read<TourBloc>().add(const RefreshTour());
|
||||
},
|
||||
child: DeliveryOverview(details: details),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _EmptyTourBody extends StatelessWidget {
|
||||
const _EmptyTourBody();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Wenn der ERP-Sync für heute keine Tour gemeldet hat, ist das ein
|
||||
// normaler Zustand — kein Fehler. UX-Hinweis und Pull-to-refresh.
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
context.read<TourBloc>().add(const RefreshTour());
|
||||
},
|
||||
child: ListView(
|
||||
children: const [
|
||||
SizedBox(height: 120),
|
||||
Icon(Icons.event_busy, size: 64, color: Colors.grey),
|
||||
SizedBox(height: 16),
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 32),
|
||||
child: Text(
|
||||
'Für heute ist keine Tour zugewiesen.\n'
|
||||
'Zum Aktualisieren nach unten ziehen.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import 'package:collection/collection.dart';
|
||||
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/tour_details.dart';
|
||||
import 'package:hl_lieferservice/feature/cars/bloc/cars_bloc.dart';
|
||||
import 'package:hl_lieferservice/feature/cars/bloc/cars_state.dart';
|
||||
import 'package:hl_lieferservice/feature/delivery/bloc/phase_bloc.dart';
|
||||
import 'package:hl_lieferservice/feature/delivery/bloc/phase_event.dart';
|
||||
import 'package:hl_lieferservice/feature/delivery/bloc/tour_bloc.dart';
|
||||
@ -8,8 +11,6 @@ import 'package:hl_lieferservice/feature/delivery/bloc/tour_event.dart';
|
||||
import 'package:hl_lieferservice/feature/delivery/bloc/tour_state.dart';
|
||||
import 'package:hl_lieferservice/feature/delivery/model/delivery_phase.dart';
|
||||
import 'package:hl_lieferservice/feature/delivery/overview/presentation/delivery_fail_page.dart';
|
||||
import 'package:hl_lieferservice/model/delivery.dart';
|
||||
import 'package:hl_lieferservice/model/tour.dart';
|
||||
import 'package:hl_lieferservice/widget/home/presentation/home_drawer.dart';
|
||||
import 'package:hl_lieferservice/widget/phase_stepper/phase_stepper.dart';
|
||||
|
||||
@ -20,18 +21,11 @@ import 'package:hl_lieferservice/widget/phase_stepper/phase_stepper.dart';
|
||||
/// * Tab "Verfügbar" — alle Lieferungen, die noch keinem Fahrzeug
|
||||
/// zugeordnet sind. Multiselect, Bulk-Bestätigung per BottomBar-Button.
|
||||
/// * Tab "Vergeben" — Lieferungen, die bereits zugeordnet sind. Tap auf
|
||||
/// eigene Lieferung → Freigabe-Dialog; Tap auf fremde → Umlade-Dialog.
|
||||
/// * Persistent untere BottomBar: "Weiter zum Sortieren" — wechselt die
|
||||
/// Phase. Es gibt bewusst keinen Zwang, dass alle Lieferungen verteilt
|
||||
/// sein müssen; der Fahrer entscheidet wann er weiterzieht.
|
||||
/// eigene → Freigabe-Dialog; Tap auf fremde → Umlade-Dialog.
|
||||
/// * Persistente BottomBar: "Weiter zum Sortieren" — wechselt die Phase.
|
||||
class DeliverySelectionPage extends StatefulWidget {
|
||||
const DeliverySelectionPage({
|
||||
super.key,
|
||||
required this.selectedCarId,
|
||||
});
|
||||
const DeliverySelectionPage({super.key, required this.selectedCarId});
|
||||
|
||||
/// ID des aktuell gewählten Fahrzeugs (Eigene Lieferungen / Ziel von
|
||||
/// Übernahmen).
|
||||
final String selectedCarId;
|
||||
|
||||
@override
|
||||
@ -39,86 +33,63 @@ class DeliverySelectionPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _DeliverySelectionPageState extends State<DeliverySelectionPage> {
|
||||
/// Lokale Multi-Selektion im Tab "Verfügbar". Wird nach erfolgreichem
|
||||
/// Bulk-Assign geleert.
|
||||
final Set<String> _selectedIds = <String>{};
|
||||
|
||||
/// True während sequentieller Assign-Calls — schaltet den
|
||||
/// Bestätigungs-Button auf Spinner und disabled.
|
||||
bool _isAssigning = false;
|
||||
|
||||
String get _carIdString => widget.selectedCarId.toString();
|
||||
|
||||
/// Sucht das Plate eines Autos in der Tour-Driver-Liste. Liefert "?" als
|
||||
/// Fallback, falls die Zuordnung nicht (mehr) im Team enthalten ist —
|
||||
/// z. B. nach Personalwechsel zwischen Tour-Synchronisationen.
|
||||
String _plateFor(String? carId, Tour tour) {
|
||||
if (carId == null) return "?";
|
||||
final car = tour.driver.cars.firstWhereOrNull((c) => c.id == carId);
|
||||
return car?.plate ?? "?";
|
||||
/// Sucht das Kennzeichen eines Fahrzeugs in der aktuell geladenen
|
||||
/// CarsBloc-Liste. Liefert "?" als Fallback, wenn das Auto nicht (mehr)
|
||||
/// im Account ist — z. B. nach Personalwechsel zwischen Tour-Syncs.
|
||||
String _plateFor(String? carId) {
|
||||
if (carId == null) return '?';
|
||||
final carsState = context.read<CarsBloc>().state;
|
||||
if (carsState is! CarsLoaded) return '?';
|
||||
for (final c in carsState.cars) {
|
||||
if (c.id == carId) return c.plate;
|
||||
}
|
||||
return '?';
|
||||
}
|
||||
|
||||
/// Bulk-Assign der aktuell selektierten Lieferungen an das eigene Auto.
|
||||
/// Sequentiell, damit der lokale Tour-Stream nach jedem Schritt
|
||||
/// konsistent ist und die Listen-Filter live mitwandern. Bei Fehlern
|
||||
/// wird eine SnackBar gezeigt und die Selektion bleibt erhalten.
|
||||
Future<void> _confirmSelection() async {
|
||||
if (_selectedIds.isEmpty || _isAssigning) return;
|
||||
|
||||
setState(() => _isAssigning = true);
|
||||
final tourBloc = context.read<TourBloc>();
|
||||
void _confirmSelection() {
|
||||
if (_selectedIds.isEmpty) return;
|
||||
final ids = List<String>.from(_selectedIds);
|
||||
|
||||
try {
|
||||
for (final id in ids) {
|
||||
tourBloc.add(
|
||||
AssignCarEvent(deliveryId: id, carId: _carIdString),
|
||||
);
|
||||
}
|
||||
// Hinweis: TourBloc verarbeitet die Events asynchron; lokale
|
||||
// Tour-Updates erfolgen über den Stream. Wir leeren die Selektion
|
||||
// optimistisch, damit der Fahrer ein klares Feedback bekommt.
|
||||
if (!mounted) return;
|
||||
setState(_selectedIds.clear);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text("Fehler beim Übernehmen: $e")),
|
||||
);
|
||||
} finally {
|
||||
if (mounted) setState(() => _isAssigning = false);
|
||||
}
|
||||
}
|
||||
|
||||
/// Wechselt in die nächste Phase (Sortieren). Der Fahrer kann jederzeit
|
||||
/// zurückspringen — die persistierte Phase wird über den Stepper-Tap
|
||||
/// zurückgesetzt.
|
||||
void _goToSorting() {
|
||||
context.read<PhaseBloc>().add(
|
||||
PhaseSet(
|
||||
carId: _carIdString,
|
||||
phase: DeliveryPhase.sortieren,
|
||||
// EIN Bulk-Event statt N parallel laufender Single-Events: vermeidet
|
||||
// die Race-Condition, bei der `flutter_bloc`s default-concurrent
|
||||
// Event-Processing parallele Handler den Initial-State lesen lässt
|
||||
// und sich am Ende beim `emit` gegenseitig überschreiben.
|
||||
context.read<TourBloc>().add(
|
||||
AssignCarToDeliveries(
|
||||
deliveryIds: ids,
|
||||
carId: widget.selectedCarId,
|
||||
),
|
||||
);
|
||||
|
||||
setState(_selectedIds.clear);
|
||||
}
|
||||
|
||||
Future<void> _showReleaseDialog(Delivery delivery) async {
|
||||
void _goToSorting() {
|
||||
context.read<PhaseBloc>().add(
|
||||
PhaseSet(carId: widget.selectedCarId, phase: DeliveryPhase.sortieren),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showReleaseDialog(Delivery delivery, TourDetails details) async {
|
||||
final customer = details.customerOf(delivery);
|
||||
final result = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text("Lieferung freigeben"),
|
||||
title: const Text('Lieferung freigeben'),
|
||||
content: Text(
|
||||
"${delivery.customer.name} wurde Ihrem Fahrzeug zugeordnet. "
|
||||
"Möchten Sie diese Lieferung wieder freigeben?",
|
||||
'${customer?.name ?? 'Diese Lieferung'} wurde Ihrem Fahrzeug '
|
||||
'zugeordnet. Möchten Sie sie wieder freigeben?',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(false),
|
||||
child: const Text("Abbrechen"),
|
||||
child: const Text('Abbrechen'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(true),
|
||||
child: const Text("Freigeben"),
|
||||
child: const Text('Freigeben'),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -126,54 +97,57 @@ class _DeliverySelectionPageState extends State<DeliverySelectionPage> {
|
||||
|
||||
if (result != true || !mounted) return;
|
||||
context.read<TourBloc>().add(
|
||||
UnassignDeliveryEvent(deliveryId: delivery.id),
|
||||
AssignCarToDelivery(deliveryId: delivery.id, carId: null),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showTakeoverDialog(Delivery delivery, Tour tour) async {
|
||||
final foreignPlate = _plateFor(delivery.carId, tour);
|
||||
final ownPlate = _plateFor(widget.selectedCarId, tour);
|
||||
Future<void> _showTakeoverDialog(
|
||||
Delivery delivery,
|
||||
TourDetails details,
|
||||
) async {
|
||||
final customer = details.customerOf(delivery);
|
||||
final foreignPlate = _plateFor(delivery.assignedCarId);
|
||||
final ownPlate = _plateFor(widget.selectedCarId);
|
||||
final theme = Theme.of(context);
|
||||
|
||||
final result = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text("Lieferung umladen"),
|
||||
title: const Text('Lieferung umladen'),
|
||||
content: RichText(
|
||||
text: TextSpan(
|
||||
style: theme.textTheme.bodyMedium,
|
||||
children: [
|
||||
TextSpan(text: "${delivery.customer.name} ist aktuell "),
|
||||
TextSpan(
|
||||
text: '${customer?.name ?? 'Diese Lieferung'} ist aktuell ',
|
||||
),
|
||||
TextSpan(
|
||||
text: foreignPlate,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const TextSpan(text: " zugeordnet. Möchten Sie diese "
|
||||
"Lieferung auf "),
|
||||
const TextSpan(
|
||||
text: ' zugeordnet. Möchten Sie diese Lieferung auf ',
|
||||
),
|
||||
TextSpan(
|
||||
text: ownPlate,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const TextSpan(text: " umladen?"),
|
||||
const TextSpan(text: ' umladen?'),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(false),
|
||||
child: const Text("Abbrechen"),
|
||||
child: const Text('Abbrechen'),
|
||||
),
|
||||
FilledButton(
|
||||
// Warnfarbe, da das Umladen eine bestehende Zuordnung
|
||||
// überschreibt — last write wins. `colorScheme.error` ist
|
||||
// bewusst hart gewählt, damit der Fahrer den Eingriff in
|
||||
// fremde Disposition bewusst bestätigt.
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: theme.colorScheme.error,
|
||||
foregroundColor: theme.colorScheme.onError,
|
||||
),
|
||||
onPressed: () => Navigator.of(ctx).pop(true),
|
||||
child: const Text("Übernehmen"),
|
||||
child: const Text('Übernehmen'),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -181,16 +155,14 @@ class _DeliverySelectionPageState extends State<DeliverySelectionPage> {
|
||||
|
||||
if (result != true || !mounted) return;
|
||||
context.read<TourBloc>().add(
|
||||
AssignCarEvent(
|
||||
AssignCarToDelivery(
|
||||
deliveryId: delivery.id,
|
||||
carId: _carIdString,
|
||||
carId: widget.selectedCarId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Widgets
|
||||
// ---------------------------------------------------------------------------
|
||||
// ─── Widgets ─────────────────────────────────────────────────────────
|
||||
|
||||
Widget _plateBadge(BuildContext context, String plate, {bool own = false}) {
|
||||
final theme = Theme.of(context);
|
||||
@ -213,11 +185,7 @@ class _DeliverySelectionPageState extends State<DeliverySelectionPage> {
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
plate,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: fg,
|
||||
),
|
||||
style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold, color: fg),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -257,13 +225,13 @@ class _DeliverySelectionPageState extends State<DeliverySelectionPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _availableTab(List<Delivery> available) {
|
||||
Widget _availableTab(List<Delivery> available, TourDetails details) {
|
||||
if (available.isEmpty) {
|
||||
return _emptyState(
|
||||
icon: Icons.inbox_outlined,
|
||||
title: "Alle Lieferungen sind verteilt.",
|
||||
subtitle: "Im Tab \"Vergeben\" können Sie eigene Lieferungen "
|
||||
"freigeben oder fremde übernehmen.",
|
||||
title: 'Alle Lieferungen sind verteilt.',
|
||||
subtitle: 'Im Tab "Vergeben" können Sie eigene Lieferungen '
|
||||
'freigeben oder fremde übernehmen.',
|
||||
);
|
||||
}
|
||||
|
||||
@ -272,27 +240,29 @@ class _DeliverySelectionPageState extends State<DeliverySelectionPage> {
|
||||
itemCount: available.length,
|
||||
itemBuilder: (context, index) {
|
||||
final delivery = available[index];
|
||||
final customer = details.customerOf(delivery);
|
||||
final isSelected = _selectedIds.contains(delivery.id);
|
||||
return CheckboxListTile(
|
||||
key: ValueKey("available-${delivery.id}"),
|
||||
key: ValueKey('available-${delivery.id}'),
|
||||
value: isSelected,
|
||||
onChanged: _isAssigning
|
||||
? null
|
||||
: (checked) {
|
||||
setState(() {
|
||||
if (checked == true) {
|
||||
_selectedIds.add(delivery.id);
|
||||
} else {
|
||||
_selectedIds.remove(delivery.id);
|
||||
}
|
||||
});
|
||||
},
|
||||
onChanged: (checked) {
|
||||
// Während ein Bulk-Zuweisung läuft, blockiert der
|
||||
// OperationViewEnforcer die Eingabe global; ein separates
|
||||
// `_isAssigning`-Lock ist hier nicht mehr nötig.
|
||||
setState(() {
|
||||
if (checked == true) {
|
||||
_selectedIds.add(delivery.id);
|
||||
} else {
|
||||
_selectedIds.remove(delivery.id);
|
||||
}
|
||||
});
|
||||
},
|
||||
title: Text(
|
||||
delivery.customer.name,
|
||||
customer?.name ?? '⟨Unbekannter Kunde⟩',
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
subtitle: Text(
|
||||
delivery.customer.address.toString(),
|
||||
delivery.deliveryAddressSnapshot.oneLine,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
@ -301,11 +271,11 @@ class _DeliverySelectionPageState extends State<DeliverySelectionPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _assignedTab(List<Delivery> assigned, Tour tour) {
|
||||
Widget _assignedTab(List<Delivery> assigned, TourDetails details) {
|
||||
if (assigned.isEmpty) {
|
||||
return _emptyState(
|
||||
icon: Icons.local_shipping_outlined,
|
||||
title: "Noch keine Lieferungen verteilt.",
|
||||
title: 'Noch keine Lieferungen verteilt.',
|
||||
);
|
||||
}
|
||||
|
||||
@ -316,20 +286,21 @@ class _DeliverySelectionPageState extends State<DeliverySelectionPage> {
|
||||
itemCount: assigned.length,
|
||||
itemBuilder: (context, index) {
|
||||
final delivery = assigned[index];
|
||||
final isOwn = delivery.carId == widget.selectedCarId;
|
||||
final plate = _plateFor(delivery.carId, tour);
|
||||
final isOwn = delivery.assignedCarId == widget.selectedCarId;
|
||||
final plate = _plateFor(delivery.assignedCarId);
|
||||
final customer = details.customerOf(delivery);
|
||||
|
||||
return Material(
|
||||
color: isOwn
|
||||
? theme.colorScheme.primaryContainer.withValues(alpha: 0.35)
|
||||
: null,
|
||||
child: ListTile(
|
||||
key: ValueKey("assigned-${delivery.id}"),
|
||||
key: ValueKey('assigned-${delivery.id}'),
|
||||
onTap: () {
|
||||
if (isOwn) {
|
||||
_showReleaseDialog(delivery);
|
||||
_showReleaseDialog(delivery, details);
|
||||
} else {
|
||||
_showTakeoverDialog(delivery, tour);
|
||||
_showTakeoverDialog(delivery, details);
|
||||
}
|
||||
},
|
||||
leading: Icon(
|
||||
@ -339,14 +310,14 @@ class _DeliverySelectionPageState extends State<DeliverySelectionPage> {
|
||||
: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
title: Text(
|
||||
delivery.customer.name,
|
||||
customer?.name ?? '⟨Unbekannter Kunde⟩',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: isOwn ? theme.colorScheme.primary : null,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
delivery.customer.address.toString(),
|
||||
delivery.deliveryAddressSnapshot.oneLine,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
trailing: _plateBadge(context, plate, own: isOwn),
|
||||
@ -356,13 +327,12 @@ class _DeliverySelectionPageState extends State<DeliverySelectionPage> {
|
||||
);
|
||||
}
|
||||
|
||||
/// BottomBar mit Bulk-Confirm (kontextabhängig) + Phasen-Wechsel.
|
||||
/// Confirm-Button ist nur sichtbar, wenn etwas selektiert ist —
|
||||
/// "Weiter zum Sortieren" bleibt immer sichtbar.
|
||||
Widget _buildBottomBar() {
|
||||
final theme = Theme.of(context);
|
||||
final hasSelection = _selectedIds.isNotEmpty;
|
||||
|
||||
// Disabled-State und Spinner während des Bulk-Zuweisens übernimmt
|
||||
// global der `OperationViewEnforcer` (StartOperation/FinishOperation
|
||||
// aus dem TourBloc) — daher hier keine lokale Lock-Variable mehr.
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
@ -373,20 +343,10 @@ class _DeliverySelectionPageState extends State<DeliverySelectionPage> {
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton.icon(
|
||||
onPressed:
|
||||
_isAssigning ? null : _confirmSelection,
|
||||
icon: _isAssigning
|
||||
? SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: theme.colorScheme.onPrimary,
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.check),
|
||||
onPressed: _confirmSelection,
|
||||
icon: const Icon(Icons.check),
|
||||
label: Text(
|
||||
"Auswahl bestätigen (${_selectedIds.length})",
|
||||
'Auswahl bestätigen (${_selectedIds.length})',
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -394,9 +354,9 @@ class _DeliverySelectionPageState extends State<DeliverySelectionPage> {
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: _isAssigning ? null : _goToSorting,
|
||||
onPressed: _goToSorting,
|
||||
icon: const Icon(Icons.arrow_forward),
|
||||
label: const Text("Weiter zum Sortieren"),
|
||||
label: const Text('Weiter zum Sortieren'),
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -409,24 +369,38 @@ class _DeliverySelectionPageState extends State<DeliverySelectionPage> {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<TourBloc, TourState>(
|
||||
builder: (context, state) {
|
||||
if (state is TourLoadingFailed) {
|
||||
if (state is TourLoadFailed) {
|
||||
return const DeliveryLoadingFailedPage();
|
||||
}
|
||||
if (state is TourEmpty) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Lieferungen auswählen')),
|
||||
body: const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(24),
|
||||
child: Text(
|
||||
'Für heute ist keine Tour zugewiesen.',
|
||||
style: TextStyle(fontSize: 16),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (state is! TourLoaded) {
|
||||
return const Scaffold(
|
||||
body: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
final available = state.tour.deliveries
|
||||
.where((d) => d.carId == null)
|
||||
final details = state.details;
|
||||
final available = details.deliveries
|
||||
.where((d) => d.assignedCarId == null)
|
||||
.toList();
|
||||
final assigned = state.tour.deliveries
|
||||
.where((d) => d.carId != null)
|
||||
final assigned = details.deliveries
|
||||
.where((d) => d.assignedCarId != null)
|
||||
.toList();
|
||||
|
||||
// Falls eine selektierte Lieferung in der Zwischenzeit zugeordnet
|
||||
// wurde (z. B. durch einen parallelen Vorgang), Selektion bereinigen.
|
||||
_selectedIds.removeWhere(
|
||||
(id) => !available.any((d) => d.id == id),
|
||||
);
|
||||
@ -442,22 +416,20 @@ class _DeliverySelectionPageState extends State<DeliverySelectionPage> {
|
||||
children: [
|
||||
PhaseStepper(
|
||||
currentPhase: DeliveryPhase.auswaehlen,
|
||||
carId: _carIdString,
|
||||
carId: widget.selectedCarId,
|
||||
),
|
||||
Material(
|
||||
color: Theme.of(context).primaryColor,
|
||||
child: TabBar(
|
||||
labelColor:
|
||||
Theme.of(context).colorScheme.onPrimary,
|
||||
labelColor: Theme.of(context).colorScheme.onPrimary,
|
||||
unselectedLabelColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimary
|
||||
.withValues(alpha: 0.6),
|
||||
indicatorColor:
|
||||
Theme.of(context).colorScheme.onPrimary,
|
||||
indicatorColor: Theme.of(context).colorScheme.onPrimary,
|
||||
tabs: [
|
||||
Tab(text: "Verfügbar (${available.length})"),
|
||||
Tab(text: "Vergeben (${assigned.length})"),
|
||||
Tab(text: 'Verfügbar (${available.length})'),
|
||||
Tab(text: 'Vergeben (${assigned.length})'),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -466,8 +438,8 @@ class _DeliverySelectionPageState extends State<DeliverySelectionPage> {
|
||||
),
|
||||
body: TabBarView(
|
||||
children: [
|
||||
_availableTab(available),
|
||||
_assignedTab(assigned, state.tour),
|
||||
_availableTab(available, details),
|
||||
_assignedTab(assigned, details),
|
||||
],
|
||||
),
|
||||
bottomNavigationBar: _buildBottomBar(),
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
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/tour_details.dart';
|
||||
import 'package:hl_lieferservice/feature/cars/bloc/cars_bloc.dart';
|
||||
import 'package:hl_lieferservice/feature/cars/bloc/cars_state.dart';
|
||||
import 'package:hl_lieferservice/feature/delivery/bloc/phase_bloc.dart';
|
||||
import 'package:hl_lieferservice/feature/delivery/bloc/phase_event.dart';
|
||||
import 'package:hl_lieferservice/feature/delivery/bloc/tour_bloc.dart';
|
||||
@ -12,9 +16,15 @@ import 'package:hl_lieferservice/widget/phase_stepper/phase_stepper.dart';
|
||||
|
||||
/// Page für die zweite Phase des Lieferprozesses (Sortieren). Der Fahrer
|
||||
/// legt per Drag&Drop die Reihenfolge fest, ändert lokal so oft er möchte
|
||||
/// und bestätigt am Ende mit einem expliziten Klick. Erst dann wird die
|
||||
/// und bestätigt am Ende mit einem expliziten Klick. Dann wird die
|
||||
/// Reihenfolge ans Backend übertragen und in Phase [DeliveryPhase.beladen]
|
||||
/// gewechselt.
|
||||
///
|
||||
/// Hinweis zum Backend-Endpoint:
|
||||
/// `PUT /tours/{id}/delivery-order` erwartet die **vollständige** Reihenfolge
|
||||
/// aller Lieferungen der Tour. Bei Mehr-Auto-Teams sortiert der Fahrer
|
||||
/// trotzdem nur „seine" Lieferungen — die Reihenfolge der fremden Lieferungen
|
||||
/// wird unverändert anhand der aktuellen `sortOrder` mitgeschickt.
|
||||
class DeliverySortPage extends StatefulWidget {
|
||||
const DeliverySortPage({
|
||||
super.key,
|
||||
@ -23,9 +33,6 @@ class DeliverySortPage extends StatefulWidget {
|
||||
});
|
||||
|
||||
final String selectedCarId;
|
||||
|
||||
/// Optionaler Hook, damit die übergeordnete Routing-Stelle nach Erfolg
|
||||
/// auf die nächste Phase wechseln kann (rerender der Beladungs-Page).
|
||||
final VoidCallback? onPhaseAdvanced;
|
||||
|
||||
@override
|
||||
@ -33,82 +40,88 @@ class DeliverySortPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _DeliverySortPageState extends State<DeliverySortPage> {
|
||||
late final SortableDeliveryListController _listController;
|
||||
final SortableDeliveryListController _listController =
|
||||
SortableDeliveryListController();
|
||||
|
||||
/// Verhindert mehrfache SnackBars für denselben Fehler-State.
|
||||
String? _lastShownErrorSignature;
|
||||
|
||||
/// Letzter Tour-"Fingerabdruck" (Anzahl + erste/letzte ID), zu dem wir den
|
||||
/// Sortier-Bucket des aktuellen Autos bereits konsistent gemacht haben.
|
||||
/// Verhindert unnötige Event-Stürme, wenn der TourBloc häufig rebuildet.
|
||||
String? _lastEnsuredTourSignature;
|
||||
/// True, sobald `_confirm` gefeuert wurde — wir nutzen das, um den
|
||||
/// Übergang persisting → fertig zuverlässig zu erkennen.
|
||||
bool _isAwaitingConfirm = false;
|
||||
|
||||
/// Trackt, ob im letzten Listener-Tick `isPersistingSorting` true war —
|
||||
/// damit wir den Übergang persisting → fertig zuverlässig erkennen,
|
||||
/// auch wenn der Listener für Bucket-Maintenance bereits zwischendurch
|
||||
/// gefeuert hat.
|
||||
bool _wasPersisting = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_listController = SortableDeliveryListController();
|
||||
// Falls Tour bereits geladen ist: Sortier-Bucket sofort konsistent
|
||||
// machen. Sonst übernimmt das der BlocConsumer-Listener beim ersten
|
||||
// TourLoaded.
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
_ensureBucketIfNeeded(context.read<TourBloc>().state);
|
||||
});
|
||||
bool _multiCarTeam(BuildContext context) {
|
||||
final carsState = context.read<CarsBloc>().state;
|
||||
return carsState is CarsLoaded && carsState.cars.length >= 2;
|
||||
}
|
||||
|
||||
String _tourSignature(TourLoaded state) {
|
||||
final ids = state.tour.deliveries.map((d) => d.id).toList();
|
||||
if (ids.isEmpty) return "0";
|
||||
return "${ids.length}|${ids.first}|${ids.last}";
|
||||
List<Delivery> _ownDeliveries(TourDetails details) {
|
||||
if (_multiCarTeam(context)) {
|
||||
return details.deliveriesSorted
|
||||
.where((d) => d.assignedCarId == widget.selectedCarId)
|
||||
.toList();
|
||||
}
|
||||
return details.deliveriesSorted;
|
||||
}
|
||||
|
||||
void _ensureBucketIfNeeded(TourState state) {
|
||||
if (state is! TourLoaded) return;
|
||||
final signature = _tourSignature(state);
|
||||
if (signature == _lastEnsuredTourSignature) return;
|
||||
_lastEnsuredTourSignature = signature;
|
||||
context.read<TourBloc>().add(
|
||||
EnsureSortingForCarEvent(carId: widget.selectedCarId.toString()),
|
||||
);
|
||||
/// Baut die vollständige Tour-Reihenfolge: die fremden Lieferungen bleiben
|
||||
/// in ihrer aktuellen `sortOrder`, die eigenen werden in der vom Fahrer
|
||||
/// gewählten Reihenfolge eingefügt — an den Positionen, an denen sie
|
||||
/// vorher waren. Beispiel: hat der Fahrer „eigene" Plätze 1, 3, 5
|
||||
/// belegt und sortiert lokal um, dann landen seine neuen Reihen-Ids
|
||||
/// in derselben Reihenfolge auf den Positionen 1, 3, 5; fremde Items
|
||||
/// behalten ihre Plätze 2, 4.
|
||||
List<String> _buildFullTourOrder(
|
||||
TourDetails details,
|
||||
List<String> ownOrderedIds,
|
||||
) {
|
||||
final sorted = details.deliveriesSorted;
|
||||
if (!_multiCarTeam(context)) return ownOrderedIds;
|
||||
|
||||
final ownSlotsByPosition = <int, String>{};
|
||||
final foreignByPosition = <int, String>{};
|
||||
for (var i = 0; i < sorted.length; i++) {
|
||||
final d = sorted[i];
|
||||
if (d.assignedCarId == widget.selectedCarId) {
|
||||
ownSlotsByPosition[i] = d.id;
|
||||
} else {
|
||||
foreignByPosition[i] = d.id;
|
||||
}
|
||||
}
|
||||
final ownPositions = ownSlotsByPosition.keys.toList()..sort();
|
||||
|
||||
final result = List<String?>.filled(sorted.length, null);
|
||||
for (final entry in foreignByPosition.entries) {
|
||||
result[entry.key] = entry.value;
|
||||
}
|
||||
for (var i = 0; i < ownPositions.length && i < ownOrderedIds.length; i++) {
|
||||
result[ownPositions[i]] = ownOrderedIds[i];
|
||||
}
|
||||
// Schließe verbleibende Lücken auf — sollte normalerweise leer sein.
|
||||
return result.whereType<String>().toList();
|
||||
}
|
||||
|
||||
List<String> _orderedIdsFor(TourLoaded state) {
|
||||
return state.sortingInformation[widget.selectedCarId.toString()] ??
|
||||
const <String>[];
|
||||
}
|
||||
|
||||
/// Phase im zentralen [PhaseBloc] auf "beladen" setzen. Der Bloc kümmert
|
||||
/// sich um Persistenz via [PhaseService]. Der optionale UI-Callback wird
|
||||
/// (aus Rückwärtskompatibilität) zusätzlich gefeuert.
|
||||
void _advanceToLoading() {
|
||||
context.read<PhaseBloc>().add(
|
||||
PhaseSet(
|
||||
carId: widget.selectedCarId.toString(),
|
||||
carId: widget.selectedCarId,
|
||||
phase: DeliveryPhase.beladen,
|
||||
),
|
||||
);
|
||||
widget.onPhaseAdvanced?.call();
|
||||
}
|
||||
|
||||
void _skipEmptyToLoading() => _advanceToLoading();
|
||||
|
||||
void _confirm() {
|
||||
// _wasPersisting hier setzen — der erste Listener-Tick mit
|
||||
// isPersistingSorting=true wird von listenWhen herausgefiltert
|
||||
// (kein "phaseEnded"-Übergang), daher müssen wir das Flag vorher
|
||||
// selbst hochziehen. Sonst erkennt der Listener beim zweiten Tick
|
||||
// (isPersistingSorting=false) den Übergang nicht und der
|
||||
// Phasen-Wechsel auf "beladen" bleibt aus.
|
||||
_wasPersisting = true;
|
||||
void _confirm(TourDetails details) {
|
||||
final ownOrder = _listController.readCurrentOrder();
|
||||
final fullOrder = _buildFullTourOrder(details, ownOrder);
|
||||
if (fullOrder.isEmpty) {
|
||||
_advanceToLoading();
|
||||
return;
|
||||
}
|
||||
_isAwaitingConfirm = true;
|
||||
context.read<TourBloc>().add(
|
||||
ConfirmSortingEvent(carId: widget.selectedCarId.toString()),
|
||||
);
|
||||
ReorderDeliveries(orderedDeliveryIds: fullOrder),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _hintCard({required IconData icon, required String text, Color? color}) {
|
||||
@ -133,14 +146,14 @@ class _DeliverySortPageState extends State<DeliverySortPage> {
|
||||
const Icon(Icons.inbox_outlined, size: 64, color: Colors.grey),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
"Keine Lieferungen heute",
|
||||
'Keine Lieferungen heute',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 32),
|
||||
child: Text(
|
||||
"Für das ausgewählte Fahrzeug sind heute keine Lieferungen geplant.",
|
||||
'Für das ausgewählte Fahrzeug sind heute keine Lieferungen geplant.',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
@ -148,33 +161,29 @@ class _DeliverySortPageState extends State<DeliverySortPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _singleDeliveryHint(String singleId, TourLoaded state) {
|
||||
final delivery = state.tour.deliveries.firstWhere(
|
||||
(d) => d.id == singleId,
|
||||
orElse: () => state.tour.deliveries.first,
|
||||
);
|
||||
Widget _singleDeliveryHint(Delivery single, TourDetails details) {
|
||||
final customer = details.customerOf(single);
|
||||
return Column(
|
||||
children: [
|
||||
_hintCard(
|
||||
icon: Icons.info_outline,
|
||||
text:
|
||||
"Nur eine Lieferung — die Reihenfolge ist trivial. "
|
||||
"Tippen Sie auf \"Weiter zur Beladung\", um fortzufahren.",
|
||||
text: 'Nur eine Lieferung — die Reihenfolge ist trivial. '
|
||||
'Tippen Sie auf "Weiter zur Beladung", um fortzufahren.',
|
||||
),
|
||||
const Divider(),
|
||||
ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
child: Text(
|
||||
"1",
|
||||
'1',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
title: Text(delivery.customer.name),
|
||||
title: Text(customer?.name ?? '⟨Unbekannter Kunde⟩'),
|
||||
subtitle: Text(
|
||||
delivery.customer.address.toString(),
|
||||
single.deliveryAddressSnapshot.oneLine,
|
||||
style: const TextStyle(fontSize: 11),
|
||||
),
|
||||
),
|
||||
@ -186,55 +195,48 @@ class _DeliverySortPageState extends State<DeliverySortPage> {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocConsumer<TourBloc, TourState>(
|
||||
listenWhen: (prev, curr) {
|
||||
// Bucket-Maintenance: erstes TourLoaded oder Tour-Inhalt geändert.
|
||||
final bucketTrigger = (prev is! TourLoaded && curr is TourLoaded) ||
|
||||
(prev is TourLoaded &&
|
||||
curr is TourLoaded &&
|
||||
prev.tour.deliveries.length != curr.tour.deliveries.length);
|
||||
if (bucketTrigger) return true;
|
||||
|
||||
if (prev is! TourLoaded || curr is! TourLoaded) return false;
|
||||
final phaseEnded =
|
||||
prev.isPersistingSorting && !curr.isPersistingSorting;
|
||||
final newError =
|
||||
curr.sortingPersistError != null &&
|
||||
curr.sortingPersistError != prev.sortingPersistError;
|
||||
prev.isPersistingReorder && !curr.isPersistingReorder;
|
||||
final newError = curr.reorderError != null &&
|
||||
curr.reorderError != prev.reorderError;
|
||||
return phaseEnded || newError;
|
||||
},
|
||||
listener: (context, state) {
|
||||
if (state is! TourLoaded) return;
|
||||
|
||||
// 1) Bucket-Konsistenz nachziehen, falls die Tour gerade erst geladen
|
||||
// wurde oder sich verändert hat.
|
||||
_ensureBucketIfNeeded(state);
|
||||
|
||||
// 2) Fehler-SnackBar für Confirm-Fehler.
|
||||
final err = state.sortingPersistError;
|
||||
final err = state.reorderError;
|
||||
if (err != null && err != _lastShownErrorSignature) {
|
||||
_lastShownErrorSignature = err;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(err)),
|
||||
);
|
||||
_wasPersisting = state.isPersistingSorting;
|
||||
_isAwaitingConfirm = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// 3) Echter Übergang persisting → fertig + kein Fehler → erfolgreich
|
||||
// bestätigt → nächste Phase. Übergang über das Feld _wasPersisting
|
||||
// erkannt, da der Listener auch bei Bucket-Triggern feuert.
|
||||
if (_wasPersisting && !state.isPersistingSorting && err == null) {
|
||||
if (_isAwaitingConfirm &&
|
||||
!state.isPersistingReorder &&
|
||||
err == null) {
|
||||
_isAwaitingConfirm = false;
|
||||
_advanceToLoading();
|
||||
}
|
||||
_wasPersisting = state.isPersistingSorting;
|
||||
},
|
||||
builder: (context, state) {
|
||||
if (state is TourEmpty) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Sortieren')),
|
||||
body: _emptyState(),
|
||||
);
|
||||
}
|
||||
if (state is! TourLoaded) {
|
||||
return const Scaffold(
|
||||
body: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
final orderedIds = _orderedIdsFor(state);
|
||||
final details = state.details;
|
||||
final ownDeliveries = _ownDeliveries(details);
|
||||
|
||||
return Scaffold(
|
||||
drawer: const HomeAppDrawer(),
|
||||
@ -242,40 +244,41 @@ class _DeliverySortPageState extends State<DeliverySortPage> {
|
||||
preferredSize: const Size.fromHeight(140),
|
||||
child: PhaseStepper(
|
||||
currentPhase: DeliveryPhase.sortieren,
|
||||
carId: widget.selectedCarId.toString(),
|
||||
carId: widget.selectedCarId,
|
||||
),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: _buildBody(state, orderedIds),
|
||||
),
|
||||
bottomNavigationBar: _buildBottomBar(state, orderedIds),
|
||||
body: SafeArea(child: _buildBody(state, details, ownDeliveries)),
|
||||
bottomNavigationBar:
|
||||
_buildBottomBar(state, details, ownDeliveries),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody(TourLoaded state, List<String> orderedIds) {
|
||||
if (orderedIds.isEmpty) {
|
||||
Widget _buildBody(
|
||||
TourLoaded state,
|
||||
TourDetails details,
|
||||
List<Delivery> ownDeliveries,
|
||||
) {
|
||||
if (ownDeliveries.isEmpty) {
|
||||
return _emptyState();
|
||||
}
|
||||
|
||||
if (orderedIds.length == 1) {
|
||||
return _singleDeliveryHint(orderedIds.first, state);
|
||||
if (ownDeliveries.length == 1) {
|
||||
return _singleDeliveryHint(ownDeliveries.first, details);
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
_hintCard(
|
||||
icon: Icons.info_outline,
|
||||
text:
|
||||
"Ziehen Sie die einzelnen Lieferungen mit dem Finger in die "
|
||||
"gewünschte Position. Die Reihenfolge wird erst beim "
|
||||
"Bestätigen ans System übertragen.",
|
||||
text: 'Ziehen Sie die einzelnen Lieferungen mit dem Finger in die '
|
||||
'gewünschte Position. Die Reihenfolge wird erst beim '
|
||||
'Bestätigen ans System übertragen.',
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
child: SortableDeliveryList(
|
||||
selectedCarId: widget.selectedCarId,
|
||||
details: details,
|
||||
deliveries: ownDeliveries,
|
||||
controller: _listController,
|
||||
),
|
||||
),
|
||||
@ -283,35 +286,38 @@ class _DeliverySortPageState extends State<DeliverySortPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottomBar(TourLoaded state, List<String> orderedIds) {
|
||||
final isLoading = state.isPersistingSorting;
|
||||
Widget _buildBottomBar(
|
||||
TourLoaded state,
|
||||
TourDetails details,
|
||||
List<Delivery> ownDeliveries,
|
||||
) {
|
||||
final isLoading = state.isPersistingReorder;
|
||||
final theme = Theme.of(context);
|
||||
|
||||
// Spezialfälle: 0 / 1 Lieferungen → vereinfachte BottomBar
|
||||
if (orderedIds.isEmpty) {
|
||||
if (ownDeliveries.isEmpty) {
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton.icon(
|
||||
onPressed: isLoading ? null : _skipEmptyToLoading,
|
||||
onPressed: isLoading ? null : _advanceToLoading,
|
||||
icon: const Icon(Icons.arrow_forward),
|
||||
label: const Text("Weiter zur Beladung"),
|
||||
label: const Text('Weiter zur Beladung'),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (orderedIds.length == 1) {
|
||||
if (ownDeliveries.length == 1) {
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton.icon(
|
||||
onPressed: isLoading ? null : _confirm,
|
||||
onPressed: isLoading ? null : () => _confirm(details),
|
||||
icon: isLoading
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
@ -322,7 +328,7 @@ class _DeliverySortPageState extends State<DeliverySortPage> {
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.arrow_forward),
|
||||
label: const Text("Weiter zur Beladung"),
|
||||
label: const Text('Weiter zur Beladung'),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -336,18 +342,16 @@ class _DeliverySortPageState extends State<DeliverySortPage> {
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: isLoading
|
||||
? null
|
||||
: () => _listController.resetToDefault(),
|
||||
onPressed: isLoading ? null : _listController.resetToDefault,
|
||||
icon: const Icon(Icons.restart_alt),
|
||||
label: const Text("Zurücksetzen"),
|
||||
label: const Text('Zurücksetzen'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: FilledButton.icon(
|
||||
onPressed: isLoading ? null : _confirm,
|
||||
onPressed: isLoading ? null : () => _confirm(details),
|
||||
icon: isLoading
|
||||
? SizedBox(
|
||||
width: 16,
|
||||
@ -358,7 +362,7 @@ class _DeliverySortPageState extends State<DeliverySortPage> {
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.check),
|
||||
label: const Text("Reihenfolge bestätigen"),
|
||||
label: const Text('Reihenfolge bestätigen'),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user