import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../../api/connect/rmv/rmv_models.dart'; import '../../../../api/errors/error_mapper.dart'; import '../../../../api/geocoding/nominatim_result.dart'; import '../../../../api/geocoding/nominatim_search.dart'; import '../../../../state/app/modules/commute/repository/commute_repository.dart'; import '../../../../state/app/modules/rmv/repository/rmv_repository.dart'; import '../../../../state/app/modules/settings/bloc/settings_cubit.dart'; import '../../../../widget/app_progress_indicator.dart'; import '../../../../widget/centered_leading.dart'; import '../../rmv/widgets/station_picker_sheet.dart'; /// Settings block for the timetable-commute prototype. Toggle + home address /// flow (Nominatim → nearbyStops) + school station picker + walking buffer. class CommuteSettingsSection extends StatelessWidget { const CommuteSettingsSection({super.key}); @override Widget build(BuildContext context) { final settings = context.watch(); final s = settings.val().timetableSettings; return Column( children: [ SwitchListTile( secondary: const Icon(Icons.directions_bus_outlined), title: const Text('Pendel-Verbindung im Stundenplan'), subtitle: const Text( 'Zeigt für jeden Schultag die ÖPNV-Verbindung von und zur Schule.', ), value: s.showCommuteInTimetable, onChanged: (v) => _toggle(context, v), ), if (s.showCommuteInTimetable) ...[ ListTile( leading: const CenteredLeading(Icon(Icons.home_outlined)), title: const Text('Heimat-Haltestelle'), subtitle: Text(_homeSubtitle(s.homeAddressLabel, s.homeStation)), trailing: const Icon(Icons.edit_outlined), onTap: () => _editHome(context), ), ListTile( leading: const CenteredLeading(Icon(Icons.school_outlined)), title: const Text('Schul-Haltestelle'), subtitle: Text(s.schoolStation?.name ?? 'Noch nicht gesetzt'), trailing: const Icon(Icons.edit_outlined), onTap: () => _editSchool(context), ), ListTile( leading: const CenteredLeading(Icon(Icons.timer_outlined)), title: const Text('Pufferzeit'), subtitle: Text( '${s.commuteBufferMinutes} Min Fußweg vor/nach dem Schultag', ), trailing: const Icon(Icons.edit_outlined), onTap: () => _editBuffer(context), ), ], ], ); } String _homeSubtitle(String? label, StopLocation? home) { if (home == null) return 'Noch nicht gesetzt'; if (label == null || label.isEmpty) return home.name; return '${home.name}\n($label)'; } Future _toggle(BuildContext context, bool value) async { final settings = context.read(); settings.val(write: true).timetableSettings.showCommuteInTimetable = value; if (!value) return; final current = settings.val().timetableSettings.schoolStation; if (current != null) return; // Best-effort default resolve so the user doesn't have to pick the // school station manually if the RMV knows "Marianum". try { final resolved = await CommuteRepository().resolveDefaultSchoolStation(); if (resolved == null || !context.mounted) return; settings.val(write: true).timetableSettings.schoolStation = resolved; } catch (_) { // Silent: settings tile still shows "Noch nicht gesetzt" + edit option. } } Future _editSchool(BuildContext context) async { final picked = await showStationPickerSheet( context, title: 'Schul-Haltestelle wählen', ); if (picked == null || !context.mounted) return; context.read().val(write: true).timetableSettings.schoolStation = picked; } Future _editBuffer(BuildContext context) async { final settings = context.read(); final current = settings.val().timetableSettings.commuteBufferMinutes; final picked = await showDialog( context: context, builder: (_) => _BufferPickerDialog(initial: current), ); if (picked == null) return; settings.val(write: true).timetableSettings.commuteBufferMinutes = picked; } Future _editHome(BuildContext context) async { final picked = await showModalBottomSheet<_HomeSelection>( context: context, isScrollControlled: true, showDragHandle: true, useSafeArea: true, builder: (_) => const _HomeAddressFlow(), ); if (picked == null || !context.mounted) return; final settings = context.read(); settings.val(write: true).timetableSettings ..homeAddressLabel = picked.addressLabel ..homeStation = picked.stop; } } class _HomeSelection { final String addressLabel; final StopLocation stop; const _HomeSelection({required this.addressLabel, required this.stop}); } class _BufferPickerDialog extends StatefulWidget { final int initial; const _BufferPickerDialog({required this.initial}); @override State<_BufferPickerDialog> createState() => _BufferPickerDialogState(); } class _BufferPickerDialogState extends State<_BufferPickerDialog> { late double _value = widget.initial.toDouble(); @override Widget build(BuildContext context) => AlertDialog( title: const Text('Pufferzeit'), content: Column( mainAxisSize: MainAxisSize.min, children: [ Text( '${_value.round()} Minuten Fußweg', style: Theme.of(context).textTheme.titleLarge, ), Slider( min: 0, max: 30, divisions: 30, value: _value, label: '${_value.round()} min', onChanged: (v) => setState(() => _value = v), ), ], ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Abbrechen'), ), FilledButton( onPressed: () => Navigator.of(context).pop(_value.round()), child: const Text('Übernehmen'), ), ], ); } /// Two-step picker: address search via Nominatim → choose address → /// nearbyStops → choose stop. Returns the chosen [_HomeSelection] via /// Navigator.pop, or null if dismissed. class _HomeAddressFlow extends StatefulWidget { const _HomeAddressFlow(); @override State<_HomeAddressFlow> createState() => _HomeAddressFlowState(); } class _HomeAddressFlowState extends State<_HomeAddressFlow> { final _queryCtrl = TextEditingController(); final NominatimSearch _geo = NominatimSearch(); final RmvRepository _rmv = RmvRepository(); List? _addresses; List? _stops; NominatimResult? _chosenAddress; bool _loading = false; Object? _error; @override void dispose() { _queryCtrl.dispose(); super.dispose(); } Future _searchAddress() async { final q = _queryCtrl.text.trim(); if (q.length < 3) return; setState(() { _loading = true; _error = null; _stops = null; _chosenAddress = null; }); try { final res = await _geo.run(q, limit: 5); if (!mounted) return; setState(() { _addresses = res; _loading = false; }); } catch (e) { if (!mounted) return; setState(() { _error = e; _loading = false; }); } } Future _pickAddress(NominatimResult addr) async { setState(() { _chosenAddress = addr; _loading = true; _error = null; }); try { final stops = await _rmv.nearbyStops( lat: addr.lat, lon: addr.lon, radiusMeters: 800, max: 8, ); if (!mounted) return; stops.sort( (a, b) => (a.distanceMeters ?? 0).compareTo(b.distanceMeters ?? 0), ); setState(() { _stops = stops; _loading = false; }); } catch (e) { if (!mounted) return; setState(() { _error = e; _loading = false; }); } } void _pickStop(StopLocation stop) { final addr = _chosenAddress; if (addr == null) return; Navigator.of(context).pop( _HomeSelection(addressLabel: addr.displayName, stop: stop), ); } @override Widget build(BuildContext context) { final viewInsets = MediaQuery.of(context).viewInsets; return Padding( padding: EdgeInsets.only(bottom: viewInsets.bottom), child: Column( mainAxisSize: MainAxisSize.min, children: [ Padding( padding: const EdgeInsets.fromLTRB(16, 4, 16, 8), child: Text( 'Heimadresse einrichten', style: Theme.of(context).textTheme.titleLarge, ), ), const Padding( padding: EdgeInsets.symmetric(horizontal: 16), child: Text( 'Adresse wird zur Suche an OpenStreetMap (Nominatim) übermittelt.', style: TextStyle(fontSize: 12), ), ), const SizedBox(height: 8), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( children: [ Expanded( child: TextField( controller: _queryCtrl, autofocus: true, textInputAction: TextInputAction.search, onSubmitted: (_) => _searchAddress(), decoration: InputDecoration( hintText: 'Straße, Hausnr., Ort', prefixIcon: const Icon(Icons.home_outlined), border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), ), ), ), ), const SizedBox(width: 8), FilledButton( onPressed: _loading ? null : _searchAddress, child: const Text('Suchen'), ), ], ), ), const SizedBox(height: 8), ConstrainedBox( constraints: BoxConstraints( maxHeight: MediaQuery.of(context).size.height * 0.55, minHeight: 180, ), child: _body(), ), ], ), ); } Widget _body() { if (_loading) return const Center(child: AppProgressIndicator.medium()); if (_error != null) { return Padding( padding: const EdgeInsets.all(16), child: Text( errorToUserMessage(_error), style: TextStyle(color: Theme.of(context).colorScheme.error), ), ); } final stops = _stops; if (stops != null) return _stopsList(stops); final addresses = _addresses; if (addresses != null) return _addressList(addresses); return const Center( child: Padding( padding: EdgeInsets.all(16), child: Text( 'Gib deine Adresse ein und tippe "Suchen", um die nächstgelegenen RMV-Haltestellen zu finden.', textAlign: TextAlign.center, ), ), ); } Widget _addressList(List addresses) { if (addresses.isEmpty) { return const Center( child: Padding( padding: EdgeInsets.all(16), child: Text('Keine Adresse zur Suche gefunden.'), ), ); } return ListView.separated( itemCount: addresses.length, separatorBuilder: (_, _) => const Divider(height: 1), itemBuilder: (_, i) => ListTile( leading: const CenteredLeading(Icon(Icons.place_outlined)), title: Text(addresses[i].displayName), onTap: () => _pickAddress(addresses[i]), ), ); } Widget _stopsList(List stops) { if (stops.isEmpty) { return const Center( child: Padding( padding: EdgeInsets.all(16), child: Text('Keine Haltestelle in 800 m Umkreis gefunden.'), ), ); } return ListView.separated( itemCount: stops.length, separatorBuilder: (_, _) => const Divider(height: 1), itemBuilder: (_, i) { final s = stops[i]; return ListTile( leading: const CenteredLeading(Icon(Icons.directions_transit)), title: Text(s.name), subtitle: s.distanceMeters == null ? null : Text('${s.distanceMeters} m'), onTap: () => _pickStop(s), ); }, ); } }