import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.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/model/delivery.dart'; import 'package:hl_lieferservice/model/tour.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 Lieferung → Freigabe-Dialog; Tap auf fremde → Umlade-Dialog. /// * Persistent untere BottomBar: "Weiter zum Sortieren" — wechselt die /// Phase. Es gibt bewusst keinen Zwang, dass alle Lieferungen verteilt /// sein müssen; der Fahrer entscheidet wann er weiterzieht. class DeliverySelectionPage extends StatefulWidget { const DeliverySelectionPage({ super.key, required this.selectedCarId, }); /// ID des aktuell gewählten Fahrzeugs (Eigene Lieferungen / Ziel von /// Übernahmen). final int selectedCarId; @override State createState() => _DeliverySelectionPageState(); } class _DeliverySelectionPageState extends State { /// Lokale Multi-Selektion im Tab "Verfügbar". Wird nach erfolgreichem /// Bulk-Assign geleert. final Set _selectedIds = {}; /// True während sequentieller Assign-Calls — schaltet den /// Bestätigungs-Button auf Spinner und disabled. bool _isAssigning = false; String get _carIdString => widget.selectedCarId.toString(); /// Sucht das Plate eines Autos in der Tour-Driver-Liste. Liefert "?" als /// Fallback, falls die Zuordnung nicht (mehr) im Team enthalten ist — /// z. B. nach Personalwechsel zwischen Tour-Synchronisationen. String _plateFor(int? carId, Tour tour) { if (carId == null) return "?"; final car = tour.driver.cars.firstWhereOrNull((c) => c.id == carId); return car?.plate ?? "?"; } /// Bulk-Assign der aktuell selektierten Lieferungen an das eigene Auto. /// Sequentiell, damit der lokale Tour-Stream nach jedem Schritt /// konsistent ist und die Listen-Filter live mitwandern. Bei Fehlern /// wird eine SnackBar gezeigt und die Selektion bleibt erhalten. Future _confirmSelection() async { if (_selectedIds.isEmpty || _isAssigning) return; setState(() => _isAssigning = true); final tourBloc = context.read(); final ids = List.from(_selectedIds); try { for (final id in ids) { tourBloc.add( AssignCarEvent(deliveryId: id, carId: _carIdString), ); } // Hinweis: TourBloc verarbeitet die Events asynchron; lokale // Tour-Updates erfolgen über den Stream. Wir leeren die Selektion // optimistisch, damit der Fahrer ein klares Feedback bekommt. if (!mounted) return; setState(_selectedIds.clear); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Fehler beim Übernehmen: $e")), ); } finally { if (mounted) setState(() => _isAssigning = false); } } /// Wechselt in die nächste Phase (Sortieren). Der Fahrer kann jederzeit /// zurückspringen — die persistierte Phase wird über den Stepper-Tap /// zurückgesetzt. void _goToSorting() { context.read().add( PhaseSet( carId: _carIdString, phase: DeliveryPhase.sortieren, ), ); } Future _showReleaseDialog(Delivery delivery) async { final result = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text("Lieferung freigeben"), content: Text( "${delivery.customer.name} wurde Ihrem Fahrzeug zugeordnet. " "Möchten Sie diese Lieferung 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( UnassignDeliveryEvent(deliveryId: delivery.id), ); } Future _showTakeoverDialog(Delivery delivery, Tour tour) async { final foreignPlate = _plateFor(delivery.carId, tour); final ownPlate = _plateFor(widget.selectedCarId, tour); 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: "${delivery.customer.name} 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( // Warnfarbe, da das Umladen eine bestehende Zuordnung // überschreibt — last write wins. `colorScheme.error` ist // bewusst hart gewählt, damit der Fahrer den Eingriff in // fremde Disposition bewusst bestätigt. 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( AssignCarEvent( deliveryId: delivery.id, carId: _carIdString, ), ); } // --------------------------------------------------------------------------- // 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) { 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 isSelected = _selectedIds.contains(delivery.id); return CheckboxListTile( key: ValueKey("available-${delivery.id}"), value: isSelected, onChanged: _isAssigning ? null : (checked) { setState(() { if (checked == true) { _selectedIds.add(delivery.id); } else { _selectedIds.remove(delivery.id); } }); }, title: Text( delivery.customer.name, style: const TextStyle(fontWeight: FontWeight.w600), ), subtitle: Text( delivery.customer.address.toString(), style: const TextStyle(fontSize: 12), ), controlAffinity: ListTileControlAffinity.leading, ); }, ); } Widget _assignedTab(List assigned, Tour tour) { 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.carId == widget.selectedCarId; final plate = _plateFor(delivery.carId, tour); return Material( color: isOwn ? theme.colorScheme.primaryContainer.withValues(alpha: 0.35) : null, child: ListTile( key: ValueKey("assigned-${delivery.id}"), onTap: () { if (isOwn) { _showReleaseDialog(delivery); } else { _showTakeoverDialog(delivery, tour); } }, leading: Icon( isOwn ? Icons.check_circle : Icons.person_outline, color: isOwn ? theme.colorScheme.primary : theme.colorScheme.onSurfaceVariant, ), title: Text( delivery.customer.name, style: TextStyle( fontWeight: FontWeight.w600, color: isOwn ? theme.colorScheme.primary : null, ), ), subtitle: Text( delivery.customer.address.toString(), style: const TextStyle(fontSize: 12), ), trailing: _plateBadge(context, plate, own: isOwn), ), ); }, ); } /// BottomBar mit Bulk-Confirm (kontextabhängig) + Phasen-Wechsel. /// Confirm-Button ist nur sichtbar, wenn etwas selektiert ist — /// "Weiter zum Sortieren" bleibt immer sichtbar. Widget _buildBottomBar() { final theme = Theme.of(context); final hasSelection = _selectedIds.isNotEmpty; 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: _isAssigning ? null : _confirmSelection, icon: _isAssigning ? SizedBox( width: 16, height: 16, child: CircularProgressIndicator( strokeWidth: 2, color: theme.colorScheme.onPrimary, ), ) : 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: _isAssigning ? null : _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 TourLoadingFailed) { return const DeliveryLoadingFailedPage(); } if (state is! TourLoaded) { return const Scaffold( body: Center(child: CircularProgressIndicator()), ); } final available = state.tour.deliveries .where((d) => d.carId == null) .toList(); final assigned = state.tour.deliveries .where((d) => d.carId != null) .toList(); // Falls eine selektierte Lieferung in der Zwischenzeit zugeordnet // wurde (z. B. durch einen parallelen Vorgang), Selektion bereinigen. _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: _carIdString, ), 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), _assignedTab(assigned, state.tour), ], ), bottomNavigationBar: _buildBottomBar(), ), ); }, ); } }