import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; import '../providers/client_providers.dart'; import '../../../core/models/marketplace.dart'; import '../../../l10n/app_localizations.dart'; import '../../../widgets/app_back_button.dart'; import '../../../widgets/preferences_bar.dart'; import 'client_shell.dart'; import 'widgets/marketplace_vehicle_card.dart'; import 'widgets/offer_banner.dart'; const _orange = Color(0xFFFF6B00); const _blue = Color(0xFF1A56DB); const _allCarTypes = [ 'ECONOMY', 'COMPACT', 'MIDSIZE', 'FULLSIZE', 'SUV', 'LUXURY', 'VAN', 'TRUCK', ]; String _capitalize(String s) => s.isEmpty ? s : s[0] + s.substring(1).toLowerCase(); // ── Screen ──────────────────────────────────────────────────────────────────── class ClientHomeScreen extends ConsumerStatefulWidget { const ClientHomeScreen({super.key}); @override ConsumerState createState() => _ClientHomeScreenState(); } class _ClientHomeScreenState extends ConsumerState { // Pending form state (committed to provider only when Search is pressed) String? _city; String? _dropoffCity; DateTime? _startDate; DateTime? _endDate; Set _selectedTypes = {}; int _minPriceCents = 0; int _maxPriceCents = 50000; bool _priceFilterActive = false; bool _sameDropoff = true; @override void initState() { super.initState(); final params = ref.read(searchParamsProvider); _city = params.city; _startDate = params.startDate; _endDate = params.endDate; _selectedTypes = Set.from(params.categories ?? {}); _minPriceCents = params.minPriceCents ?? 0; _maxPriceCents = params.maxPriceCents ?? 50000; _priceFilterActive = params.maxPriceCents != null || params.minPriceCents != null; } void _applySearch() { ref.read(searchParamsProvider.notifier).state = SearchParams( city: _city, startDate: _startDate, endDate: _endDate, categories: _selectedTypes.isEmpty ? null : Set.from(_selectedTypes), maxPriceCents: _priceFilterActive ? _maxPriceCents : null, minPriceCents: _priceFilterActive && _minPriceCents > 0 ? _minPriceCents : null, ); } Future _pickDates() async { final cs = Theme.of(context).colorScheme; final now = DateTime.now(); final picked = await showDateRangePicker( context: context, firstDate: now, lastDate: now.add(const Duration(days: 365)), initialDateRange: _startDate != null && _endDate != null ? DateTimeRange(start: _startDate!, end: _endDate!) : DateTimeRange(start: now, end: now.add(const Duration(days: 1))), builder: (context, child) => Theme( data: Theme.of( context, ).copyWith(colorScheme: cs.copyWith(primary: _orange)), child: child!, ), ); if (picked != null && mounted) { setState(() { _startDate = picked.start; _endDate = picked.end; }); } } void _showCitySheet(List cities, AppLocalizations l10n) { showModalBottomSheet( context: context, isScrollControlled: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), builder: (_) => _CitySheet( cities: cities, selected: _city, l10n: l10n, onSelect: (city) { Navigator.pop(context); setState(() => _city = city); }, ), ); } void _showDropoffCitySheet(List cities, AppLocalizations l10n) { showModalBottomSheet( context: context, isScrollControlled: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), builder: (_) => _CitySheet( cities: cities, selected: _dropoffCity, l10n: l10n, titleOverride: l10n.dropoffLocation, onSelect: (city) { Navigator.pop(context); setState(() => _dropoffCity = city); }, ), ); } void _showCarTypeSheet(AppLocalizations l10n) { showModalBottomSheet( context: context, isScrollControlled: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), builder: (_) => _CarTypeSheet( selected: Set.from(_selectedTypes), l10n: l10n, onApply: (types) { Navigator.pop(context); setState(() => _selectedTypes = types); }, ), ); } void _showPriceSheet(AppLocalizations l10n) { showModalBottomSheet( context: context, isScrollControlled: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), builder: (_) => _PriceSheet( minCents: _minPriceCents, maxCents: _maxPriceCents, active: _priceFilterActive, l10n: l10n, onApply: (min, max, active) { Navigator.pop(context); setState(() { _minPriceCents = min; _maxPriceCents = max; _priceFilterActive = active; }); }, ), ); } List _applyClientFilters( List all, SearchParams params, ) { var result = all; final cats = params.categories; if (cats != null && cats.length > 1) { result = result.where((v) => cats.contains(v.category)).toList(); } final minP = params.minPriceCents; if (minP != null && minP > 0) { result = result.where((v) => v.dailyRateCents >= minP).toList(); } return result; } @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final l10n = AppLocalizations.of(context); final params = ref.watch(searchParamsProvider); final searchAsync = ref.watch(vehicleSearchProvider(params)); final citiesAsync = ref.watch(citiesProvider); final langCode = Localizations.localeOf(context).languageCode; final typeLabel = _selectedTypes.isEmpty ? l10n.allCarTypes : _selectedTypes.length == 1 ? _capitalize(_selectedTypes.first) : '${_selectedTypes.length} types'; final priceLabel = _priceFilterActive ? '${(_minPriceCents / 100).toStringAsFixed(0)} – ${(_maxPriceCents / 100).toStringAsFixed(0)} MAD/day' : l10n.anyPrice; return ClientShell( backgroundColor: cs.surface, body: CustomScrollView( slivers: [ // ── Search form ────────────────────────────────────────────────── SliverToBoxAdapter( child: SafeArea( bottom: false, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Title + preferences Padding( padding: const EdgeInsets.fromLTRB(20, 16, 12, 0), child: Row( children: [ AppBackButton( fallbackRoute: '/landing', color: cs.onSurface, ), const SizedBox(width: 4), Expanded( child: Text( l10n.findCarToRent, style: TextStyle( color: cs.onSurface, fontSize: 20, fontWeight: FontWeight.bold, ), ), ), const PreferencesBar(), ], ), ), const SizedBox(height: 20), // Same / Different drop-off toggle Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Row( children: [ _DropoffTab( label: l10n.sameDropoff, active: _sameDropoff, onTap: () => setState(() { _sameDropoff = true; _dropoffCity = null; }), ), const SizedBox(width: 28), _DropoffTab( label: l10n.differentDropoff, active: !_sameDropoff, onTap: () => setState(() => _sameDropoff = false), ), ], ), ), const SizedBox(height: 16), // Location field(s) Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( children: [ // Pickup _SearchInputBox( onTap: () => citiesAsync.whenData( (c) => _showCitySheet(c, l10n), ), child: _LocationRow( icon: Icons.radio_button_checked, iconColor: _orange, text: _city ?? (_sameDropoff ? l10n.findCarToRent : l10n.pickupLocation), placeholder: _city == null, cs: cs, onClear: _city != null ? () => setState(() => _city = null) : null, ), ), // Animated drop-off field AnimatedSize( duration: const Duration(milliseconds: 250), curve: Curves.easeInOut, child: _sameDropoff ? const SizedBox.shrink() : Column( children: [ // Connector line Row( children: [ const SizedBox(width: 23), Container( width: 2, height: 16, color: cs.outlineVariant, ), ], ), // Drop-off box _SearchInputBox( onTap: () => citiesAsync.whenData( (c) => _showDropoffCitySheet(c, l10n), ), child: _LocationRow( icon: Icons.location_on, iconColor: _blue, text: _dropoffCity ?? l10n.dropoffLocation, placeholder: _dropoffCity == null, cs: cs, onClear: _dropoffCity != null ? () => setState( () => _dropoffCity = null, ) : null, ), ), ], ), ), ], ), ), const SizedBox(height: 10), // Date range field Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: _SearchInputBox( onTap: _pickDates, child: _startDate != null && _endDate != null ? _DateRangeRow( start: _startDate!, end: _endDate!, langCode: langCode, cs: cs, ) : Row( children: [ Icon( Icons.calendar_today_outlined, size: 18, color: cs.onSurfaceVariant, ), const SizedBox(width: 12), Text( l10n.pickDates, style: TextStyle( color: cs.onSurfaceVariant, fontSize: 15, ), ), ], ), ), ), const SizedBox(height: 10), // Car type + Price chips Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( children: [ Expanded( child: _OptionTile( label: typeLabel, icon: Icons.directions_car_outlined, active: _selectedTypes.isNotEmpty, onTap: () => _showCarTypeSheet(l10n), onClear: _selectedTypes.isNotEmpty ? () => setState(() => _selectedTypes = {}) : null, ), ), const SizedBox(width: 10), Expanded( child: _OptionTile( label: priceLabel, icon: Icons.attach_money_rounded, active: _priceFilterActive, onTap: () => _showPriceSheet(l10n), onClear: _priceFilterActive ? () => setState(() { _priceFilterActive = false; _minPriceCents = 0; _maxPriceCents = 50000; }) : null, ), ), ], ), ), const SizedBox(height: 20), // Search button Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: SizedBox( width: double.infinity, height: 54, child: ElevatedButton( onPressed: _applySearch, style: ElevatedButton.styleFrom( backgroundColor: _orange, foregroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), elevation: 0, ), child: Text( l10n.search, style: const TextStyle( fontSize: 17, fontWeight: FontWeight.bold, ), ), ), ), ), const SizedBox(height: 16), // Results count line Padding( padding: const EdgeInsets.fromLTRB(20, 0, 20, 10), child: searchAsync.when( loading: () => Row( children: [ SizedBox( width: 12, height: 12, child: CircularProgressIndicator( strokeWidth: 1.5, color: cs.onSurfaceVariant, ), ), const SizedBox(width: 8), Text( l10n.searching, style: TextStyle( color: cs.onSurfaceVariant, fontSize: 13, ), ), ], ), error: (_, _) => const SizedBox.shrink(), data: (r) { final count = _applyClientFilters( r.data, params, ).length; return Text( l10n.vehiclesFound(count), style: TextStyle( color: cs.onSurfaceVariant, fontSize: 13, ), ); }, ), ), Divider(height: 1, color: cs.outlineVariant), ], ), ), ), // ── Active offers ──────────────────────────────────────────────── ref .watch(offersProvider) .when( loading: () => const SliverToBoxAdapter(child: SizedBox.shrink()), error: (_, _) => const SliverToBoxAdapter(child: SizedBox.shrink()), data: (offers) { if (offers.isEmpty) { return const SliverToBoxAdapter(child: SizedBox.shrink()); } return SliverToBoxAdapter( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 10), child: Text( 'Special Offers', style: TextStyle( fontSize: 15, fontWeight: FontWeight.bold, color: cs.onSurface, ), ), ), SizedBox( height: 110, child: ListView.separated( scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: 16), itemCount: offers.length, separatorBuilder: (_, _) => const SizedBox(width: 10), itemBuilder: (_, i) => OfferBanner(offer: offers[i]), ), ), const SizedBox(height: 4), ], ), ); }, ), // ── Results ────────────────────────────────────────────────────── searchAsync.when( loading: () => const SliverFillRemaining( child: Center( child: CircularProgressIndicator( color: _orange, strokeWidth: 2.5, ), ), ), error: (_, _) => SliverFillRemaining( child: _ErrorBody( onRetry: () => ref.invalidate(vehicleSearchProvider(params)), l10n: l10n, cs: cs, ), ), data: (result) { final vehicles = _applyClientFilters(result.data, params); if (vehicles.isEmpty) { return SliverFillRemaining( child: _EmptyBody(l10n: l10n, cs: cs), ); } return SliverPadding( padding: const EdgeInsets.fromLTRB(16, 12, 16, 32), sliver: SliverList.separated( itemCount: vehicles.length, separatorBuilder: (_, _) => const SizedBox(height: 12), itemBuilder: (_, i) { final v = vehicles[i]; return MarketplaceVehicleCard( vehicle: v, selectedStart: params.startDate, selectedEnd: params.endDate, onTap: () => context.push('/client/vehicles/${v.id}', extra: v), ); }, ), ); }, ), ], ), ); } } // ── Drop-off toggle tab ─────────────────────────────────────────────────────── class _DropoffTab extends StatelessWidget { final String label; final bool active; final VoidCallback onTap; const _DropoffTab({ required this.label, required this.active, required this.onTap, }); @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; return GestureDetector( onTap: onTap, child: Text( label, style: TextStyle( fontSize: 17, fontWeight: active ? FontWeight.w700 : FontWeight.w400, color: active ? cs.onSurface : cs.onSurfaceVariant, ), ), ); } } // ── Rounded input container ─────────────────────────────────────────────────── class _SearchInputBox extends StatelessWidget { final VoidCallback onTap; final Widget child; const _SearchInputBox({required this.onTap, required this.child}); @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; return GestureDetector( onTap: onTap, child: Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 18), decoration: BoxDecoration( color: cs.surfaceContainerHigh, borderRadius: BorderRadius.circular(14), border: Border.all(color: cs.outlineVariant), ), child: child, ), ); } } // ── Location row (used in pickup + drop-off fields) ─────────────────────────── class _LocationRow extends StatelessWidget { final IconData icon; final Color iconColor; final String text; final bool placeholder; final ColorScheme cs; final VoidCallback? onClear; const _LocationRow({ required this.icon, required this.iconColor, required this.text, required this.placeholder, required this.cs, this.onClear, }); @override Widget build(BuildContext context) => Row( children: [ Icon(icon, size: 18, color: iconColor), const SizedBox(width: 12), Expanded( child: Text( text, style: TextStyle( color: placeholder ? cs.onSurfaceVariant : cs.onSurface, fontSize: 15, ), ), ), if (onClear != null) GestureDetector( onTap: onClear, child: Icon(Icons.close, size: 16, color: cs.onSurfaceVariant), ), ], ); } // ── Date range display ──────────────────────────────────────────────────────── class _DateRangeRow extends StatelessWidget { final DateTime start; final DateTime end; final String langCode; final ColorScheme cs; const _DateRangeRow({ required this.start, required this.end, required this.langCode, required this.cs, }); @override Widget build(BuildContext context) { final dayFmt = DateFormat('MMM d', langCode); final dowFmt = DateFormat('EEE', langCode); return Row( children: [ _DateChunk( date: dayFmt.format(start), sub: '${dowFmt.format(start)} 12:00', cs: cs, ), Padding( padding: const EdgeInsets.symmetric(horizontal: 10), child: Icon( Icons.arrow_forward, size: 14, color: cs.onSurfaceVariant, ), ), _DateChunk( date: dayFmt.format(end), sub: '${dowFmt.format(end)} 12:00', cs: cs, ), ], ); } } class _DateChunk extends StatelessWidget { final String date; final String sub; final ColorScheme cs; const _DateChunk({required this.date, required this.sub, required this.cs}); @override Widget build(BuildContext context) => Row( children: [ Text( date, style: TextStyle( color: cs.onSurface, fontSize: 16, fontWeight: FontWeight.w600, ), ), const SizedBox(width: 6), Text(sub, style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13)), ], ); } // ── Option tile (car type / price) ──────────────────────────────────────────── class _OptionTile extends StatelessWidget { final String label; final IconData icon; final bool active; final VoidCallback onTap; final VoidCallback? onClear; const _OptionTile({ required this.label, required this.icon, required this.active, required this.onTap, this.onClear, }); @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; return GestureDetector( onTap: onTap, child: AnimatedContainer( duration: const Duration(milliseconds: 180), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14), decoration: BoxDecoration( color: active ? _blue.withValues(alpha: 0.12) : cs.surfaceContainerHigh, borderRadius: BorderRadius.circular(14), border: Border.all(color: active ? _blue : cs.outlineVariant), ), child: Row( children: [ Icon(icon, size: 16, color: active ? _blue : cs.onSurfaceVariant), const SizedBox(width: 8), Expanded( child: Text( label, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 13, fontWeight: active ? FontWeight.w600 : FontWeight.normal, color: cs.onSurface, ), ), ), if (onClear != null) ...[ const SizedBox(width: 4), GestureDetector( onTap: onClear, child: Icon( Icons.close, size: 14, color: active ? _blue : cs.onSurfaceVariant, ), ), ], ], ), ), ); } } // ── Car Type Sheet ──────────────────────────────────────────────────────────── class _CarTypeSheet extends StatefulWidget { final Set selected; final AppLocalizations l10n; final ValueChanged> onApply; const _CarTypeSheet({ required this.selected, required this.l10n, required this.onApply, }); @override State<_CarTypeSheet> createState() => _CarTypeSheetState(); } class _CarTypeSheetState extends State<_CarTypeSheet> { late Set _selected; @override void initState() { super.initState(); _selected = Set.from(widget.selected); } bool get _allSelected => _selected.length == _allCarTypes.length; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final l10n = widget.l10n; return DraggableScrollableSheet( initialChildSize: 0.7, maxChildSize: 0.9, minChildSize: 0.5, expand: false, builder: (_, controller) => Column( children: [ const SizedBox(height: 12), _SheetHandle(cs: cs), Padding( padding: const EdgeInsets.fromLTRB(20, 14, 20, 6), child: Row( children: [ Text( l10n.carType, style: TextStyle( color: cs.onSurface, fontSize: 17, fontWeight: FontWeight.bold, ), ), const Spacer(), TextButton( onPressed: () { setState(() { if (_allSelected) { _selected = {}; } else { _selected = Set.from(_allCarTypes); } }); }, style: TextButton.styleFrom( foregroundColor: _orange, padding: EdgeInsets.zero, minimumSize: Size.zero, tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), child: Text( _allSelected ? l10n.clearAll : l10n.selectAll, style: const TextStyle(fontSize: 14), ), ), ], ), ), Expanded( child: ListView( controller: controller, children: _allCarTypes.map((type) { final checked = _selected.contains(type); return CheckboxListTile( value: checked, onChanged: (_) { setState(() { if (checked) { _selected.remove(type); } else { _selected.add(type); } }); }, title: Text( _capitalize(type), style: TextStyle(color: cs.onSurface, fontSize: 15), ), activeColor: _orange, checkColor: Colors.white, side: BorderSide(color: cs.outlineVariant), controlAffinity: ListTileControlAffinity.leading, ); }).toList(), ), ), Padding( padding: EdgeInsets.fromLTRB( 16, 10, 16, 16 + MediaQuery.of(context).padding.bottom, ), child: SizedBox( width: double.infinity, height: 50, child: ElevatedButton( onPressed: () => widget.onApply(_selected), style: ElevatedButton.styleFrom( backgroundColor: _orange, foregroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(14), ), elevation: 0, ), child: Text( l10n.apply, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), ), ), ), ], ), ); } } // ── Price Sheet ─────────────────────────────────────────────────────────────── class _PriceSheet extends StatefulWidget { final int minCents; final int maxCents; final bool active; final AppLocalizations l10n; final void Function(int min, int max, bool active) onApply; static const double _cap = 500; // $500/day cap const _PriceSheet({ required this.minCents, required this.maxCents, required this.active, required this.l10n, required this.onApply, }); @override State<_PriceSheet> createState() => _PriceSheetState(); } class _PriceSheetState extends State<_PriceSheet> { late RangeValues _range; @override void initState() { super.initState(); _range = RangeValues( (widget.minCents / 100).clamp(0, _PriceSheet._cap).toDouble(), (widget.maxCents / 100).clamp(0, _PriceSheet._cap).toDouble(), ); } String _fmt(double v) => v >= _PriceSheet._cap ? '${v.toStringAsFixed(0)}+ MAD' : '${v.toStringAsFixed(0)} MAD'; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final l10n = widget.l10n; return Padding( padding: EdgeInsets.only( bottom: MediaQuery.of(context).viewInsets.bottom, ), child: Column( mainAxisSize: MainAxisSize.min, children: [ const SizedBox(height: 12), _SheetHandle(cs: cs), Padding( padding: const EdgeInsets.fromLTRB(20, 14, 20, 6), child: Row( children: [ Text( l10n.price, style: TextStyle( color: cs.onSurface, fontSize: 17, fontWeight: FontWeight.bold, ), ), const Spacer(), if (widget.active) TextButton( onPressed: () => widget.onApply(0, 50000, false), style: TextButton.styleFrom( foregroundColor: _orange, padding: EdgeInsets.zero, minimumSize: Size.zero, tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), child: Text( l10n.clearAll, style: const TextStyle(fontSize: 14), ), ), ], ), ), // Min / max labels Padding( padding: const EdgeInsets.symmetric(horizontal: 24), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ _PriceTag(label: _fmt(_range.start), cs: cs), Icon(Icons.arrow_forward, size: 14, color: cs.onSurfaceVariant), _PriceTag(label: '${_fmt(_range.end)}/day', cs: cs), ], ), ), const SizedBox(height: 8), // Range slider Padding( padding: const EdgeInsets.symmetric(horizontal: 8), child: SliderTheme( data: SliderTheme.of(context).copyWith( activeTrackColor: _orange, inactiveTrackColor: cs.outlineVariant, thumbColor: _orange, overlayColor: _orange.withValues(alpha: 0.15), trackHeight: 4, ), child: RangeSlider( values: _range, min: 0, max: _PriceSheet._cap, divisions: 50, onChanged: (v) => setState(() => _range = v), ), ), ), const SizedBox(height: 8), Padding( padding: EdgeInsets.fromLTRB( 16, 4, 16, 16 + MediaQuery.of(context).padding.bottom, ), child: SizedBox( width: double.infinity, height: 50, child: ElevatedButton( onPressed: () => widget.onApply( (_range.start * 100).round(), (_range.end * 100).round(), true, ), style: ElevatedButton.styleFrom( backgroundColor: _orange, foregroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(14), ), elevation: 0, ), child: Text( l10n.apply, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), ), ), ), ], ), ); } } class _PriceTag extends StatelessWidget { final String label; final ColorScheme cs; const _PriceTag({required this.label, required this.cs}); @override Widget build(BuildContext context) => Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: cs.surfaceContainerHigh, borderRadius: BorderRadius.circular(8), border: Border.all(color: cs.outlineVariant), ), child: Text( label, style: TextStyle( color: cs.onSurface, fontWeight: FontWeight.w600, fontSize: 15, ), ), ); } // ── City Sheet ──────────────────────────────────────────────────────────────── class _CitySheet extends StatefulWidget { final List cities; final String? selected; final AppLocalizations l10n; final ValueChanged onSelect; final String? titleOverride; const _CitySheet({ required this.cities, required this.selected, required this.l10n, required this.onSelect, this.titleOverride, }); @override State<_CitySheet> createState() => _CitySheetState(); } class _CitySheetState extends State<_CitySheet> { String _query = ''; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final filtered = widget.cities .where((c) => c.toLowerCase().contains(_query.toLowerCase())) .toList(); return DraggableScrollableSheet( initialChildSize: 0.6, maxChildSize: 0.9, minChildSize: 0.4, expand: false, builder: (_, controller) => Column( children: [ const SizedBox(height: 12), _SheetHandle(cs: cs), Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( widget.titleOverride ?? widget.l10n.selectCity, style: TextStyle( fontSize: 17, fontWeight: FontWeight.bold, color: cs.onSurface, ), ), const SizedBox(height: 12), TextField( autofocus: true, style: TextStyle(color: cs.onSurface), decoration: InputDecoration( hintText: widget.l10n.searchCities, prefixIcon: Icon( Icons.search, size: 20, color: cs.onSurfaceVariant, ), contentPadding: const EdgeInsets.symmetric(vertical: 8), ), onChanged: (v) => setState(() => _query = v), ), ], ), ), Expanded( child: ListView( controller: controller, children: [ ListTile( leading: Icon( Icons.public_outlined, color: cs.onSurfaceVariant, ), title: Text( widget.l10n.allCities, style: TextStyle(color: cs.onSurface), ), selected: widget.selected == null, selectedColor: _orange, trailing: widget.selected == null ? const Icon(Icons.check, color: _orange, size: 18) : null, onTap: () => widget.onSelect(null), ), ...filtered.map( (city) => ListTile( leading: Icon( Icons.location_city_outlined, color: cs.onSurfaceVariant, ), title: Text(city, style: TextStyle(color: cs.onSurface)), selected: widget.selected == city, selectedColor: _orange, trailing: widget.selected == city ? const Icon(Icons.check, color: _orange, size: 18) : null, onTap: () => widget.onSelect(city), ), ), ], ), ), ], ), ); } } // ── Shared sheet drag handle ────────────────────────────────────────────────── class _SheetHandle extends StatelessWidget { final ColorScheme cs; const _SheetHandle({required this.cs}); @override Widget build(BuildContext context) => Container( width: 40, height: 4, decoration: BoxDecoration( color: cs.outlineVariant, borderRadius: BorderRadius.circular(2), ), ); } // ── Error / Empty states ───────────────────────────────────────────────────── class _ErrorBody extends StatelessWidget { final VoidCallback onRetry; final AppLocalizations l10n; final ColorScheme cs; const _ErrorBody({ required this.onRetry, required this.l10n, required this.cs, }); @override Widget build(BuildContext context) => Center( child: Padding( padding: const EdgeInsets.all(32), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.wifi_off_outlined, size: 48, color: cs.onSurfaceVariant), const SizedBox(height: 12), Text( l10n.couldNotLoadVehicles, style: TextStyle( color: cs.onSurface, fontSize: 16, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 8), Text( l10n.checkConnectionRetry, style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13), textAlign: TextAlign.center, ), const SizedBox(height: 20), OutlinedButton( onPressed: onRetry, style: OutlinedButton.styleFrom( foregroundColor: _orange, side: const BorderSide(color: _orange), ), child: Text(l10n.retry), ), ], ), ), ); } class _EmptyBody extends StatelessWidget { final AppLocalizations l10n; final ColorScheme cs; const _EmptyBody({required this.l10n, required this.cs}); @override Widget build(BuildContext context) => Center( child: Padding( padding: const EdgeInsets.all(32), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.search_off, size: 56, color: cs.outlineVariant), const SizedBox(height: 16), Text( l10n.noVehiclesFound, style: TextStyle( color: cs.onSurface, fontSize: 16, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 8), Text( l10n.tryDifferentSearch, style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13), textAlign: TextAlign.center, ), ], ), ), ); }