_ensureSingleCarAssignment lief nur EINMAL pro Session (bool-Guard je Auto).
Eine nach dem ersten Auto-Assign gesyncte Lieferung (Reload/Pull-to-Refresh)
bekam daher kein assignedCarId mehr und erschien erst nach App-Neustart in
der Auslieferung. Jetzt wird jede (auch neu dazugekommene) noch nicht
zugeordnete Lieferung genau einmal zugewiesen (Set dispatchter Ids statt
bool), ohne die laufende Zuweisung bei jedem Build zu respammen.
Ergänzt den Anzeige-Fix 30fdde1 (Auslieferung == Beladen-Filter).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
218 lines
9.3 KiB
Dart
218 lines
9.3 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_bloc/flutter_bloc.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/cars/bloc/cars_bloc.dart';
|
|
import 'package:hl_lieferservice/feature/cars/bloc/cars_event.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/phase_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_overview_page.dart';
|
|
import 'package:hl_lieferservice/feature/delivery/overview/presentation/delivery_selection_page.dart';
|
|
import 'package:hl_lieferservice/feature/delivery/overview/presentation/delivery_sort_page.dart';
|
|
import 'package:hl_lieferservice/feature/loading/presentation/loading_overview_page.dart';
|
|
|
|
/// Wurzel-Widget des authentifizierten Bereichs. Routet anhand der aktuellen
|
|
/// Phase des ausgewählten Fahrzeugs:
|
|
///
|
|
/// * Phase Sortieren / Beladen → die jeweilige Phase-Page wird direkt
|
|
/// gerendert (kein BottomNav). Navigation läuft über den Phasen-Stepper.
|
|
/// * Phase Ausliefern → klassisches Home mit BottomNavigationBar
|
|
/// (Auslieferung / Fahrzeuge / Einstellungen). Beladung als Tab entfällt,
|
|
/// da die Phase abgeschlossen ist.
|
|
class Home extends StatefulWidget {
|
|
const Home({super.key});
|
|
|
|
@override
|
|
State<StatefulWidget> createState() => _HomeState();
|
|
}
|
|
|
|
class _HomeState extends State<Home> {
|
|
String? _initializedCarId;
|
|
|
|
/// Merkt, für welches Auto die automatische Lieferungs-Zuordnung bereits
|
|
/// Auto, für das zuletzt automatisch zugewiesen wurde (Ein-Auto-Teams).
|
|
String? _autoAssignCar;
|
|
|
|
/// Lieferungs-Ids, für die die Auto-Zuweisung bereits ausgelöst wurde. So
|
|
/// werden **neu gesyncte** Lieferungen (die beim Reload dazukommen) NACH-
|
|
/// träglich zugewiesen, ohne dass eine noch laufende Zuweisung (bis der
|
|
/// Server antwortet und `assignedCarId` gesetzt ist) bei jedem Build
|
|
/// respammt wird. (s. [_ensureSingleCarAssignment])
|
|
final Set<String> _autoAssignDispatched = {};
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
|
|
// Tour beim ersten Aufbau laden. Account-Filter sitzt jetzt
|
|
// serverseitig im JWT — kein Personalnummer-Argument mehr nötig.
|
|
context.read<TourBloc>().add(const LoadTour());
|
|
|
|
// CarsBloc auch hier triggern: wenn der CarSelectBloc beim App-Start
|
|
// eine valide Tages-Auswahl aus den SharedPreferences fand, wurde die
|
|
// CarSelectionPage übersprungen — und damit auch ihr `CarLoad`-Trigger.
|
|
// Ohne diesen Aufruf hängen drei Stellen am leeren CarsBloc-State:
|
|
// `PhaseStepper.carCount`, `app.carCountResolver` (Eintrittsphase) und
|
|
// `delivery_selection_page._plateFor` (Vergeben-Tab zeigt sonst "?").
|
|
// Der Bloc-interne `if (state is CarsLoaded && !event.force) return;`
|
|
// macht den Aufruf idempotent.
|
|
context.read<CarsBloc>().add(const CarLoad());
|
|
}
|
|
|
|
/// Stellt sicher, dass für das aktuell gewählte Auto eine Phase im
|
|
/// [PhaseBloc] existiert. Wird im build() reaktiv aufgerufen, daher mit
|
|
/// `_initializedCarId` gegen mehrfache Loads gesichert.
|
|
///
|
|
/// Wichtig: Wir feuern den Load erst, sobald sowohl Tour als auch Cars
|
|
/// einen geladen-Zustand haben — sonst fragt der `PhaseBloc` den
|
|
/// `carCountResolver` ab, bekommt `null` und entscheidet für Mehr-Auto-
|
|
/// Teams fälschlich auf `sortieren` statt `auswaehlen`.
|
|
void _ensurePhaseLoaded(String carId) {
|
|
if (_initializedCarId == carId) return;
|
|
final carsState = context.read<CarsBloc>().state;
|
|
if (carsState is! CarsLoaded) return;
|
|
_initializedCarId = carId;
|
|
context.read<PhaseBloc>().add(PhaseLoadForCar(carId: carId));
|
|
}
|
|
|
|
/// Ein-Auto-Teams überspringen die „Auswählen"-Phase (s. [PhaseBloc]),
|
|
/// in der Lieferungen normalerweise einem Fahrzeug zugeordnet werden.
|
|
/// Ohne Zuordnung (`assignedCarId`) blieben sie in „Ausliefern" unsichtbar.
|
|
/// Daher hier einmalig: ist genau EIN Fahrzeug im Team, werden alle noch
|
|
/// nicht zugeordneten Lieferungen automatisch diesem Fahrzeug zugewiesen.
|
|
///
|
|
/// Reaktiv (im build) aufgerufen, aber per [_autoAssignedForCar] gegen
|
|
/// Mehrfachausführung gesichert. Greift erst, wenn Tour UND Cars geladen
|
|
/// sind (sonst Abbruch ohne Flag → erneuter Versuch beim nächsten Build).
|
|
void _ensureSingleCarAssignment(String carId) {
|
|
final carsState = context.read<CarsBloc>().state;
|
|
final tourState = context.read<TourBloc>().state;
|
|
if (carsState is! CarsLoaded || tourState is! TourLoaded) return;
|
|
|
|
// Nur Ein-Auto-Teams: bei ≥2 Fahrzeugen entscheidet der Fahrer aktiv in
|
|
// der „Auswählen"-Phase, hier wird bewusst nichts automatisch zugewiesen.
|
|
if (carsState.cars.length != 1) return;
|
|
|
|
// Fahrzeugwechsel → Tracking zurücksetzen.
|
|
if (_autoAssignCar != carId) {
|
|
_autoAssignCar = carId;
|
|
_autoAssignDispatched.clear();
|
|
}
|
|
|
|
final tourBloc = context.read<TourBloc>();
|
|
for (final delivery in tourState.details.deliveries) {
|
|
// Jede (auch NEU gesyncte) noch nicht zugeordnete Lieferung genau einmal
|
|
// zuweisen. `Set.add` liefert `false`, wenn schon dispatcht → kein Respam
|
|
// in den Builds, bis der Server `assignedCarId` gesetzt hat.
|
|
if (delivery.assignedCarId != carId &&
|
|
_autoAssignDispatched.add(delivery.id)) {
|
|
tourBloc.add(
|
|
AssignCarToDelivery(deliveryId: delivery.id, carId: carId),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return BlocBuilder<CarSelectBloc, CarSelectState>(
|
|
builder: (context, carState) {
|
|
// Ohne ausgewähltes Auto bleibt Home leer — der CarSelectionEnforcer
|
|
// legt die Selection-Page als Overlay darüber.
|
|
if (carState is! CarSelectComplete) {
|
|
return const Scaffold(body: SizedBox.shrink());
|
|
}
|
|
|
|
final carId = carState.selectedCar.id.toString();
|
|
|
|
// Nachzieh-Trigger: wenn Cars erst NACH dem Tour-Build fertig
|
|
// werden, würde `_ensurePhaseLoaded` beim Tour-Build noch
|
|
// skippen. Dieser Listener feuert sobald `CarsLoaded` da ist.
|
|
return BlocListener<CarsBloc, CarsState>(
|
|
listenWhen: (prev, curr) =>
|
|
prev is! CarsLoaded && curr is CarsLoaded,
|
|
listener: (context, _) {
|
|
_ensurePhaseLoaded(carId);
|
|
_ensureSingleCarAssignment(carId);
|
|
},
|
|
child: BlocBuilder<TourBloc, TourState>(
|
|
builder: (context, tourState) {
|
|
if (tourState is TourLoaded || tourState is TourEmpty) {
|
|
_ensurePhaseLoaded(carId);
|
|
}
|
|
if (tourState is TourLoaded) {
|
|
_ensureSingleCarAssignment(carId);
|
|
}
|
|
|
|
return BlocBuilder<PhaseBloc, PhaseState>(
|
|
builder: (context, phaseState) {
|
|
final phase = phaseState is PhaseReady
|
|
? phaseState.phaseFor(carId)
|
|
: null;
|
|
|
|
// Solange Tour, Cars oder Phase noch laden, kurzen
|
|
// Spinner zeigen — das dauert in der Praxis maximal
|
|
// ein paar Frames.
|
|
if (phase == null) {
|
|
return const Scaffold(
|
|
body: Center(child: CircularProgressIndicator()),
|
|
);
|
|
}
|
|
|
|
return _buildForPhase(
|
|
context,
|
|
phase,
|
|
carState.selectedCar.id,
|
|
);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildForPhase(
|
|
BuildContext context,
|
|
DeliveryPhase phase,
|
|
String selectedCarId,
|
|
) {
|
|
switch (phase) {
|
|
case DeliveryPhase.auswaehlen:
|
|
// Auswahl-Page nur sichtbar bei Teams mit ≥2 Fahrzeugen — der
|
|
// PhaseBloc setzt diese Phase nicht für Ein-Auto-Teams.
|
|
return DeliverySelectionPage(selectedCarId: selectedCarId);
|
|
case DeliveryPhase.sortieren:
|
|
// Sort-Page baut eigenen Scaffold inkl. Stepper-Header.
|
|
return DeliverySortPage(selectedCarId: selectedCarId);
|
|
case DeliveryPhase.beladen:
|
|
// Beladen-Phase: Einstieg über die Übersicht. Der Fahrer wählt selbst
|
|
// aus, mit welchem Kunden er starten möchte — das Kunden-Vollbild
|
|
// wird per Tap auf eine Karte geöffnet (siehe LoadingOverviewPage).
|
|
return const LoadingOverviewPage();
|
|
case DeliveryPhase.ausliefern:
|
|
return const _DeliveryHome();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Home für die Auslieferungs-Phase. Reine Hülle um die Übersicht — es gibt
|
|
/// keine BottomNavigationBar mehr: Fahrzeug-Verwaltung und Einstellungen
|
|
/// sind über den Drawer erreichbar, der Fahrzeug-Wechsel direkt aus dem
|
|
/// PhaseStepper (Icon neben dem Plate).
|
|
class _DeliveryHome extends StatelessWidget {
|
|
const _DeliveryHome();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return const DeliveryOverviewPage();
|
|
}
|
|
}
|