import 'package:collection/collection.dart'; 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/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'; import 'package:hl_lieferservice/feature/delivery/overview/presentation/delivery_fail_page.dart'; import 'package:hl_lieferservice/feature/loading/model/loading_group.dart'; import 'package:hl_lieferservice/feature/loading/presentation/loading_customer_page.dart'; import 'package:hl_lieferservice/feature/loading/util/loading_order.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'; /// Übersichts-Ansicht für die Beladen-Phase: alle Kunden in Beladereihen- /// folge mit Fortschritts-Status. KEIN Scanner — der Scanner-Fokus bleibt /// auf der [LoadingCustomerPage]. Tap auf einen Kunden öffnet seine /// Vollbild-Ansicht mit dem entsprechenden Index. class LoadingOverviewPage extends StatelessWidget { const LoadingOverviewPage({super.key}); String? _lookupCarPlate(String? carId, Tour tour) { if (carId == null) return null; return tour.driver.cars.firstWhereOrNull((c) => c.id == carId)?.plate; } List _buildGroups(TourLoaded state, String carIdStr) { final orderedIds = LoadingOrder.computeForCar( state: state, carIdStr: carIdStr, ); final byId = {for (final d in state.tour.deliveries) d.id: d}; final groups = []; for (final id in orderedIds) { final delivery = byId[id]; if (delivery == null) continue; if (delivery.state == DeliveryState.finished) continue; final scannable = delivery.articles.where((a) => a.scannable).toList(growable: false); if (scannable.isEmpty && delivery.state != DeliveryState.canceled) { continue; } groups.add(LoadingGroup( delivery: delivery, articles: scannable, carPlate: _lookupCarPlate(delivery.carId, state.tour), )); } return groups; } @override Widget build(BuildContext context) { return BlocBuilder( builder: (context, carState) { final carIdStr = carState is CarSelectComplete ? carState.selectedCar.id.toString() : ""; return BlocBuilder( builder: (context, tourState) { if (tourState is TourLoadingFailed) { return const DeliveryLoadingFailedPage(); } if (tourState is! TourLoaded) { return const Scaffold( body: Center(child: CircularProgressIndicator()), ); } final groups = _buildGroups(tourState, carIdStr); return Scaffold( drawer: const HomeAppDrawer(), appBar: PreferredSize( preferredSize: const Size.fromHeight(140), child: PhaseStepper( currentPhase: DeliveryPhase.beladen, carId: carIdStr, ), ), body: SafeArea( top: false, child: groups.isEmpty ? const _EmptyOverview() : _OverviewList(groups: groups), ), ); }, ); }, ); } } class _OverviewList extends StatelessWidget { const _OverviewList({required this.groups}); final List groups; @override Widget build(BuildContext context) { final totalActive = groups .where((g) => g.delivery.state != DeliveryState.canceled) .length; final doneActive = groups .where((g) => g.delivery.state != DeliveryState.canceled && g.isComplete) .length; return Column( children: [ Padding( padding: const EdgeInsets.fromLTRB(16, 12, 16, 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( "Beladereihenfolge", style: Theme.of(context).textTheme.titleMedium, ), Text( "$doneActive / $totalActive Kunden", style: Theme.of(context).textTheme.titleSmall?.copyWith( fontWeight: FontWeight.bold, ), ), ], ), const SizedBox(height: 6), ClipRRect( borderRadius: BorderRadius.circular(4), child: LinearProgressIndicator( value: totalActive == 0 ? 0.0 : doneActive / totalActive, minHeight: 6, backgroundColor: Theme.of(context) .colorScheme .surfaceContainerHighest, valueColor: AlwaysStoppedAnimation( doneActive == totalActive && totalActive > 0 ? Colors.green : Theme.of(context).primaryColor, ), ), ), ], ), ), const Divider(height: 1), Expanded( child: ListView.builder( padding: const EdgeInsets.only(top: 8, bottom: 16), itemCount: groups.length, itemBuilder: (context, index) { final g = groups[index]; return _OverviewTile( position: index + 1, group: g, onTap: () { // Push (kein pushReplacement): die Übersicht ist seit dem // Routing-Umbau in home.dart die Wurzel der Beladen-Phase. // Vom Vollbild kehrt der Fahrer per pop zurück auf diese // Übersicht — der Stack bleibt damit flach. Navigator.of(context).push( MaterialPageRoute( builder: (_) => LoadingCustomerPage(initialIndex: index), ), ); }, ); }, ), ), ], ); } } class _OverviewTile extends StatelessWidget { const _OverviewTile({ required this.position, required this.group, required this.onTap, }); final int position; final LoadingGroup group; final VoidCallback onTap; @override Widget build(BuildContext context) { final theme = Theme.of(context); final canceled = group.delivery.state == DeliveryState.canceled; final isComplete = group.isComplete; final isPartial = group.isPartial; final hasExternalWarehouse = group.hasExternalWarehouseArticles; // cardColor und borderColor sind nicht final, weil das Außenlager- // Highlight sie weiter unten ggf. überschreibt. Color cardColor; Color borderColor; final Color titleColor; final String statusText; final IconData statusIcon; if (canceled) { cardColor = Colors.grey.withValues(alpha: 0.08); borderColor = Colors.grey.withValues(alpha: 0.35); titleColor = Colors.grey.shade700; statusText = "Abgebrochen"; statusIcon = Icons.cancel_outlined; } else if (isComplete && hasExternalWarehouse) { // Standardlager ist fertig, aber es liegen noch Artikel in einem // anderen Lager — die Lieferung ist also NICHT komplett beladen. // Wir machen das im Status-Text explizit, damit der Fahrer nicht // fälschlich davon ausgeht, dass nichts mehr offen ist. cardColor = Colors.deepOrange.withValues(alpha: 0.10); borderColor = Colors.deepOrange.withValues(alpha: 0.45); titleColor = Colors.deepOrange.shade800; statusText = "Standardlager fertig — Außenlager offen"; statusIcon = Icons.warehouse_outlined; } else if (isComplete) { cardColor = Colors.green.withValues(alpha: 0.07); borderColor = Colors.green.withValues(alpha: 0.35); titleColor = Colors.green.shade700; statusText = "Fertig beladen"; statusIcon = Icons.check_circle_outline; } else if (isPartial) { cardColor = Colors.orange.withValues(alpha: 0.07); borderColor = Colors.orange.withValues(alpha: 0.35); titleColor = Colors.orange.shade800; statusText = "Beladung läuft"; statusIcon = Icons.pending_outlined; } else { cardColor = theme.colorScheme.surfaceContainerLow; borderColor = Colors.transparent; titleColor = theme.colorScheme.onSurface; statusText = "Offen"; statusIcon = Icons.radio_button_unchecked; } // Außenlager-Hervorhebung: lebt unabhängig vom Scan-Status. Eine // abgebrochene Lieferung bleibt grau, ansonsten überschreibt das // Außenlager-Highlight die Standard-Farben durch ein klar erkennbares // Orange — der Fahrer muss früh genug wissen, dass er ein anderes // Lager anfahren wird. Der Sonderzweig "isComplete && hasExternal- // Warehouse" oben hat das Highlight schon gesetzt, hier greift es // für die noch nicht fertigen Fälle. if (!canceled && hasExternalWarehouse && !isComplete) { cardColor = Colors.deepOrange.withValues(alpha: 0.10); borderColor = Colors.deepOrange.withValues(alpha: 0.65); } final progressLabel = canceled ? "—" : "${group.completeArticles}/${group.totalArticles} Artikel"; return Opacity( opacity: canceled ? 0.65 : 1.0, child: Card( margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), elevation: 0, color: cardColor, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), side: BorderSide(color: borderColor), ), child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(12), child: Padding( padding: const EdgeInsets.all(12), child: Row( children: [ CircleAvatar( backgroundColor: canceled ? Colors.grey : theme.colorScheme.primary, foregroundColor: theme.colorScheme.onPrimary, radius: 18, child: Text( "$position", style: const TextStyle(fontWeight: FontWeight.bold), ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( group.delivery.customer.name, style: TextStyle( fontSize: 15, fontWeight: FontWeight.w600, color: titleColor, decoration: canceled ? TextDecoration.lineThrough : TextDecoration.none, ), ), const SizedBox(height: 2), Text( group.delivery.customer.address.toString(), style: TextStyle( fontSize: 12, color: theme.colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 4), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.only(top: 2), child: Icon(statusIcon, size: 14, color: titleColor), ), const SizedBox(width: 4), // Expanded, damit lange Status-Texte wie // "Standardlager fertig — Außenlager offen" // umbrechen statt zu überlaufen. Expanded( child: Text( statusText, style: TextStyle( fontSize: 12, fontWeight: FontWeight.w500, color: titleColor, ), softWrap: true, ), ), const SizedBox(width: 10), Text( progressLabel, style: TextStyle( fontSize: 12, color: theme.colorScheme.onSurfaceVariant, ), ), ], ), if (!canceled && hasExternalWarehouse) ...[ const SizedBox(height: 6), _ExternalWarehouseBadge( labels: group.externalWarehouseLabels, ), ], ], ), ), const Icon(Icons.chevron_right), ], ), ), ), ), ); } } /// Hinweis-Badge unter dem Status-Row einer Lieferung mit Artikeln aus /// einem oder mehreren Außenlagern. Listet die betroffenen Lager-Namen /// auf, damit der Fahrer beim Beladen weiß, wohin er zusätzlich muss. class _ExternalWarehouseBadge extends StatelessWidget { const _ExternalWarehouseBadge({required this.labels}); final List labels; @override Widget build(BuildContext context) { final text = labels.isEmpty ? "Außenlager" : "Außenlager: ${labels.join(", ")}"; return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: Colors.deepOrange.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(6), border: Border.all(color: Colors.deepOrange.withValues(alpha: 0.6)), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.warehouse_outlined, size: 14, color: Colors.deepOrange.shade700), const SizedBox(width: 4), Flexible( child: Text( text, style: TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: Colors.deepOrange.shade800, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), ), ], ), ); } } class _EmptyOverview extends StatelessWidget { const _EmptyOverview(); @override Widget build(BuildContext context) { final scheme = Theme.of(context).colorScheme; return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.inbox_outlined, size: 64, color: scheme.onSurfaceVariant), const SizedBox(height: 12), Text( "Keine Lieferungen zum Beladen", style: Theme.of(context).textTheme.titleMedium, ), ], ), ); } }