import "dart:async"; import "dart:math"; import "package:bus_running_record/models/channels/operations_channel.dart"; import "package:bus_running_record/models/operations/stop.dart"; import "package:bus_running_record/widgets/trip_diagram.dart"; import "package:flutter/foundation.dart"; import "package:go_router/go_router.dart"; import "package:shadcn_flutter/shadcn_flutter.dart"; class OperationsChannelView extends StatefulWidget { const OperationsChannelView({required this.channel, super.key}); final OperationsChannel channel; @override State createState() => _OperationsChannelViewState(); } class _OperationsChannelViewState extends State { bool _loadingMeta = true; bool _loadingDetail = false; String? _scheduleId; List _duties = const []; List _busWorkNumbers = const []; List<({String tripNumber, String dutyNumber})> _tripMeta = const []; List _stopNames = const []; Map _aliases = const {}; String filterBy = "By Duty"; Map filterValue = {}; List? _dutyTrips; List? _busTrips; OperationsTripSnapshot? _tripSnapshot; List<({OperationsTrip trip, String? time})>? _stopSchedule; bool _stopHasMore = false; bool _stopLoadingMore = false; int _stopOffset = 0; static const int _stopPageSize = 20; List _stopRowKeys = []; final ScrollController _stopScrollController = ScrollController(); final GlobalKey _stopScrollViewKey = GlobalKey(); @override void initState() { super.initState(); _stopScrollController.addListener(_onStopScroll); unawaited(_loadMeta()); } @override void dispose() { _stopScrollController.dispose(); super.dispose(); } void _onStopScroll() { if (!_stopHasMore || _stopLoadingMore || _loadingDetail) return; final pos = _stopScrollController.position; if (pos.pixels >= pos.maxScrollExtent - 100) { unawaited(_loadMoreStop()); } } Future _invoke(String functionName, Map body) async { final client = widget.channel.client; client.functions.setAuth( client.auth.currentSession?.accessToken ?? "", ); return client.functions.invoke( functionName, body: body, headers: { "Authorization": "Bearer ${client.auth.currentSession?.accessToken ?? ""}", }, ); } Future _loadMeta() async { setState(() { _loadingMeta = true; }); try { final response = await _invoke("operations-schedule-meta", { "channel_id": widget.channel.id, }); final data = response.data as Map?; if (data == null) throw StateError("Empty response from meta function."); if (data["has_schedule"] != true) { if (!mounted) return; setState(() { _scheduleId = null; _loadingMeta = false; }); return; } final scheduleId = (data["schedule_id"] ?? "").toString(); final duties = List.from(data["duties"] as List? ?? []); final busWorkNumbers = List.from(data["bus_work_numbers"] as List? ?? []); final tripMetaRaw = data["trips"] as List? ?? []; final tripMeta = tripMetaRaw.map((t) { final m = t as Map; return ( tripNumber: (m["trip_number"] ?? "").toString(), dutyNumber: (m["duty_number"] ?? "").toString(), ); }).toList(); final stopNames = List.from(data["stop_names"] as List? ?? []); final aliasesRaw = data["aliases"] as List? ?? []; final aliases = {}; for (final row in aliasesRaw) { final m = row as Map; final raw = (m["raw_stop_name"] ?? "").toString().trim(); final alias = (m["alias_stop_name"] ?? "").toString().trim(); final source = (m["source"] ?? "user").toString(); if (raw.isEmpty || alias.isEmpty) continue; aliases[Stop.normalizeName(raw)] = (alias: alias, source: source); } final firstDuty = duties.isNotEmpty ? duties.first : null; if (!mounted) return; setState(() { _scheduleId = scheduleId; _duties = duties; _busWorkNumbers = busWorkNumbers; _tripMeta = tripMeta; _stopNames = stopNames; _aliases = aliases; _loadingMeta = false; if (firstDuty != null) { filterBy = "By Duty"; filterValue["By Duty"] = firstDuty; } }); if (firstDuty != null) { unawaited(_loadDutyDetail(firstDuty)); } } catch (error, stackTrace) { debugPrint("[OperationsChannelView] loadMeta failed: $error"); debugPrintStack(stackTrace: stackTrace); if (!mounted) return; setState(() => _loadingMeta = false); } } Future _loadDutyDetail(String dutyNumber) async { final scheduleId = _scheduleId; if (scheduleId == null) return; setState(() { _loadingDetail = true; _dutyTrips = null; }); try { final response = await _invoke("operations-duty-detail", { "channel_id": widget.channel.id, "schedule_id": scheduleId, "duty_number": dutyNumber, }); final data = response.data as Map?; final tripsRaw = data?["trips"] as List? ?? []; final snapshots = []; for (final tripData in tripsRaw) { final m = tripData as Map; final trip = OperationsTrip( id: (m["id"] ?? "").toString(), scheduleId: scheduleId, tripNumber: (m["trip_number"] ?? "").toString(), dutyNumber: (m["duty_number"] ?? "").toString(), busWorkNumber: (m["bus_work_number"] ?? "").toString(), direction: (m["direction"] ?? "").toString(), sortOrder: (m["sort_order"] as num?)?.toInt() ?? 0, ); final stopsRaw = m["stops"] as List? ?? []; final stops = stopsRaw.map((s) { final sm = s as Map; final name = (sm["stop_name"] ?? "").toString().trim(); final aliasEntry = _aliases[Stop.normalizeName(name)]; return OperationsScheduledStop( id: (sm["id"] ?? "").toString(), tripId: trip.id, sequence: (sm["stop_sequence"] as num?)?.toInt() ?? 0, name: name, alias: aliasEntry?.alias, aliasSource: aliasEntry?.source, scheduledTime: (sm["scheduled_time"] ?? "").toString().trim().isEmpty ? null : sm["scheduled_time"].toString(), ); }).toList() ..sort((a, b) => a.sequence.compareTo(b.sequence)); snapshots.add(OperationsTripSnapshot( trip: trip, stops: stops.map((s) => OperationsStop(scheduledStop: s)).toList(), scheduledStops: stops, )); } snapshots.sort((a, b) { final aTime = a.scheduledStops.firstOrNull?.scheduledTime ?? ""; final bTime = b.scheduledStops.firstOrNull?.scheduledTime ?? ""; return aTime.compareTo(bTime); }); if (!mounted) return; setState(() { _dutyTrips = snapshots; _loadingDetail = false; }); } catch (error, stackTrace) { debugPrint("[OperationsChannelView] loadDutyDetail failed: $error"); debugPrintStack(stackTrace: stackTrace); if (!mounted) return; setState(() => _loadingDetail = false); } } Future _loadBusDetail(String busWorkNumber) async { final scheduleId = _scheduleId; if (scheduleId == null) return; setState(() { _loadingDetail = true; _busTrips = null; }); try { final response = await _invoke("operations-bus-detail", { "channel_id": widget.channel.id, "schedule_id": scheduleId, "bus_work_number": busWorkNumber, }); final data = response.data as Map?; final tripsRaw = data?["trips"] as List? ?? []; final snapshots = []; for (final tripData in tripsRaw) { final m = tripData as Map; final trip = OperationsTrip( id: (m["id"] ?? "").toString(), scheduleId: scheduleId, tripNumber: (m["trip_number"] ?? "").toString(), dutyNumber: (m["duty_number"] ?? "").toString(), busWorkNumber: (m["bus_work_number"] ?? "").toString(), direction: (m["direction"] ?? "").toString(), sortOrder: (m["sort_order"] as num?)?.toInt() ?? 0, ); final stopsRaw = m["stops"] as List? ?? []; final stops = stopsRaw.map((s) { final sm = s as Map; final name = (sm["stop_name"] ?? "").toString().trim(); final aliasEntry = _aliases[Stop.normalizeName(name)]; return OperationsScheduledStop( id: (sm["id"] ?? "").toString(), tripId: trip.id, sequence: (sm["stop_sequence"] as num?)?.toInt() ?? 0, name: name, alias: aliasEntry?.alias, aliasSource: aliasEntry?.source, scheduledTime: (sm["scheduled_time"] ?? "").toString().trim().isEmpty ? null : sm["scheduled_time"].toString(), ); }).toList() ..sort((a, b) => a.sequence.compareTo(b.sequence)); snapshots.add(OperationsTripSnapshot( trip: trip, stops: stops.map((s) => OperationsStop(scheduledStop: s)).toList(), scheduledStops: stops, )); } snapshots.sort((a, b) { final aTime = a.scheduledStops.firstOrNull?.scheduledTime ?? ""; final bTime = b.scheduledStops.firstOrNull?.scheduledTime ?? ""; return aTime.compareTo(bTime); }); if (!mounted) return; setState(() { _busTrips = snapshots; _loadingDetail = false; }); } catch (error, stackTrace) { debugPrint("[OperationsChannelView] loadBusDetail failed: $error"); debugPrintStack(stackTrace: stackTrace); if (!mounted) return; setState(() => _loadingDetail = false); } } Future _loadTripDetail(String tripNumber) async { final scheduleId = _scheduleId; if (scheduleId == null) return; setState(() { _loadingDetail = true; _tripSnapshot = null; }); try { final response = await _invoke("operations-trip-detail", { "channel_id": widget.channel.id, "schedule_id": scheduleId, "trip_number": tripNumber, }); final data = response.data as Map?; final tripData = data?["trip"] as Map?; if (tripData == null) { if (!mounted) return; setState(() => _loadingDetail = false); return; } final trip = OperationsTrip( id: (tripData["id"] ?? "").toString(), scheduleId: scheduleId, tripNumber: (tripData["trip_number"] ?? "").toString(), dutyNumber: (tripData["duty_number"] ?? "").toString(), busWorkNumber: (tripData["bus_work_number"] ?? "").toString(), direction: (tripData["direction"] ?? "").toString(), sortOrder: (tripData["sort_order"] as num?)?.toInt() ?? 0, ); final stopsRaw = data?["stops"] as List? ?? []; final stops = stopsRaw.map((s) { final sm = s as Map; final name = (sm["stop_name"] ?? "").toString().trim(); final aliasEntry = _aliases[Stop.normalizeName(name)]; return OperationsScheduledStop( id: (sm["id"] ?? "").toString(), tripId: trip.id, sequence: (sm["stop_sequence"] as num?)?.toInt() ?? 0, name: name, alias: aliasEntry?.alias, aliasSource: aliasEntry?.source, scheduledTime: (sm["scheduled_time"] ?? "").toString().trim().isEmpty ? null : sm["scheduled_time"].toString(), ); }).toList(); if (!mounted) return; setState(() { _tripSnapshot = OperationsTripSnapshot( trip: trip, stops: stops.map((s) => OperationsStop(scheduledStop: s)).toList(), scheduledStops: stops, ); _loadingDetail = false; }); } catch (error, stackTrace) { debugPrint("[OperationsChannelView] loadTripDetail failed: $error"); debugPrintStack(stackTrace: stackTrace); if (!mounted) return; setState(() => _loadingDetail = false); } } Future _loadStopDetail(String stopName) async { final scheduleId = _scheduleId; if (scheduleId == null) return; setState(() { _loadingDetail = true; _stopSchedule = null; _stopOffset = 0; _stopHasMore = false; }); try { final response = await _invoke("operations-stop-detail", { "channel_id": widget.channel.id, "schedule_id": scheduleId, "stop_name": stopName, "offset": 0, "limit": _stopPageSize, }); final data = response.data as Map?; final page = _parseStopSchedule(scheduleId, data?["schedule"] as List? ?? []); final hasMore = data?["has_more"] == true; if (!mounted) return; setState(() { _stopSchedule = page; _stopOffset = page.length; _stopHasMore = hasMore; _stopRowKeys = List.generate(page.length, (_) => GlobalKey()); _loadingDetail = false; }); WidgetsBinding.instance.addPostFrameCallback((_) => _scrollToClosestTime(page)); } catch (error, stackTrace) { debugPrint("[OperationsChannelView] loadStopDetail failed: $error"); debugPrintStack(stackTrace: stackTrace); if (!mounted) return; setState(() => _loadingDetail = false); } } Future _loadMoreStop() async { final scheduleId = _scheduleId; if (scheduleId == null || !_stopHasMore || _stopLoadingMore) return; final stopName = (filterValue["By Stop"] ?? "").toString().trim(); if (stopName.isEmpty) return; setState(() => _stopLoadingMore = true); try { final response = await _invoke("operations-stop-detail", { "channel_id": widget.channel.id, "schedule_id": scheduleId, "stop_name": stopName, "offset": _stopOffset, "limit": _stopPageSize, }); final data = response.data as Map?; final page = _parseStopSchedule(scheduleId, data?["schedule"] as List? ?? []); final hasMore = data?["has_more"] == true; if (!mounted) return; setState(() { _stopSchedule = [...?_stopSchedule, ...page]; _stopOffset += page.length; _stopHasMore = hasMore; _stopLoadingMore = false; }); } catch (error, stackTrace) { debugPrint("[OperationsChannelView] loadMoreStop failed: $error"); debugPrintStack(stackTrace: stackTrace); if (!mounted) return; setState(() => _stopLoadingMore = false); } } void _scrollToClosestTime(List<({OperationsTrip trip, String? time})> schedule) { if (!_stopScrollController.hasClients) return; if (schedule.isEmpty) return; final now = DateTime.now(); final nowMins = now.hour * 60 + now.minute; int bestIndex = 0; int bestDiff = 9999; for (var i = 0; i < schedule.length; i++) { final time = schedule[i].time; if (time == null) continue; final parts = time.split(":"); if (parts.length < 2) continue; final h = int.tryParse(parts[0]); final m = int.tryParse(parts[1]); if (h == null || m == null) continue; final diff = ((h * 60 + m) - nowMins).abs(); if (diff < bestDiff) { bestDiff = diff; bestIndex = i; } } if (bestIndex >= _stopRowKeys.length) return; final rowCtx = _stopRowKeys[bestIndex].currentContext; if (rowCtx == null) return; final scrollCtx = _stopScrollViewKey.currentContext; if (scrollCtx == null) return; final rowBox = rowCtx.findRenderObject() as RenderBox?; final scrollBox = scrollCtx.findRenderObject() as RenderBox?; if (rowBox == null || scrollBox == null) return; final rowOffset = rowBox.localToGlobal(Offset.zero, ancestor: scrollBox); final target = (_stopScrollController.offset + rowOffset.dy).clamp( _stopScrollController.position.minScrollExtent, _stopScrollController.position.maxScrollExtent, ); _stopScrollController.animateTo( target, duration: const Duration(milliseconds: 350), curve: Curves.easeOut, ); } List<({OperationsTrip trip, String? time})> _parseStopSchedule( String scheduleId, List scheduleRaw, ) { return scheduleRaw.map((row) { final m = row as Map; final tripData = m["trip"] as Map; final trip = OperationsTrip( id: (tripData["id"] ?? "").toString(), scheduleId: scheduleId, tripNumber: (tripData["trip_number"] ?? "").toString(), dutyNumber: (tripData["duty_number"] ?? "").toString(), busWorkNumber: (tripData["bus_work_number"] ?? "").toString(), direction: (tripData["direction"] ?? "").toString(), sortOrder: 0, ); final time = (m["scheduled_time"] ?? "").toString().trim(); return (trip: trip, time: time.isEmpty ? null : time); }).toList(); } String? _aliasForStop(String rawName) => _aliases[Stop.normalizeName(rawName)]?.alias; Widget _stopLabel(String rawName) { final alias = _aliasForStop(rawName); if (alias == null || alias.isEmpty) return Text(rawName); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(alias).semiBold, if (alias != rawName) Text( rawName, style: const TextStyle(fontSize: 11, height: 1), ).xSmall.muted.semiBold, ], ); } List _diagramEntries(List stops) { return stops.map((stop) => TripDiagramEntry( label: stop.displayName, labelIcon: stop.aliasSource == "ai" ? LucideIcons.sparkles : null, subtitle: stop.displayName == stop.name ? null : stop.name, time: stop.scheduledTime, )).toList(growable: false); } @override Widget build(BuildContext context) { double bottomPadding = MediaQuery.of(context).padding.bottom; bool isMobile = defaultTargetPlatform == TargetPlatform.iOS || defaultTargetPlatform == TargetPlatform.android; if (_loadingMeta) { return const Center(child: CircularProgressIndicator()); } if (_scheduleId == null) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Text("No schedule uploaded yet.").h4, Gap(8), Button.secondary( onPressed: () => context.go( "/channel/${widget.channel.organizationId}/${widget.channel.id}/upload", ), child: const Text("Upload Schedule"), ), ], ), ); } return Column( children: [ Expanded( child: Builder( builder: (context) { if (_loadingDetail) { return const Center(child: CircularProgressIndicator()); } // By Stop if (filterBy == "By Stop") { final selectedStop = (filterValue["By Stop"] ?? "").toString().trim(); if (selectedStop.isEmpty) { return Center(child: Text("Select a stop to preview its schedule.").small.muted); } final schedules = _stopSchedule ?? const []; return Column( children: [ Gap(8), Divider(), Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Row( children: [ Icon(LucideIcons.pin), Gap(8), _stopLabel(selectedStop), ], ), ), Divider(), Gap(2), Divider(), Expanded( child: SingleChildScrollView( key: _stopScrollViewKey, controller: _stopScrollController, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (schedules.isEmpty) Padding( padding: const EdgeInsets.all(16), child: Text("No scheduled times found for this stop.").small.muted, ) else ...schedules.indexed.map( (entry) { final (i, row) = entry; final rowKey = i < _stopRowKeys.length ? _stopRowKeys[i] : null; return Container( key: rowKey, padding: const EdgeInsets.symmetric(vertical: 10), decoration: BoxDecoration( border: Border( bottom: BorderSide( color: Colors.white.withValues(alpha: 0.07), ), ), ), child: Row( children: [ Gap(16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text("${row.trip.dutyNumber} • ").extraBold, Text( "Trip ${row.trip.tripNumber} • Bus ${row.trip.busWorkNumber}", style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w600, ), ).muted, ], ), Row( children: [ Text(row.trip.direction).muted.textSmall, if (row.trip.direction.toLowerCase().contains("out")) ...[ Gap(4), Icon(LucideIcons.arrowUpRight).iconSmall, ] else if (row.trip.direction.toLowerCase().contains("in")) ...[ Gap(4), Icon(LucideIcons.arrowDownLeft).iconSmall, ], ], ), ], ), ), Text(row.time ?? "--:--").extraBold.mono, Gap(2), Icon(LucideIcons.chevronRight), Gap(2), ], ), ); }, ), if (_stopLoadingMore) const Padding( padding: EdgeInsets.all(16), child: Center(child: CircularProgressIndicator()), ), ], ), ), ), ], ); } // By Bus if (filterBy == "By Bus") { final selectedBus = (filterValue["By Bus"] ?? "").toString(); if (selectedBus.isEmpty) { return Center(child: Text("Select a bus to preview.").small.muted); } final snapshots = _busTrips ?? const []; if (snapshots.isEmpty) { return Center(child: Text("No trips found for this bus.").small.muted); } return Column( children: [ Gap(8), Divider(), Container( height: 50, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Row( children: [ Icon(LucideIcons.busFront), Gap(10), Text("Bus $selectedBus".toUpperCase()).extraBold.textSmall, ], ), ), Divider(), Gap(2), Divider(), Expanded( child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ for (var i = 0; i < snapshots.length; i++) ...[ if (i != 0) ...[ Divider(), Gap(2), Divider(), ], Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Row( children: [ Icon(LucideIcons.bus), Gap(8), Text( "Trip ${snapshots[i].trip.tripNumber} • Duty ${snapshots[i].trip.dutyNumber}", ).semiBold.textSmall, ], ), ), Divider(), TripDiagram( lineColor: Colors.red, entries: _diagramEntries(snapshots[i].scheduledStops), leftOffset: 12, rowHeight: 48, ), ], ], ), ), ), ], ); } // By Duty if (filterBy == "By Duty") { final selectedDuty = (filterValue["By Duty"] ?? "").toString(); if (selectedDuty.isEmpty) { return Center(child: Text("Select a duty to preview.").small.muted); } final snapshots = _dutyTrips ?? const []; if (snapshots.isEmpty) { return Center(child: Text("No trips found for this duty.").small.muted); } return Column( children: [ Gap(8), Divider(), Container( height: 50, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Row( children: [ Icon(LucideIcons.busFront), Gap(10), Text("Duty $selectedDuty".toUpperCase()).extraBold.textSmall, ], ), ), Divider(), Gap(2), Divider(), Expanded( child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ for (var i = 0; i < snapshots.length; i++) ...[ if (i != 0) ...[ Divider(), Gap(2), Divider(), ], Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Row( children: [ Icon(LucideIcons.bus), Gap(8), Text( "Trip ${snapshots[i].trip.tripNumber} • Duty ${snapshots[i].trip.dutyNumber}", ).semiBold.textSmall, ], ), ), Divider(), TripDiagram( lineColor: Colors.red, entries: _diagramEntries(snapshots[i].scheduledStops), leftOffset: 12, rowHeight: 48, ), ], ], ), ), ), ], ); } // By Trip if (filterBy == "By Trip") { final selectedTripNumber = (filterValue["By Trip"] ?? "").toString(); if (selectedTripNumber.isEmpty) { return Center(child: Text("Select a trip to preview.").small.muted); } final snapshot = _tripSnapshot; if (snapshot == null) { return Center(child: Text("No data for this trip.").small.muted); } return SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Gap(8), Divider(), Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Row( children: [ Icon(LucideIcons.bus), Gap(8), Text( "Trip ${snapshot.trip.tripNumber} • Duty ${snapshot.trip.dutyNumber}", ).semiBold.textSmall, ], ), ), Divider(), TripDiagram( lineColor: Colors.red, entries: _diagramEntries(snapshot.scheduledStops), leftOffset: 12, rowHeight: 48, ), ], ), ); } return Center( child: Text("Select a filter to preview the schedule.").small.muted, ); }, ), ), Divider(), Padding( padding: const EdgeInsets.all(16), child: Column( children: [ SizedBox( width: double.infinity, height: 50, child: Select( itemBuilder: (context, item) => Text("Filter by: $item"), popupConstraints: BoxConstraints( maxHeight: 300, maxWidth: !isMobile ? 200 : double.infinity, ), onChanged: (value) { setState(() { filterBy = value ?? filterBy; }); }, value: filterBy, popup: SelectPopup( items: SelectItemList( children: [ SelectItemButton(value: "By Duty", child: Text("By Duty")), SelectItemButton(value: "By Bus", child: Text("By Bus")), SelectItemButton(value: "By Trip", child: Text("By Trip")), SelectItemButton(value: "By Stop", child: Text("By Stop")), ], ), ), ), ), Gap(16), SizedBox( width: double.infinity, height: 50, child: Select( itemBuilder: (context, item) { if (filterBy == "By Duty") return Text("Duty $item"); if (filterBy == "By Bus") return Text("Bus $item"); if (filterBy == "By Trip") { final tripNumber = item.toString(); final match = _tripMeta.where((t) => t.tripNumber == tripNumber).firstOrNull; if (match == null) return Text("Trip $tripNumber"); return Text("Trip ${match.tripNumber} • Duty ${match.dutyNumber}"); } if (filterBy == "By Stop") return _stopLabel(item.toString()); return Text("Undefined"); }, popupConstraints: BoxConstraints( maxHeight: 300, maxWidth: !isMobile ? 200 : double.infinity, ), onChanged: (value) { if (value == null) return; setState(() { filterValue[filterBy] = value; _dutyTrips = null; _busTrips = null; _tripSnapshot = null; _stopSchedule = null; }); if (filterBy == "By Duty") { unawaited(_loadDutyDetail(value.toString())); } else if (filterBy == "By Bus") { unawaited(_loadBusDetail(value.toString())); } else if (filterBy == "By Trip") { unawaited(_loadTripDetail(value.toString())); } else if (filterBy == "By Stop") { unawaited(_loadStopDetail(value.toString())); } }, value: filterValue[filterBy], popup: SelectPopup.builder( searchPlaceholder: Text("Search ${filterBy.toLowerCase()}"), builder: (context, searchQuery) { List items = []; if (filterBy == "By Duty") { items = _duties.map((d) => SelectItemButton( value: d, child: Text(d), )).toList(); } else if (filterBy == "By Bus") { items = _busWorkNumbers.map((b) => SelectItemButton( value: b, child: Text(b), )).toList(); } else if (filterBy == "By Trip") { final trips = List.from(_tripMeta); trips.sort((a, b) { final aNum = int.tryParse(a.tripNumber); final bNum = int.tryParse(b.tripNumber); if (aNum != null && bNum != null) return aNum.compareTo(bNum); return a.tripNumber.compareTo(b.tripNumber); }); items = trips.map((t) => SelectItemButton( value: t.tripNumber, child: Text("Trip ${t.tripNumber}"), )).toList(); } else if (filterBy == "By Stop") { items = _stopNames.map((s) => SelectItemButton( value: s, child: _stopLabel(s), )).toList(); } return SelectItemList(children: items); }, ), ), ), ], ), ), Gap(max(bottomPadding, 16)), ], ); } }