import 'package:flutter/material.dart'; import '../../../../api/connect/rmv/rmv_models.dart'; import '../../../../api/errors/error_mapper.dart'; import '../../../../extensions/date_time.dart'; import '../../../../state/app/modules/rmv/repository/rmv_repository.dart'; import '../../../../widget/app_progress_indicator.dart'; import '../widgets/product_chip.dart'; import '../widgets/realtime_time.dart'; class JourneyDetailView extends StatefulWidget { final String journeyRef; final DateTime? date; const JourneyDetailView({super.key, required this.journeyRef, this.date}); @override State createState() => _JourneyDetailViewState(); } class _JourneyDetailViewState extends State { final RmvRepository _repo = RmvRepository(); JourneyDetail? _detail; bool _loading = true; Object? _error; @override void initState() { super.initState(); _load(); } Future _load() async { setState(() { _loading = true; _error = null; }); try { final detail = await _repo.journeyDetail( widget.journeyRef, date: widget.date, ); if (!mounted) return; setState(() { _detail = detail; _loading = false; }); } catch (e) { if (!mounted) return; setState(() { _error = e; _loading = false; }); } } @override Widget build(BuildContext context) { final detail = _detail; return Scaffold( appBar: AppBar( title: Row( children: [ ProductChip(product: detail?.product), const SizedBox(width: 8), Expanded( child: Text( detail?.direction ?? 'Fahrt', overflow: TextOverflow.ellipsis, ), ), ], ), ), body: _body(), ); } Widget _body() { if (_loading) { return const Center(child: AppProgressIndicator.large()); } final err = _error; if (err != null) { return Center( child: Padding( padding: const EdgeInsets.all(24), child: Text( errorToUserMessage(err), textAlign: TextAlign.center, ), ), ); } final stops = _detail?.stops ?? const []; if (stops.isEmpty) { return const Center(child: Text('Keine Halte verfügbar.')); } return RefreshIndicator( onRefresh: _load, child: ListView.separated( itemCount: stops.length, separatorBuilder: (_, _) => const Divider(height: 1), itemBuilder: (_, i) => _stopTile(stops[i], i, stops.length), ), ); } Widget _stopTile(JourneyStop stop, int idx, int total) { final isFirst = idx == 0; final isLast = idx == total - 1; final arrival = stop.realArrival ?? stop.scheduledArrival; final departure = stop.realDeparture ?? stop.scheduledDeparture; final track = (stop.realDepTrack?.isNotEmpty ?? false) ? stop.realDepTrack : stop.depTrack; return ListTile( leading: SizedBox( width: 24, child: Icon( isFirst ? Icons.trip_origin : (isLast ? Icons.place : Icons.fiber_manual_record), size: isFirst || isLast ? 20 : 10, color: stop.cancelled ? Colors.red : Theme.of(context).colorScheme.primary, ), ), title: Text( stop.name, style: TextStyle( decoration: stop.cancelled ? TextDecoration.lineThrough : null, fontWeight: (isFirst || isLast) ? FontWeight.bold : null, ), ), subtitle: (track == null || track.isEmpty) ? null : Text('Gleis $track'), trailing: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.end, children: [ if (stop.scheduledArrival != null) RealtimeTime( scheduled: stop.scheduledArrival!, realtime: stop.realArrival, cancelled: stop.cancelledArrival, style: Theme.of(context).textTheme.bodyMedium, ), if (stop.scheduledDeparture != null && stop.scheduledDeparture != stop.scheduledArrival) RealtimeTime( scheduled: stop.scheduledDeparture!, realtime: stop.realDeparture, cancelled: stop.cancelledDeparture, style: Theme.of(context).textTheme.bodyMedium, ), if (arrival == null && departure == null) const Text('-'), ], ), isThreeLine: arrival != null && departure != null && arrival != departure, ); } } /// Allows showing "departure 14:35" as a tooltip in the journey timeline. String formatStopMoment(DateTime t) => t.formatHm();