diff --git a/lib/feature/delivery/overview/presentation/delivery_overview_page.dart b/lib/feature/delivery/overview/presentation/delivery_overview_page.dart index 7c55acd..9166204 100644 --- a/lib/feature/delivery/overview/presentation/delivery_overview_page.dart +++ b/lib/feature/delivery/overview/presentation/delivery_overview_page.dart @@ -35,16 +35,9 @@ class DeliveryOverviewPage extends StatelessWidget { carId: carId, ), ), - body: BlocConsumer( - listenWhen: (prev, next) => - next is TourLoaded && next.refreshError != null, - listener: (context, state) { - if (state is TourLoaded && state.refreshError != null) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(state.refreshError!)), - ); - } - }, + // Die Refresh-Fehler-Snackbar (inkl. Retry) sitzt zentral im + // PhaseStepper-Header (`_ReloadButton`) und gilt damit für alle Phasen. + body: BlocBuilder( builder: (context, state) { switch (state) { case TourLoaded(:final details): diff --git a/lib/feature/delivery/overview/presentation/delivery_selection_page.dart b/lib/feature/delivery/overview/presentation/delivery_selection_page.dart index 684ae71..6f5f0a8 100644 --- a/lib/feature/delivery/overview/presentation/delivery_selection_page.dart +++ b/lib/feature/delivery/overview/presentation/delivery_selection_page.dart @@ -370,7 +370,14 @@ class _DeliverySelectionPageState extends State { return BlocBuilder( builder: (context, state) { if (state is TourLoadFailed) { - return const DeliveryLoadingFailedPage(); + // Mit Scaffold + Drawer: die Fehlerseite (inkl. „Erneut versuchen") + // stand bisher nackt ohne Material-Rahmen da, und der Fahrer kam + // nicht an Einstellungen/Logout. + return Scaffold( + drawer: const HomeAppDrawer(), + appBar: AppBar(title: const Text('Lieferungen auswählen')), + body: const DeliveryLoadingFailedPage(), + ); } if (state is TourEmpty) { return Scaffold( diff --git a/lib/feature/delivery/overview/presentation/delivery_sort_page.dart b/lib/feature/delivery/overview/presentation/delivery_sort_page.dart index 794ff1e..887af24 100644 --- a/lib/feature/delivery/overview/presentation/delivery_sort_page.dart +++ b/lib/feature/delivery/overview/presentation/delivery_sort_page.dart @@ -11,6 +11,7 @@ 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/widget/sortable_delivery_list.dart'; +import 'package:hl_lieferservice/feature/delivery/overview/presentation/delivery_fail_page.dart'; import 'package:hl_lieferservice/widget/home/presentation/home_drawer.dart'; import 'package:hl_lieferservice/widget/phase_stepper/phase_stepper.dart'; @@ -251,6 +252,25 @@ class _DeliverySortPageState extends State { body: _emptyState(), ); } + if (state is TourLoadFailed) { + // Laden gescheitert: Fehlerseite mit „Erneut versuchen" im selben + // Rahmen (Header + Drawer). Vorher fiel dieser Zustand in den + // Lade-Zweig darunter → Endlos-Spinner ohne Ausweg. + return Scaffold( + drawer: const HomeAppDrawer(), + appBar: PreferredSize( + preferredSize: const Size.fromHeight(140), + child: PhaseStepper( + currentPhase: DeliveryPhase.sortieren, + carId: widget.selectedCarId, + ), + ), + body: const SafeArea( + top: false, + child: DeliveryLoadingFailedPage(), + ), + ); + } if (state is! TourLoaded) { // Lade-Zwischenzustand (auch beim 'Neu laden') mit derselben // PhaseStepper-AppBar — sonst flackert die Leiste kurz auf die diff --git a/lib/feature/loading/presentation/loading_overview_page.dart b/lib/feature/loading/presentation/loading_overview_page.dart index 2299e39..38c3f1d 100644 --- a/lib/feature/loading/presentation/loading_overview_page.dart +++ b/lib/feature/loading/presentation/loading_overview_page.dart @@ -64,9 +64,6 @@ class LoadingOverviewPage extends StatelessWidget { return BlocBuilder( builder: (context, tourState) { - if (tourState is TourLoadFailed) { - return const DeliveryLoadingFailedPage(); - } // Auch ohne/vor geladener Tour den Phasen-Header zeigen — sonst // verschwinden Kopf-Kalender, Menü und Reload hinter einer nackten // 'Beladung'-AppBar und der Fahrer kommt an einem leeren Tag nicht @@ -78,6 +75,18 @@ class LoadingOverviewPage extends StatelessWidget { carId: carId, ), ); + if (tourState is TourLoadFailed) { + // Fehlerseite (mit „Erneut versuchen") im selben Rahmen wie die + // übrigen Zustände — vorher ohne Scaffold/Header/Drawer. + return Scaffold( + drawer: const HomeAppDrawer(), + appBar: stepperHeader, + body: const SafeArea( + top: false, + child: DeliveryLoadingFailedPage(), + ), + ); + } if (tourState is TourEmpty) { return Scaffold( drawer: const HomeAppDrawer(), diff --git a/lib/widget/attachment_image.dart b/lib/widget/attachment_image.dart index acae1bd..42232ad 100644 --- a/lib/widget/attachment_image.dart +++ b/lib/widget/attachment_image.dart @@ -64,6 +64,14 @@ class _AttachmentImageState extends State { } } + /// Stößt den Download nach einem Fehler erneut an (Tipp auf die + /// Fehleranzeige). Ein fehlgeschlagener Bild-Load ist kein Endzustand. + void _retry() { + setState(() { + _future = _load(); + }); + } + static const _ext = 'jpeg'; Future _load() async { @@ -133,12 +141,42 @@ class _AttachmentImageState extends State { if (snapshot.hasError || snapshot.data == null || snapshot.data!.isEmpty) { + // Laden fehlgeschlagen (Netz/VPN/Server): Tippen lädt erneut. + // Bewusst GestureDetector statt InkWell — das Widget wird auch + // ohne Material-Vorfahren (Vollbild-Ansicht) verwendet. return Center( - child: Padding( - padding: const EdgeInsets.all(16), - child: Icon( - Icons.broken_image_outlined, - color: theme.colorScheme.onSurfaceVariant, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: _retry, + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.broken_image_outlined, + color: theme.colorScheme.onSurfaceVariant, + ), + const SizedBox(height: 4), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.refresh, + size: 14, + color: theme.colorScheme.primary, + ), + const SizedBox(width: 4), + Text( + 'Erneut laden', + style: theme.textTheme.labelSmall?.copyWith( + color: theme.colorScheme.primary, + ), + ), + ], + ), + ], + ), ), ), ); diff --git a/lib/widget/home/presentation/home.dart b/lib/widget/home/presentation/home.dart index 0161236..6fc7a89 100644 --- a/lib/widget/home/presentation/home.dart +++ b/lib/widget/home/presentation/home.dart @@ -5,6 +5,7 @@ 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/cars/presentation/car_fail_page.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'; @@ -12,6 +13,8 @@ import 'package:hl_lieferservice/feature/delivery/bloc/tour_bloc.dart'; import 'package:hl_lieferservice/feature/delivery/bloc/tour_date_cubit.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/overview/presentation/delivery_fail_page.dart'; +import 'package:hl_lieferservice/widget/home/presentation/home_drawer.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'; @@ -178,6 +181,29 @@ class _HomeState extends State { // Spinner zeigen — das dauert in der Praxis maximal // ein paar Frames. if (phase == null) { + // Start-Sackgasse vermeiden: die Phase wird erst geladen, + // wenn Tour UND Fahrzeuge da sind. Scheiterte einer der + // beiden Requests, drehte hier bisher endlos der Spinner. + // Jetzt: Fehlerseite mit „Erneut versuchen" (+ Drawer für + // Einstellungen/Logout). + if (tourState is TourLoadFailed) { + return Scaffold( + drawer: const HomeAppDrawer(), + appBar: AppBar(), + body: const SafeArea( + child: DeliveryLoadingFailedPage(), + ), + ); + } + // `watch`: dieser Builder soll auch auf CarsBloc-Wechsel + // reagieren (Failed ↔ Loading ↔ Loaded beim Retry). + if (context.watch().state is CarsLoadingFailed) { + return Scaffold( + drawer: const HomeAppDrawer(), + appBar: AppBar(), + body: const SafeArea(child: CarsLoadingFailedPage()), + ); + } return const Scaffold( body: Center(child: CircularProgressIndicator()), ); diff --git a/lib/widget/phase_stepper/phase_stepper.dart b/lib/widget/phase_stepper/phase_stepper.dart index dadeda0..985820f 100644 --- a/lib/widget/phase_stepper/phase_stepper.dart +++ b/lib/widget/phase_stepper/phase_stepper.dart @@ -484,7 +484,31 @@ class _ReloadButton extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocBuilder( + return BlocConsumer( + // Fehlgeschlagener Hintergrund-Reload → Snackbar MIT Retry-Aktion. + // Zentral im gemeinsamen Header, damit es in ALLEN Phasen gilt (vorher + // nur auf der Ausliefern-Seite und ohne Retry; auf Sortieren/Beladen + // blieb der Fehler stumm). Nur bei neu gesetztem/geändertem Fehler + // feuern — nicht bei jedem Folge-Emit, solange der Fehler noch steht. + listenWhen: (prev, curr) => + curr is TourLoaded && + curr.refreshError != null && + (prev is! TourLoaded || prev.refreshError != curr.refreshError), + listener: (context, state) { + if (state is! TourLoaded || state.refreshError == null) return; + final bloc = context.read(); + ScaffoldMessenger.of(context) + ..hideCurrentSnackBar() + ..showSnackBar( + SnackBar( + content: Text(state.refreshError!), + action: SnackBarAction( + label: 'Erneut versuchen', + onPressed: () => bloc.add(const RefreshTour()), + ), + ), + ); + }, // Nur neu rendern, wenn sich der Refresh-Status ändert — sonst // läuft der Builder bei jedem Scan-Tick mit. buildWhen: (prev, curr) {