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/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_state.dart'; import 'package:hl_lieferservice/feature/delivery/model/delivery_phase.dart'; /// Horizontaler Phasen-Stepper für die drei bzw. vier Schritte /// (Auswählen) → Sortieren → Beladen → Ausliefern. /// /// Ersetzt während aller Pre-Lieferungs-Phasen die [BottomNavigationBar] /// und dient zugleich als globaler Header oberhalb der Phase-Inhalts-Page. /// /// Sichtbare Phasen: /// * Ein-Auto-Teams: Sortieren, Beladen, Ausliefern (3 Schritte). /// * Mehr-Auto-Teams: Auswählen, Sortieren, Beladen, Ausliefern (4 Schritte). /// /// Die Sichtbarkeitsliste wird intern aus dem [TourBloc] abgeleitet /// (Anzahl `tour.driver.cars`). So müssen Aufrufer den Stepper nicht mit /// Routing-Wissen versorgen — er bleibt eine reine Anzeige-Komponente, /// die auf den globalen State reagiert. /// /// Verhalten: /// * Aktuelle Phase ist visuell hervorgehoben. /// * Bereits besuchte Phasen (Phasen, die der Fahrer heute schon erreicht /// hatte — verfolgt über `maxPhase` im [PhaseBloc]) lassen sich antippen, /// auch wenn man aktuell auf einer früheren Phase steht. Damit kann der /// 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). /// * Rechts steht das Plate des aktiv gewählten Fahrzeugs. class PhaseStepper extends StatelessWidget { const PhaseStepper({ super.key, required this.currentPhase, required this.carId, this.visiblePhases, }); /// Die aktuell aktive Phase des Fahrzeugs. final DeliveryPhase currentPhase; /// Auto-ID, an der die Phase im [PhaseBloc] gespeichert wird. final String carId; /// Optionaler Override für Tests / Sonderfälle. Default: aus TourBloc /// abgeleitet (Anzahl cars im Team). final List? visiblePhases; IconData _iconFor(DeliveryPhase phase) { return switch (phase) { DeliveryPhase.auswaehlen => Icons.checklist_outlined, DeliveryPhase.sortieren => Icons.reorder, DeliveryPhase.beladen => Icons.inventory_2_outlined, DeliveryPhase.ausliefern => Icons.local_shipping_outlined, }; } /// Stellt sicher, dass auch die [currentPhase] in der angezeigten Liste /// enthalten ist. Falls z. B. ein persistierter `auswaehlen`-State noch /// aus einem Mehr-Auto-Team stammt, das Team aber inzwischen nur noch /// ein Auto hat, würde die Phase sonst unsichtbar — wir nehmen sie dann /// vorne mit auf, damit der Stepper konsistent bleibt. List _effectivePhases(int carCount) { final base = DeliveryPhaseExtension.visiblePhasesForCarCount(carCount); if (base.contains(currentPhase)) return base; return [currentPhase, ...base]; } void _onTap( BuildContext context, DeliveryPhase target, DeliveryPhase maxReached, ) { // Vergleich über die natürliche Enum-Reihenfolge, weil "höchste erreichte // Phase" cross-Team konsistent ist (1- vs. Mehr-Auto-Teams). if (target.index > maxReached.index) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( "Schritt \"${target.displayName}\" ist noch nicht erreicht.", ), duration: const Duration(seconds: 2), ), ); return; } if (target == currentPhase) return; // Vor dem Phasen-Wechsel alle gepushten Routen entfernen (z. B. die // LoadingOverviewPage). Sonst rendert home.dart die neue Phase nur im // Hintergrund, während die alte Page mit ihrem Scanner-State sichtbar // bleibt — was zu einer toten Kamera-View führt. Navigator.of(context).popUntil((route) => route.isFirst); context.read().add( PhaseSet(carId: carId, phase: target), ); } @override Widget build(BuildContext context) { final theme = Theme.of(context); final onPrimary = theme.colorScheme.onPrimary; return BlocBuilder( // Stepper reagiert auf cars.length-Änderungen — sonst praktisch statisch. buildWhen: (prev, curr) { if (visiblePhases != null) return prev != curr; // override aktiv final prevCars = prev is TourLoaded ? prev.tour.driver.cars.length : 0; final currCars = curr is TourLoaded ? curr.tour.driver.cars.length : 0; return prevCars != currCars || prev.runtimeType != curr.runtimeType; }, builder: (context, tourState) { final carCount = tourState is TourLoaded ? tourState.tour.driver.cars.length : 0; final phases = visiblePhases ?? _effectivePhases(carCount); // Höchste erreichte Phase aus dem PhaseBloc — bestimmt, welche // Vorwärts-Sprünge erlaubt sind. final phaseBlocState = context.watch().state; final maxReached = phaseBlocState is PhaseReady ? (phaseBlocState.maxPhaseFor(carId) ?? currentPhase) : currentPhase; return Material( color: theme.primaryColor, child: SafeArea( bottom: false, child: Padding( padding: const EdgeInsets.fromLTRB(4, 4, 8, 30), child: Column( mainAxisSize: MainAxisSize.min, children: [ Row( children: [ // Menü-Button — öffnet den Drawer des umgebenden Scaffolds. IconButton( icon: Icon(Icons.menu, color: onPrimary), tooltip: "Menü", onPressed: () => Scaffold.of(context).openDrawer(), ), const Spacer(), BlocBuilder( builder: (context, state) { if (state is! CarSelectComplete) { return const SizedBox.shrink(); } return Padding( padding: const EdgeInsets.only(right: 4), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.local_shipping, color: onPrimary, size: 18, ), const SizedBox(width: 6), Text( state.selectedCar.plate, style: TextStyle( color: onPrimary, fontWeight: FontWeight.bold, fontSize: 14, ), ), ], ), ); }, ), ], ), Padding( padding: const EdgeInsets.symmetric(horizontal: 4), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ for (int i = 0; i < phases.length; i++) ...[ Expanded( child: _StepperItem( phase: phases[i], currentPhase: currentPhase, maxReached: maxReached, visiblePhases: phases, icon: _iconFor(phases[i]), onTap: () => _onTap(context, phases[i], maxReached), ), ), if (i < phases.length - 1) _Connector( // Verbindung gilt als "abgehakt", wenn die rechte // Phase bereits besucht wurde. isPassed: phases[i + 1].index <= maxReached.index, ), ], ], ), ), ], ), ), ), ); }, ); } } class _StepperItem extends StatelessWidget { const _StepperItem({ required this.phase, required this.currentPhase, required this.maxReached, required this.visiblePhases, required this.icon, required this.onTap, }); final DeliveryPhase phase; final DeliveryPhase currentPhase; final DeliveryPhase maxReached; final List visiblePhases; final IconData icon; final VoidCallback onTap; @override Widget build(BuildContext context) { final theme = Theme.of(context); final onPrimary = theme.colorScheme.onPrimary; final int myStep = phase.stepNumberIn(visiblePhases); final bool isCurrent = phase == currentPhase; // "Besucht" = irgendwann heute schon erreicht (über Enum-Index), aber // nicht die aktuelle Phase. Erlaubt sowohl Rück- als auch Vorwärts-Tap. final bool isPassed = !isCurrent && phase.index <= maxReached.index; final Color circleColor; final Color iconColor; final Color labelColor; final FontWeight labelWeight; if (isCurrent) { circleColor = onPrimary; iconColor = theme.primaryColor; labelColor = onPrimary; labelWeight = FontWeight.w700; } else if (isPassed) { circleColor = onPrimary.withValues(alpha: 0.85); iconColor = theme.primaryColor; labelColor = onPrimary; labelWeight = FontWeight.w600; } else { circleColor = onPrimary.withValues(alpha: 0.25); iconColor = onPrimary.withValues(alpha: 0.65); labelColor = onPrimary.withValues(alpha: 0.65); labelWeight = FontWeight.w500; } final child = Column( mainAxisSize: MainAxisSize.min, children: [ Stack( alignment: Alignment.center, children: [ Container( width: 32, height: 32, decoration: BoxDecoration( color: circleColor, shape: BoxShape.circle, border: isCurrent ? Border.all(color: onPrimary, width: 2) : null, ), child: Icon(icon, color: iconColor, size: 18), ), if (isPassed) Positioned( right: 0, bottom: 0, child: Container( width: 13, height: 13, decoration: BoxDecoration( color: Colors.green.shade600, shape: BoxShape.circle, border: Border.all(color: theme.primaryColor, width: 1.5), ), child: const Icon( Icons.check, color: Colors.white, size: 8, ), ), ), ], ), const SizedBox(height: 3), Text( phase.displayName, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( color: labelColor, fontSize: 12, fontWeight: labelWeight, ), ), ], ); return Semantics( button: true, selected: isCurrent, label: "${phase.displayName}, Schritt $myStep von ${visiblePhases.length}", child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(8), child: Padding( padding: const EdgeInsets.symmetric(vertical: 2), child: child, ), ), ); } } class _Connector extends StatelessWidget { const _Connector({required this.isPassed}); final bool isPassed; @override Widget build(BuildContext context) { final onPrimary = Theme.of(context).colorScheme.onPrimary; return Padding( padding: const EdgeInsets.only(bottom: 18), child: Container( width: 14, height: 2, color: isPassed ? onPrimary : onPrimary.withValues(alpha: 0.35), ), ); } }