Final commit.

This commit is contained in:
Dennis Nemec
2026-06-01 17:12:28 +02:00
parent 3ecbc82885
commit a9bf8ecdd1
385 changed files with 29081 additions and 12089 deletions

View File

@ -1,12 +1,13 @@
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/events.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_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
@ -19,9 +20,9 @@ import 'package:hl_lieferservice/feature/delivery/model/delivery_phase.dart';
/// * 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 Sichtbarkeitsliste wird intern aus dem [CarsBloc] abgeleitet
/// (Anzahl Fahrzeuge im Account). So müssen Aufrufer den Stepper nicht
/// mit Routing-Wissen versorgen — er bleibt eine reine Anzeige-Komponente,
/// die auf den globalen State reagiert.
///
/// Verhalten:
@ -47,8 +48,8 @@ class PhaseStepper extends StatelessWidget {
/// 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).
/// Optionaler Override für Tests / Sonderfälle. Default: aus CarsBloc
/// abgeleitet (Anzahl cars im Account).
final List<DeliveryPhase>? visiblePhases;
IconData _iconFor(DeliveryPhase phase) {
@ -107,18 +108,16 @@ class PhaseStepper extends StatelessWidget {
final theme = Theme.of(context);
final onPrimary = theme.colorScheme.onPrimary;
return BlocBuilder<TourBloc, TourState>(
return BlocBuilder<CarsBloc, CarsState>(
// 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;
final prevCars = prev is CarsLoaded ? prev.cars.length : 0;
final currCars = curr is CarsLoaded ? curr.cars.length : 0;
return prevCars != currCars || prev.runtimeType != curr.runtimeType;
},
builder: (context, tourState) {
final carCount = tourState is TourLoaded
? tourState.tour.driver.cars.length
: 0;
builder: (context, carsState) {
final carCount = carsState is CarsLoaded ? carsState.cars.length : 0;
final phases = visiblePhases ?? _effectivePhases(carCount);
// Höchste erreichte Phase aus dem PhaseBloc — bestimmt, welche
@ -151,27 +150,9 @@ class PhaseStepper extends StatelessWidget {
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,
),
),
],
),
return _SelectedCarPill(
plate: state.selectedCar.plate,
onPrimary: onPrimary,
);
},
),
@ -337,6 +318,65 @@ class _StepperItem extends StatelessWidget {
}
}
/// Dezente Pille rechts oben im Header: Lkw-Icon, Plate, kleiner Swap-Button.
///
/// Visuell zurückhaltend gehalten: leicht durchscheinender Hintergrund auf
/// dem Primary-Header, kompakter IconButton — der Plate-Text bleibt
/// dominantes Element, der Wechseln-Knopf ist eine sekundäre Geste, die
/// erst auf Anfrage benutzt wird.
class _SelectedCarPill extends StatelessWidget {
const _SelectedCarPill({required this.plate, required this.onPrimary});
final String plate;
final Color onPrimary;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.only(left: 10, right: 2),
decoration: BoxDecoration(
color: onPrimary.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.local_shipping, color: onPrimary, size: 16),
const SizedBox(width: 6),
Text(
plate,
style: TextStyle(
color: onPrimary,
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
const SizedBox(width: 2),
IconButton(
tooltip: 'Fahrzeug wechseln',
icon: Icon(
Icons.swap_horiz_rounded,
// Etwas heller als das Plate, damit der Button als sekundäre
// Aktion gelesen wird und das Plate nicht überstrahlt.
color: onPrimary.withValues(alpha: 0.85),
size: 18,
),
visualDensity: VisualDensity.compact,
padding: const EdgeInsets.all(4),
constraints: const BoxConstraints(
minWidth: 32,
minHeight: 32,
),
splashRadius: 18,
onPressed: () =>
context.read<CarSelectBloc>().add(CarSelectChange()),
),
],
),
);
}
}
class _Connector extends StatelessWidget {
const _Connector({required this.isPassed});