Final commit.
This commit is contained in:
@ -1,30 +1,30 @@
|
||||
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';
|
||||
import 'package:hl_lieferservice/domain/entity/delivery.dart';
|
||||
import 'package:hl_lieferservice/domain/entity/tour_details.dart';
|
||||
|
||||
/// Drag&Drop-Liste der heutigen Lieferungen eines Fahrzeugs.
|
||||
///
|
||||
/// Hält die aktuell sichtbare Reihenfolge in lokalem State, damit das
|
||||
/// Verschieben spürbar unmittelbar wirkt — und gibt jeden Drop zusätzlich
|
||||
/// als [ReorderDeliveryEvent] an den [TourBloc] weiter. Dort übernimmt
|
||||
/// der [ReorderService] die lokale Persistenz. Es findet hier bewusst
|
||||
/// **kein** API-Call statt; der Backend-Sync läuft erst, wenn der Fahrer
|
||||
/// die Reihenfolge in der übergeordneten Page bestätigt.
|
||||
/// Hält die aktuell sichtbare Reihenfolge in lokalem State; ein Tour-Reload
|
||||
/// von außen würde sie überschreiben — das ist Absicht, damit der Backend-
|
||||
/// Stand bei Pull-to-refresh durchschlägt. Der eigentliche Backend-Sync
|
||||
/// erfolgt erst, wenn der Fahrer in der übergeordneten Page bestätigt.
|
||||
class SortableDeliveryList extends StatefulWidget {
|
||||
const SortableDeliveryList({
|
||||
super.key,
|
||||
required this.selectedCarId,
|
||||
required this.details,
|
||||
required this.deliveries,
|
||||
this.controller,
|
||||
});
|
||||
|
||||
final String? selectedCarId;
|
||||
/// Aggregat-Snapshot — wird für Kunden-Lookup gebraucht.
|
||||
final TourDetails details;
|
||||
|
||||
/// Optionaler Controller zum Zurücksetzen der Liste durch Eltern-Widgets
|
||||
/// (z. B. Button "Zurücksetzen" in der Page).
|
||||
/// Die in der Liste anzuzeigenden Lieferungen, vorgefiltert vom Aufrufer
|
||||
/// (z. B. nur die dem ausgewählten Fahrzeug zugewiesenen).
|
||||
final List<Delivery> deliveries;
|
||||
|
||||
/// Optionaler Controller zum Auslesen der aktuellen Reihenfolge und zum
|
||||
/// Zurücksetzen durch Eltern-Widgets.
|
||||
final SortableDeliveryListController? controller;
|
||||
|
||||
@override
|
||||
@ -32,12 +32,12 @@ class SortableDeliveryList extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _SortableDeliveryListState extends State<SortableDeliveryList> {
|
||||
late List<String> _localSortedList;
|
||||
late List<String> _orderedIds;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_localSortedList = _readSortedListFromBloc();
|
||||
_orderedIds = widget.deliveries.map((d) => d.id).toList(growable: true);
|
||||
widget.controller?._attach(this);
|
||||
}
|
||||
|
||||
@ -48,6 +48,17 @@ class _SortableDeliveryListState extends State<SortableDeliveryList> {
|
||||
oldWidget.controller?._detach(this);
|
||||
widget.controller?._attach(this);
|
||||
}
|
||||
// Wenn sich die Eingangsliste fundamental ändert (Tour-Reload, neue
|
||||
// Lieferung hinzu/weg), local-state neu synchronisieren.
|
||||
final incomingIds = widget.deliveries.map((d) => d.id).toSet();
|
||||
final localIds = _orderedIds.toSet();
|
||||
if (incomingIds.length != localIds.length ||
|
||||
!incomingIds.containsAll(localIds)) {
|
||||
setState(() {
|
||||
_orderedIds =
|
||||
widget.deliveries.map((d) => d.id).toList(growable: true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@ -56,115 +67,64 @@ class _SortableDeliveryListState extends State<SortableDeliveryList> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
List<String> _readSortedListFromBloc() {
|
||||
final state = context.read<TourBloc>().state;
|
||||
if (state is TourLoaded) {
|
||||
return [
|
||||
...state.sortingInformation[widget.selectedCarId.toString()] ?? [],
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/// Setzt die Liste auf die natürliche Reihenfolge zurück, in der die
|
||||
/// Lieferungen in der Tour stehen. Wird vom Controller (Button
|
||||
/// "Zurücksetzen") aufgerufen und meldet jeden notwendigen Swap als
|
||||
/// Reorder-Event, damit der lokale Persistenz-State synchron bleibt.
|
||||
///
|
||||
/// Filterlogik (muss konsistent zu `_ensureSortingForCar` im TourBloc
|
||||
/// sein):
|
||||
/// * Ein-Auto-Teams: alle Tour-Lieferungen.
|
||||
/// * Mehr-Auto-Teams: nur Lieferungen, die dem ausgewählten Fahrzeug
|
||||
/// nach der Auswahl bereits zugeordnet sind.
|
||||
void _resetToDefault() {
|
||||
final state = context.read<TourBloc>().state;
|
||||
if (state is! TourLoaded) return;
|
||||
|
||||
final cars = state.tour.driver.cars;
|
||||
final carIdStr = widget.selectedCarId.toString();
|
||||
final List<String> defaultOrder = cars.length >= 2
|
||||
? state.tour.deliveries
|
||||
.where((d) => d.carId?.toString() == carIdStr)
|
||||
.map((d) => d.id)
|
||||
.toList()
|
||||
: state.tour.deliveries.map((d) => d.id).toList();
|
||||
|
||||
setState(() {
|
||||
_localSortedList = [...defaultOrder];
|
||||
_orderedIds =
|
||||
widget.deliveries.map((d) => d.id).toList(growable: true);
|
||||
});
|
||||
|
||||
final container = {
|
||||
...state.sortingInformation,
|
||||
carIdStr: [...defaultOrder],
|
||||
};
|
||||
context.read<TourBloc>().add(
|
||||
ReplaceSortingEvent(
|
||||
carId: carIdStr,
|
||||
newSortingInformation: container,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<String> _readCurrentOrder() => List<String>.of(_orderedIds);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<TourBloc, TourState>(
|
||||
builder: (context, state) {
|
||||
if (state is! TourLoaded) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
final byId = {for (final d in widget.deliveries) d.id: d};
|
||||
|
||||
return ReorderableListView(
|
||||
buildDefaultDragHandles: true,
|
||||
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) {
|
||||
final Delivery delivery = state.tour.deliveries.firstWhere(
|
||||
(delivery) => delivery.id == id,
|
||||
);
|
||||
final int pos = _localSortedList.indexOf(id) + 1;
|
||||
|
||||
return ListTile(
|
||||
key: Key("reorder-item-${delivery.id}"),
|
||||
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: const TextStyle(fontSize: 11),
|
||||
),
|
||||
trailing: const Icon(Icons.drag_handle),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
return ReorderableListView(
|
||||
buildDefaultDragHandles: true,
|
||||
onReorder: (oldIndex, newIndex) {
|
||||
setState(() {
|
||||
if (newIndex > oldIndex) newIndex -= 1;
|
||||
final id = _orderedIds.removeAt(oldIndex);
|
||||
_orderedIds.insert(newIndex, id);
|
||||
});
|
||||
},
|
||||
children: _orderedIds.asMap().entries.map((entry) {
|
||||
final id = entry.value;
|
||||
final pos = entry.key + 1;
|
||||
final delivery = byId[id];
|
||||
if (delivery == null) {
|
||||
return ListTile(
|
||||
key: Key('reorder-item-orphan-$id'),
|
||||
title: Text('Lieferung $id nicht mehr in der Tour'),
|
||||
);
|
||||
}
|
||||
final customer = widget.details.customerOf(delivery);
|
||||
return ListTile(
|
||||
key: Key('reorder-item-${delivery.id}'),
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
child: Text(
|
||||
'$pos',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
title: Text(customer?.name ?? '⟨Unbekannter Kunde⟩'),
|
||||
subtitle: Text(
|
||||
delivery.deliveryAddressSnapshot.oneLine,
|
||||
style: const TextStyle(fontSize: 11),
|
||||
),
|
||||
trailing: const Icon(Icons.drag_handle),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Schmaler Controller, mit dem Eltern-Widgets die Liste zurücksetzen
|
||||
/// können, ohne den internen State direkt anzufassen.
|
||||
/// und die aktuelle Reihenfolge auslesen können.
|
||||
class SortableDeliveryListController {
|
||||
_SortableDeliveryListState? _state;
|
||||
|
||||
@ -173,6 +133,11 @@ class SortableDeliveryListController {
|
||||
if (_state == state) _state = null;
|
||||
}
|
||||
|
||||
/// Setzt die Liste auf die Default-Reihenfolge (Tour-Reihenfolge) zurück.
|
||||
/// Setzt die Liste auf die vom Aufrufer übergebene Default-Reihenfolge
|
||||
/// zurück (= aktueller `widget.deliveries`-Stand).
|
||||
void resetToDefault() => _state?._resetToDefault();
|
||||
|
||||
/// Aktuelle Reihenfolge der Delivery-IDs, wie sie der Fahrer sieht.
|
||||
List<String> readCurrentOrder() =>
|
||||
_state?._readCurrentOrder() ?? const <String>[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user