feat(header): Datumswahl im Kopf statt Fahrzeug-Pille

Neue Kalender-Pille im Phase-Stepper: zeigt das Tour-Datum und oeffnet einen
Datepicker, um die Tour eines anderen Tages zu laden (z. B. morgen vorab).
Fahrzeugwechsel bleibt ueber den Drawer erreichbar.

- TourDateCubit haelt das gewaehlte Datum (null = heute, Server autoritativ)
- LoadTour({date}) + TourBloc merkt das Datum (RefreshTour laedt denselben Tag)
- Repository getMyTourDetails({date}) -> API listMyTours(date:) -> GET /me/tours?date=

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dennis Nemec
2026-09-01 15:42:44 +02:00
parent 7324aa4e7d
commit 7b7cb4eb63
8 changed files with 204 additions and 79 deletions

View File

@ -1,14 +1,12 @@
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_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/model/delivery_phase.dart';
@ -153,17 +151,9 @@ class PhaseStepper extends StatelessWidget {
),
_ReloadButton(onPrimary: onPrimary),
const Spacer(),
BlocBuilder<CarSelectBloc, CarSelectState>(
builder: (context, state) {
if (state is! CarSelectComplete) {
return const SizedBox.shrink();
}
return _SelectedCarPill(
plate: state.selectedCar.plate,
onPrimary: onPrimary,
);
},
),
// Kopf-Kalender: ersetzt die frühere Fahrzeug-Pille.
// Der Fahrzeugwechsel bleibt über den Drawer erreichbar.
_SelectedDatePill(onPrimary: onPrimary),
],
),
Padding(
@ -332,57 +322,127 @@ class _StepperItem extends StatelessWidget {
/// 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});
/// Kopf-Kalender-Pille: zeigt das aktuell angezeigte Tour-Datum und öffnet
/// bei Tipp einen Datepicker, um die Tour eines anderen Tages zu laden
/// (z. B. die von morgen vorab). Ersetzt die frühere Fahrzeug-Pille; der
/// Fahrzeugwechsel läuft jetzt über den Drawer.
///
/// Das angezeigte Datum ist das im [TourDateCubit] gewählte; ist keins
/// gewählt (`null` = „heute"), fällt es auf das Datum der geladenen Tour
/// zurück (autoritativ vom Server) und zur Not auf die Geräte-Uhr.
class _SelectedDatePill extends StatelessWidget {
const _SelectedDatePill({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,
),
final selected = context.watch<TourDateCubit>().state;
final tourState = context.watch<TourBloc>().state;
final loaded = tourState is TourLoaded ? tourState.details.tour.date : null;
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final display = selected ??
(loaded != null
? DateTime(loaded.year, loaded.month, loaded.day)
: today);
final relative = _relativeLabel(display, today);
return Material(
color: onPrimary.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(20),
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: () => _pickDate(context, display, today),
child: Padding(
padding: const EdgeInsets.fromLTRB(10, 5, 8, 5),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.event, color: onPrimary, size: 16),
const SizedBox(width: 6),
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (relative != null)
Text(
relative,
style: TextStyle(
color: onPrimary.withValues(alpha: 0.85),
fontSize: 10,
height: 1.0,
fontWeight: FontWeight.w600,
),
),
Text(
_fmt(display),
style: TextStyle(
color: onPrimary,
fontWeight: FontWeight.bold,
fontSize: 14,
height: 1.15,
),
),
],
),
const SizedBox(width: 2),
Icon(
Icons.expand_more,
color: onPrimary.withValues(alpha: 0.85),
size: 18,
),
],
),
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()),
),
],
),
),
);
}
/// „Heute"/„Morgen"/„Gestern" relativ zur Geräte-Uhr, sonst `null`.
/// Bewusst ohne Locale-Abhängigkeit (kein `intl`-Init nötig).
static String? _relativeLabel(DateTime d, DateTime today) {
final diff = DateTime(d.year, d.month, d.day).difference(today).inDays;
if (diff == 0) return 'Heute';
if (diff == 1) return 'Morgen';
if (diff == -1) return 'Gestern';
return null;
}
static String _fmt(DateTime d) {
final dd = d.day.toString().padLeft(2, '0');
final mm = d.month.toString().padLeft(2, '0');
return '$dd.$mm.${d.year}';
}
Future<void> _pickDate(
BuildContext context,
DateTime current,
DateTime today,
) async {
// Standard-Fenster um heute; falls das aktuell angezeigte Datum (z. B.
// ein Dev-Override in der Vergangenheit) außerhalb liegt, Fenster
// erweitern, damit initialDate immer im gültigen Bereich bleibt.
final firstDefault = today.subtract(const Duration(days: 14));
final lastDefault = today.add(const Duration(days: 60));
final firstDate = current.isBefore(firstDefault) ? current : firstDefault;
final lastDate = current.isAfter(lastDefault) ? current : lastDefault;
final picked = await showDatePicker(
context: context,
initialDate: current,
firstDate: firstDate,
lastDate: lastDate,
helpText: 'Tour-Datum wählen',
cancelText: 'Abbrechen',
confirmText: 'Anzeigen',
);
if (picked == null || !context.mounted) return;
context.read<TourDateCubit>().pick(picked);
context.read<TourBloc>().add(LoadTour(date: picked));
}
}
class _Connector extends StatelessWidget {