From a206636ed037f2f86e0262a97045a3cf129f8874 Mon Sep 17 00:00:00 2001 From: Dennis Nemec Date: Thu, 18 Jun 2026 13:08:18 +0200 Subject: [PATCH] feat(tour): Tour-Neuladen ueberall + Drawer in Leer-/Ladezustaenden - PhaseStepper: Reload-Button (RefreshTour, Spinner waehrend Refresh) - Beladen-Empty-State: 'Neu laden'-Button (LoadTour) + Hinweis 'keine Tour verfuegbar' - Drawer + AppBar in TourEmpty/Lade-Branches (Beladen-Uebersicht, Lieferungen auswaehlen, Sortieren) -> kein Festsitzen ohne Logout Co-Authored-By: Claude Opus 4.8 (1M context) --- .../presentation/delivery_selection_page.dart | 8 ++- .../presentation/delivery_sort_page.dart | 10 +++- .../presentation/loading_overview_page.dart | 27 +++++++++- lib/widget/phase_stepper/phase_stepper.dart | 52 +++++++++++++++++++ 4 files changed, 91 insertions(+), 6 deletions(-) diff --git a/lib/feature/delivery/overview/presentation/delivery_selection_page.dart b/lib/feature/delivery/overview/presentation/delivery_selection_page.dart index 9130790..684ae71 100644 --- a/lib/feature/delivery/overview/presentation/delivery_selection_page.dart +++ b/lib/feature/delivery/overview/presentation/delivery_selection_page.dart @@ -374,6 +374,7 @@ class _DeliverySelectionPageState extends State { } if (state is TourEmpty) { return Scaffold( + drawer: const HomeAppDrawer(), appBar: AppBar(title: const Text('Lieferungen auswählen')), body: const Center( child: Padding( @@ -388,8 +389,11 @@ class _DeliverySelectionPageState extends State { ); } if (state is! TourLoaded) { - return const Scaffold( - body: Center(child: CircularProgressIndicator()), + // Drawer auch hier — Fahrer soll im Lade-Hang ausloggen können. + return Scaffold( + drawer: const HomeAppDrawer(), + appBar: AppBar(title: const Text('Lieferungen auswählen')), + body: const Center(child: CircularProgressIndicator()), ); } diff --git a/lib/feature/delivery/overview/presentation/delivery_sort_page.dart b/lib/feature/delivery/overview/presentation/delivery_sort_page.dart index 8a5ac67..b6cb43f 100644 --- a/lib/feature/delivery/overview/presentation/delivery_sort_page.dart +++ b/lib/feature/delivery/overview/presentation/delivery_sort_page.dart @@ -223,15 +223,21 @@ class _DeliverySortPageState extends State { } }, builder: (context, state) { + // Drawer in jedem Branch beibehalten — sonst sitzt der Fahrer im + // „Keine Tour heute"- oder Lade-Screen fest, ohne Zugriff auf + // Einstellungen / Logout. if (state is TourEmpty) { return Scaffold( + drawer: const HomeAppDrawer(), appBar: AppBar(title: const Text('Sortieren')), body: _emptyState(), ); } if (state is! TourLoaded) { - return const Scaffold( - body: Center(child: CircularProgressIndicator()), + return Scaffold( + drawer: const HomeAppDrawer(), + appBar: AppBar(title: const Text('Sortieren')), + body: const Center(child: CircularProgressIndicator()), ); } diff --git a/lib/feature/loading/presentation/loading_overview_page.dart b/lib/feature/loading/presentation/loading_overview_page.dart index b411bda..98b93af 100644 --- a/lib/feature/loading/presentation/loading_overview_page.dart +++ b/lib/feature/loading/presentation/loading_overview_page.dart @@ -10,6 +10,7 @@ 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'; +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'; @@ -74,8 +75,12 @@ class LoadingOverviewPage extends StatelessWidget { ); } if (tourState is! TourLoaded) { - return const Scaffold( - body: Center(child: CircularProgressIndicator()), + // Drawer auch im Ladezustand — analog zum TourEmpty-Branch, + // damit der Fahrer beim Hängen nicht ohne Logout dasitzt. + return Scaffold( + drawer: const HomeAppDrawer(), + appBar: AppBar(title: const Text('Beladung')), + body: const Center(child: CircularProgressIndicator()), ); } @@ -866,6 +871,24 @@ class _EmptyOverview extends StatelessWidget { 'Keine Lieferungen zum Beladen', style: Theme.of(context).textTheme.titleMedium, ), + const SizedBox(height: 4), + Text( + 'Für heute ist aktuell keine Tour verfügbar.', + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith(color: scheme.onSurfaceVariant), + textAlign: TextAlign.center, + ), + const SizedBox(height: 20), + // Erneut die heutige Tour vom Backend laden. `LoadTour` blendet + // währenddessen den Seiten-Ladeindikator ein (TourLoading-Zweig) und + // landet danach wieder hier (TourEmpty) oder in der Tour-Ansicht. + FilledButton.tonalIcon( + onPressed: () => context.read().add(const LoadTour()), + icon: const Icon(Icons.refresh), + label: const Text('Neu laden'), + ), ], ), ); diff --git a/lib/widget/phase_stepper/phase_stepper.dart b/lib/widget/phase_stepper/phase_stepper.dart index 15d9407..a4ec463 100644 --- a/lib/widget/phase_stepper/phase_stepper.dart +++ b/lib/widget/phase_stepper/phase_stepper.dart @@ -8,6 +8,9 @@ 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'; /// Horizontaler Phasen-Stepper für die drei bzw. vier Schritte @@ -33,6 +36,10 @@ import 'package:hl_lieferservice/feature/delivery/model/delivery_phase.dart'; /// Fahrer beliebig zwischen besuchten Schritten hin- und herspringen. /// * Noch nicht besuchte Phasen sind sperren (SnackBar-Hinweis). /// * Über das Menü-Icon links wird der Drawer geöffnet (Fahrzeuge/Settings). +/// * Daneben sitzt ein Reload-Button, der über `RefreshTour` einen +/// Backend-Refresh anstößt (z. B. wenn ein Disponent eine Lieferung +/// nachgetragen oder umverteilt hat). Während ein Refresh läuft, +/// ersetzt ein kleiner Spinner das Icon und der Button ist gesperrt. /// * Rechts steht das Plate des aktiv gewählten Fahrzeugs. class PhaseStepper extends StatelessWidget { const PhaseStepper({ @@ -144,6 +151,7 @@ class PhaseStepper extends StatelessWidget { tooltip: "Menü", onPressed: () => Scaffold.of(context).openDrawer(), ), + _ReloadButton(onPrimary: onPrimary), const Spacer(), BlocBuilder( builder: (context, state) { @@ -395,3 +403,47 @@ class _Connector extends StatelessWidget { ); } } + +/// Reload-Button in der oberen Reihe des [PhaseStepper]s. Feuert +/// `RefreshTour` am [TourBloc] — refresht im Hintergrund und behält den +/// aktuellen `TourLoaded`-Snapshot sichtbar (kein Flicker zurück auf den +/// Lade-Spinner). Während des Refreshs ersetzt eine kleine +/// Fortschrittsanzeige das Icon und der Button ist gesperrt, um +/// Doppel-Triggern zu verhindern. +class _ReloadButton extends StatelessWidget { + const _ReloadButton({required this.onPrimary}); + + final Color onPrimary; + + @override + Widget build(BuildContext context) { + return BlocBuilder( + // Nur neu rendern, wenn sich der Refresh-Status ändert — sonst + // läuft der Builder bei jedem Scan-Tick mit. + buildWhen: (prev, curr) { + final p = prev is TourLoaded && prev.isRefreshing; + final c = curr is TourLoaded && curr.isRefreshing; + return p != c || prev.runtimeType != curr.runtimeType; + }, + builder: (context, state) { + final isRefreshing = state is TourLoaded && state.isRefreshing; + return IconButton( + tooltip: 'Tour aktualisieren', + onPressed: isRefreshing + ? null + : () => context.read().add(const RefreshTour()), + icon: isRefreshing + ? SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + color: onPrimary, + ), + ) + : Icon(Icons.refresh, color: onPrimary), + ); + }, + ); + } +}