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:
@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user