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)
This commit is contained in:
Dennis Nemec
2026-05-14 22:27:56 +02:00
parent ac6b03227d
commit 456fb59668
29 changed files with 5425 additions and 1015 deletions

View File

@ -0,0 +1,178 @@
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';
/// 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.
class SortableDeliveryList extends StatefulWidget {
const SortableDeliveryList({
super.key,
required this.selectedCarId,
this.controller,
});
final int? selectedCarId;
/// Optionaler Controller zum Zurücksetzen der Liste durch Eltern-Widgets
/// (z. B. Button "Zurücksetzen" in der Page).
final SortableDeliveryListController? controller;
@override
State<StatefulWidget> createState() => _SortableDeliveryListState();
}
class _SortableDeliveryListState extends State<SortableDeliveryList> {
late List<String> _localSortedList;
@override
void initState() {
super.initState();
_localSortedList = _readSortedListFromBloc();
widget.controller?._attach(this);
}
@override
void didUpdateWidget(covariant SortableDeliveryList oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.controller != widget.controller) {
oldWidget.controller?._detach(this);
widget.controller?._attach(this);
}
}
@override
void dispose() {
widget.controller?._detach(this);
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];
});
final container = {
...state.sortingInformation,
carIdStr: [...defaultOrder],
};
context.read<TourBloc>().add(
ReplaceSortingEvent(
carId: carIdStr,
newSortingInformation: container,
),
);
}
@override
Widget build(BuildContext context) {
return BlocBuilder<TourBloc, TourState>(
builder: (context, state) {
if (state is! TourLoaded) {
return const Center(child: CircularProgressIndicator());
}
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(),
);
},
);
}
}
/// Schmaler Controller, mit dem Eltern-Widgets die Liste zurücksetzen
/// können, ohne den internen State direkt anzufassen.
class SortableDeliveryListController {
_SortableDeliveryListState? _state;
void _attach(_SortableDeliveryListState state) => _state = state;
void _detach(_SortableDeliveryListState state) {
if (_state == state) _state = null;
}
/// Setzt die Liste auf die Default-Reihenfolge (Tour-Reihenfolge) zurück.
void resetToDefault() => _state?._resetToDefault();
}