Final commit.

This commit is contained in:
Dennis Nemec
2026-06-01 17:12:28 +02:00
parent 3ecbc82885
commit a9bf8ecdd1
385 changed files with 29081 additions and 12089 deletions

View File

@ -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>[];
}