import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:hl_lieferservice/domain/entity/delivery.dart'; import 'package:hl_lieferservice/domain/entity/tour_details.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/tour_bloc.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'; 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'; /// Page für die erste Phase bei Mehr-Auto-Teams: Auswählen der eigenen /// Lieferungen aus dem gemeinsamen Tour-Pool. /// /// Aufbau: /// * Tab "Verfügbar" — alle Lieferungen, die noch keinem Fahrzeug /// zugeordnet sind. Multiselect, Bulk-Bestätigung per BottomBar-Button. /// * Tab "Vergeben" — Lieferungen, die bereits zugeordnet sind. Tap auf /// eigene → Freigabe-Dialog; Tap auf fremde → Umlade-Dialog. /// * Persistente BottomBar: "Weiter zum Sortieren" — wechselt die Phase. class DeliverySelectionPage extends StatefulWidget { const DeliverySelectionPage({super.key, required this.selectedCarId}); final String selectedCarId; @override State createState() => _DeliverySelectionPageState(); } class _DeliverySelectionPageState extends State { final Set _selectedIds = {}; /// Sucht das Kennzeichen eines Fahrzeugs in der aktuell geladenen /// CarsBloc-Liste. Liefert "?" als Fallback, wenn das Auto nicht (mehr) /// im Account ist — z. B. nach Personalwechsel zwischen Tour-Syncs. String _plateFor(String? carId) { if (carId == null) return '?'; final carsState = context.read().state; if (carsState is! CarsLoaded) return '?'; for (final c in carsState.cars) { if (c.id == carId) return c.plate; } return '?'; } void _confirmSelection() { if (_selectedIds.isEmpty) return; final ids = List.from(_selectedIds); // EIN Bulk-Event statt N parallel laufender Single-Events: vermeidet // die Race-Condition, bei der `flutter_bloc`s default-concurrent // Event-Processing parallele Handler den Initial-State lesen lässt // und sich am Ende beim `emit` gegenseitig überschreiben. context.read().add( AssignCarToDeliveries( deliveryIds: ids, carId: widget.selectedCarId, ), ); setState(_selectedIds.clear); } void _goToSorting() { context.read().add( PhaseSet(carId: widget.selectedCarId, phase: DeliveryPhase.sortieren), ); } Future _showReleaseDialog(Delivery delivery, TourDetails details) async { final customer = details.customerOf(delivery); final result = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Lieferung freigeben'), content: Text( '${customer?.name ?? 'Diese Lieferung'} wurde Ihrem Fahrzeug ' 'zugeordnet. Möchten Sie sie wieder freigeben?', ), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(false), child: const Text('Abbrechen'), ), FilledButton( onPressed: () => Navigator.of(ctx).pop(true), child: const Text('Freigeben'), ), ], ), ); if (result != true || !mounted) return; context.read().add( AssignCarToDelivery(deliveryId: delivery.id, carId: null), ); } Future _showTakeoverDialog( Delivery delivery, TourDetails details, ) async { final customer = details.customerOf(delivery); final foreignPlate = _plateFor(delivery.assignedCarId); final ownPlate = _plateFor(widget.selectedCarId); final theme = Theme.of(context); final result = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Lieferung umladen'), content: RichText( text: TextSpan( style: theme.textTheme.bodyMedium, children: [ TextSpan( text: '${customer?.name ?? 'Diese Lieferung'} ist aktuell ', ), TextSpan( text: foreignPlate, style: const TextStyle(fontWeight: FontWeight.bold), ), const TextSpan( text: ' zugeordnet. Möchten Sie diese Lieferung auf ', ), TextSpan( text: ownPlate, style: const TextStyle(fontWeight: FontWeight.bold), ), const TextSpan(text: ' umladen?'), ], ), ), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(false), child: const Text('Abbrechen'), ), FilledButton( style: FilledButton.styleFrom( backgroundColor: theme.colorScheme.error, foregroundColor: theme.colorScheme.onError, ), onPressed: () => Navigator.of(ctx).pop(true), child: const Text('Übernehmen'), ), ], ), ); if (result != true || !mounted) return; context.read().add( AssignCarToDelivery( deliveryId: delivery.id, carId: widget.selectedCarId, ), ); } // ─── Widgets ───────────────────────────────────────────────────────── Widget _plateBadge(BuildContext context, String plate, {bool own = false}) { final theme = Theme.of(context); final bg = own ? theme.colorScheme.primary : theme.colorScheme.surfaceContainerHighest; final fg = own ? theme.colorScheme.onPrimary : theme.colorScheme.onSurfaceVariant; return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: bg, borderRadius: BorderRadius.circular(6), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.local_shipping_outlined, size: 12, color: fg), const SizedBox(width: 4), Text( plate, style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold, color: fg), ), ], ), ); } Widget _emptyState({ required IconData icon, required String title, String? subtitle, }) { final theme = Theme.of(context); return Center( child: Padding( padding: const EdgeInsets.all(32), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 64, color: theme.colorScheme.outline), const SizedBox(height: 12), Text( title, style: theme.textTheme.titleMedium, textAlign: TextAlign.center, ), if (subtitle != null) ...[ const SizedBox(height: 6), Text( subtitle, style: theme.textTheme.bodySmall, textAlign: TextAlign.center, ), ], ], ), ), ); } Widget _availableTab(List available, TourDetails details) { if (available.isEmpty) { return _emptyState( icon: Icons.inbox_outlined, title: 'Alle Lieferungen sind verteilt.', subtitle: 'Im Tab "Vergeben" können Sie eigene Lieferungen ' 'freigeben oder fremde übernehmen.', ); } return ListView.builder( padding: const EdgeInsets.symmetric(vertical: 4), itemCount: available.length, itemBuilder: (context, index) { final delivery = available[index]; final customer = details.customerOf(delivery); final isSelected = _selectedIds.contains(delivery.id); return CheckboxListTile( key: ValueKey('available-${delivery.id}'), value: isSelected, onChanged: (checked) { // Während ein Bulk-Zuweisung läuft, blockiert der // OperationViewEnforcer die Eingabe global; ein separates // `_isAssigning`-Lock ist hier nicht mehr nötig. setState(() { if (checked == true) { _selectedIds.add(delivery.id); } else { _selectedIds.remove(delivery.id); } }); }, title: Text( customer?.name ?? '⟨Unbekannter Kunde⟩', style: const TextStyle(fontWeight: FontWeight.w600), ), subtitle: Text( delivery.deliveryAddressSnapshot.oneLine, style: const TextStyle(fontSize: 12), ), controlAffinity: ListTileControlAffinity.leading, ); }, ); } Widget _assignedTab(List assigned, TourDetails details) { if (assigned.isEmpty) { return _emptyState( icon: Icons.local_shipping_outlined, title: 'Noch keine Lieferungen verteilt.', ); } final theme = Theme.of(context); return ListView.builder( padding: const EdgeInsets.symmetric(vertical: 4), itemCount: assigned.length, itemBuilder: (context, index) { final delivery = assigned[index]; final isOwn = delivery.assignedCarId == widget.selectedCarId; final plate = _plateFor(delivery.assignedCarId); final customer = details.customerOf(delivery); return Material( color: isOwn ? theme.colorScheme.primaryContainer.withValues(alpha: 0.35) : null, child: ListTile( key: ValueKey('assigned-${delivery.id}'), onTap: () { if (isOwn) { _showReleaseDialog(delivery, details); } else { _showTakeoverDialog(delivery, details); } }, leading: Icon( isOwn ? Icons.check_circle : Icons.person_outline, color: isOwn ? theme.colorScheme.primary : theme.colorScheme.onSurfaceVariant, ), title: Text( customer?.name ?? '⟨Unbekannter Kunde⟩', style: TextStyle( fontWeight: FontWeight.w600, color: isOwn ? theme.colorScheme.primary : null, ), ), subtitle: Text( delivery.deliveryAddressSnapshot.oneLine, style: const TextStyle(fontSize: 12), ), trailing: _plateBadge(context, plate, own: isOwn), ), ); }, ); } Widget _buildBottomBar() { final hasSelection = _selectedIds.isNotEmpty; // Disabled-State und Spinner während des Bulk-Zuweisens übernimmt // global der `OperationViewEnforcer` (StartOperation/FinishOperation // aus dem TourBloc) — daher hier keine lokale Lock-Variable mehr. return SafeArea( child: Padding( padding: const EdgeInsets.all(12), child: Column( mainAxisSize: MainAxisSize.min, children: [ if (hasSelection) SizedBox( width: double.infinity, child: FilledButton.icon( onPressed: _confirmSelection, icon: const Icon(Icons.check), label: Text( 'Auswahl bestätigen (${_selectedIds.length})', ), ), ), if (hasSelection) const SizedBox(height: 8), SizedBox( width: double.infinity, child: OutlinedButton.icon( onPressed: _goToSorting, icon: const Icon(Icons.arrow_forward), label: const Text('Weiter zum Sortieren'), ), ), ], ), ), ); } @override Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { if (state is TourLoadFailed) { return const DeliveryLoadingFailedPage(); } if (state is TourEmpty) { return Scaffold( appBar: AppBar(title: const Text('Lieferungen auswählen')), body: const Center( child: Padding( padding: EdgeInsets.all(24), child: Text( 'Für heute ist keine Tour zugewiesen.', style: TextStyle(fontSize: 16), textAlign: TextAlign.center, ), ), ), ); } if (state is! TourLoaded) { return const Scaffold( body: Center(child: CircularProgressIndicator()), ); } final details = state.details; final available = details.deliveries .where((d) => d.assignedCarId == null) .toList(); final assigned = details.deliveries .where((d) => d.assignedCarId != null) .toList(); _selectedIds.removeWhere( (id) => !available.any((d) => d.id == id), ); return DefaultTabController( length: 2, child: Scaffold( drawer: const HomeAppDrawer(), appBar: PreferredSize( preferredSize: const Size.fromHeight(140 + kTextTabBarHeight), child: Column( mainAxisSize: MainAxisSize.min, children: [ PhaseStepper( currentPhase: DeliveryPhase.auswaehlen, carId: widget.selectedCarId, ), Material( color: Theme.of(context).primaryColor, child: TabBar( labelColor: Theme.of(context).colorScheme.onPrimary, unselectedLabelColor: Theme.of(context) .colorScheme .onPrimary .withValues(alpha: 0.6), indicatorColor: Theme.of(context).colorScheme.onPrimary, tabs: [ Tab(text: 'Verfügbar (${available.length})'), Tab(text: 'Vergeben (${assigned.length})'), ], ), ), ], ), ), body: TabBarView( children: [ _availableTab(available, details), _assignedTab(assigned, details), ], ), bottomNavigationBar: _buildBottomBar(), ), ); }, ); } }