import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:geolocator/geolocator.dart' hide ServiceStatus; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:http/http.dart' as http; import 'package:intl/intl.dart'; import 'package:prosapp_web_app/models/schedules_entity.dart'; import 'package:prosapp_web_app/models/service.dart'; import 'package:prosapp_web_app/models/location_preferences.dart'; import 'package:prosapp_web_app/models/service_status.dart'; import 'package:prosapp_web_app/models/usuario.dart'; import 'package:prosapp_web_app/models/usuario_profesional.dart'; import 'package:prosapp_web_app/providers/auth_provider.dart'; import 'package:prosapp_web_app/providers/cities_provider.dart'; import 'package:prosapp_web_app/providers/professionals_provider.dart'; import 'package:prosapp_web_app/providers/theme_provider.dart'; import 'package:prosapp_web_app/router/router.dart'; import 'package:prosapp_web_app/services/api_service.dart'; import 'package:prosapp_web_app/services/maps_service.dart'; import 'package:prosapp_web_app/services/navigation_service.dart'; import 'package:prosapp_web_app/services/notifications_service.dart'; import 'package:prosapp_web_app/utils/network_utility.dart'; import 'package:prosapp_web_app/utils/time_of_day_extension.dart'; import 'package:provider/provider.dart'; class DashboardView extends StatefulWidget { const DashboardView({super.key}); @override State createState() => _DashboardViewState(); } class _DashboardViewState extends State { Usuario? user; // Map GoogleMapController? _mapController; LatLng _mapCenter = const LatLng(6.2442, -75.5812); // Medellín — centro geográfico de Colombia como fallback neutro bool _mapsReady = false; bool _mapsLoading = true; bool _geocoding = false; String _currentAddress = ''; String? _mapsApiKey; // Search / autocomplete final _searchController = TextEditingController(); final _searchFocus = FocusNode(); List _suggestions = []; Timer? _debounce; // Booking UsuarioProfesional? _professional; DateTime? _selectedDay; TimeOfDay? _selectedHour; bool _requesting = false; // For professionals that accept both office and delivery, the patient chooses. // true = delivery, false = office. Meaningless when professional only accepts one. bool _bookAsDelivery = false; // Throttle reverse geocode on camera idle DateTime _lastGeocode = DateTime(0); // Indica si _mapCenter ya fue fijado por GPS o geocoding (no el default) bool _mapPositioned = false; // Ciudad detectada, disponibilidad y coincidencia con perfil String _detectedCity = ''; bool _cityAvailable = true; bool _cityMismatch = false; @override void initState() { super.initState(); user = Provider.of(context, listen: false).user; _initMaps(); } Future _initMaps() async { final ok = await MapsService.load(); if (!mounted) return; try { final data = await ApiService.instance.get('/settings/maps-key'); _mapsApiKey = data['api_key'] as String? ?? ''; } catch (_) {} // Pre-position map on user's city so autocomplete bias is correct from the start await _centerOnUserCity(); setState(() { _mapsReady = ok; _mapsLoading = false; }); if (ok) _detectLocation(); } Future _detectLocation() async { try { bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (!serviceEnabled) { await _centerOnUserCity(); return; } LocationPermission permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { permission = await Geolocator.requestPermission(); if (permission == LocationPermission.denied) { await _centerOnUserCity(); return; } } if (permission == LocationPermission.deniedForever) { await _centerOnUserCity(); return; } final pos = await Geolocator.getCurrentPosition( locationSettings: const LocationSettings( accuracy: LocationAccuracy.medium, timeLimit: Duration(seconds: 8), ), ); final latlng = LatLng(pos.latitude, pos.longitude); if (mounted) setState(() { _mapCenter = latlng; _mapPositioned = true; }); _mapController?.animateCamera(CameraUpdate.newLatLngZoom(latlng, 16)); _reverseGeocode(latlng); } catch (_) { await _centerOnUserCity(); } } static const Map _colombiaCityCoords = { 'bucaramanga': LatLng(7.1198, -73.1227), 'bogota': LatLng(4.7110, -74.0721), 'medellin': LatLng(6.2442, -75.5812), 'cali': LatLng(3.4516, -76.5320), 'barranquilla': LatLng(10.9639, -74.7964), 'cartagena': LatLng(10.3932, -75.4832), 'cucuta': LatLng(7.8939, -72.5078), 'pereira': LatLng(4.8087, -75.6906), 'manizales': LatLng(5.0703, -75.5138), 'santa marta': LatLng(11.2408, -74.1990), 'ibague': LatLng(4.4389, -75.2322), 'villavicencio': LatLng(4.1420, -73.6266), 'pasto': LatLng(1.2136, -77.2811), 'monteria': LatLng(8.7575, -75.8845), 'neiva': LatLng(2.9273, -75.2819), 'armenia': LatLng(4.5339, -75.6811), 'sincelejo': LatLng(9.3047, -75.3978), 'tunja': LatLng(5.5353, -73.3678), 'floridablanca': LatLng(7.0640, -73.0868), 'giron': LatLng(7.0730, -73.1701), 'piedecuesta': LatLng(6.9907, -73.0494), 'soledad': LatLng(10.9200, -74.7647), 'bello': LatLng(6.3367, -75.5572), 'soacha': LatLng(4.5797, -74.2172), 'buenaventura': LatLng(3.8833, -77.0311), 'valledupar': LatLng(10.4779, -73.2536), 'palmira': LatLng(3.5394, -76.3035), 'popayan': LatLng(2.4448, -76.6147), 'riohacha': LatLng(11.5444, -72.9072), 'quibdo': LatLng(5.6940, -76.6583), }; LatLng? _cityFallbackCoords(String city) { String norm(String s) => s.toLowerCase().trim() .replaceAll(RegExp(r'[áà]'), 'a').replaceAll(RegExp(r'[éè]'), 'e') .replaceAll(RegExp(r'[íì]'), 'i').replaceAll(RegExp(r'[óò]'), 'o') .replaceAll(RegExp(r'[úù]'), 'u').replaceAll('.', '').replaceAll(',', ''); final normalized = norm(city); for (final entry in _colombiaCityCoords.entries) { if (normalized == entry.key || normalized.startsWith('${entry.key} ') || entry.key.startsWith('$normalized ')) { return entry.value; } } return null; } Future _centerOnUserCity() async { final city = user?.city; if (city == null || city.isEmpty) return; // Apply hardcoded fallback immediately so the map never loads on the wrong city final fallback = _cityFallbackCoords(city); if (fallback != null && mounted) { setState(() { _mapCenter = fallback; _mapPositioned = true; }); _mapController?.animateCamera(CameraUpdate.newLatLngZoom(fallback, 14)); } // Then try geocoding for a more precise position await _geocodeAndMoveMap(city); } Future _reverseGeocode(LatLng pos) async { final now = DateTime.now(); if (now.difference(_lastGeocode).inMilliseconds < 800) return; _lastGeocode = now; if (mounted) setState(() => _geocoding = true); try { // Use Maps JS SDK Geocoder — works with the same API key as the map, // no separate Geocoding API billing required final result = await MapsService.reverseGeocode(pos.latitude, pos.longitude); if (result != null && mounted) { final city = _extractCity(result.addressComponents); final citiesProvider = Provider.of(context, listen: false); final available = city.isEmpty || citiesProvider.isCityAvailable(city); final userCity = user?.city ?? ''; final mismatch = userCity.isNotEmpty && city.isNotEmpty && !_citiesMatch(city, userCity); setState(() { _currentAddress = result.formattedAddress; _searchController.text = result.formattedAddress; _detectedCity = city; _cityAvailable = available; _cityMismatch = mismatch; }); if (!mismatch && city.isNotEmpty) { context.read().setLocationContext( city: city, lat: pos.latitude, lng: pos.longitude, ); } } } catch (_) {} if (mounted) setState(() => _geocoding = false); } String _extractCity(List components) { for (final c in components) { final types = List.from(c['types'] ?? []); if (types.contains('locality') || types.contains('administrative_area_level_2')) { return c['long_name'] as String? ?? ''; } } return ''; } bool _citiesMatch(String a, String b) { String normalize(String s) => s.toLowerCase().trim() .replaceAll(RegExp(r'[áà]'), 'a') .replaceAll(RegExp(r'[éè]'), 'e') .replaceAll(RegExp(r'[íì]'), 'i') .replaceAll(RegExp(r'[óò]'), 'o') .replaceAll(RegExp(r'[úù]'), 'u'); final na = normalize(a); final nb = normalize(b); // Exact match, or one is a full-word prefix of the other (handles "Bogotá D.C." vs "Bogotá") return na == nb || na.startsWith('$nb ') || nb.startsWith('$na '); } Future _geocodeAndMoveMap(String address) async { if (_mapsApiKey == null || _mapsApiKey!.isEmpty) return; try { final res = await http.get(Uri.parse( 'https://maps.googleapis.com/maps/api/geocode/json' '?address=${Uri.encodeComponent(address)}' '&key=$_mapsApiKey&language=es', )); if (res.statusCode == 200) { final data = jsonDecode(res.body); final results = data['results'] as List?; if (results != null && results.isNotEmpty) { final loc = results[0]['geometry']['location']; final pos = LatLng( (loc['lat'] as num).toDouble(), (loc['lng'] as num).toDouble(), ); if (mounted) setState(() { _mapCenter = pos; _mapPositioned = true; }); _mapController?.animateCamera(CameraUpdate.newLatLngZoom(pos, 16)); } } } catch (_) {} } void _onSearchChanged(String value) { _debounce?.cancel(); if (value.length < 3) { setState(() => _suggestions = []); return; } _debounce = Timer(const Duration(milliseconds: 450), () async { // Bias toward user's city: if map not yet positioned, append city to query // and skip the coordinates bias (which would be a wrong default location) final userCity = user?.city; final cityMissing = userCity != null && userCity.isNotEmpty && !value.toLowerCase().contains(userCity.toLowerCase()); final input = cityMissing ? '$value $userCity' : value; final params = {'input': input}; if (_mapPositioned) params['location'] = '${_mapCenter.latitude},${_mapCenter.longitude}'; final uri = Uri.https('admin.prosapp.co', '/autocomplete', params); final response = await NetworkUtility.fetchUrl(uri); if (response != null && mounted) { final decoded = jsonDecode(response); final results = decoded['results'] ?? decoded['predictions'] ?? []; setState(() => _suggestions = results is List ? results : []); } }); } void _selectSuggestion(String address) { setState(() { _currentAddress = address; _searchController.text = address; _suggestions = []; }); _searchFocus.unfocus(); _geocodeAndMoveMap(address); } Future _selectProfessionalFromMarker(UsuarioProfesional prof) async { try { final result = await NavigationService.navigateToFuture( '/dashboard/calendar/${prof.user.id}', ); if (result is List && result.length >= 2) { setState(() { _professional = prof; _selectedDay = result[0] as DateTime; _selectedHour = result[1] as TimeOfDay; _bookAsDelivery = prof.professionalInfo.locationPreferences == LocationPreferences.delivery; }); } } catch (_) {} } Future _selectProfessional() async { try { // Pass current location context to professionals list before navigating context.read().setLocationContext( city: _detectedCity.isNotEmpty ? _detectedCity : user?.city, lat: _mapCenter.latitude, lng: _mapCenter.longitude, ); final result = await NavigationService.navigateToFuture(Flurorouter.professionalsRoute); final prof = result[0] as UsuarioProfesional; setState(() { _professional = prof; _selectedDay = result[1]; _selectedHour = result[2]; _bookAsDelivery = prof.professionalInfo.locationPreferences == LocationPreferences.delivery; }); } catch (_) {} } Future _requestService() async { if (_professional == null) { NotificationsService.showSnackBarError('Selecciona un profesional'); return; } if (_selectedDay == null || _selectedHour == null) { NotificationsService.showSnackBarError('Selecciona fecha y hora'); return; } final profPrefs = _professional!.professionalInfo.locationPreferences; // Determine if this booking is for office or delivery final bool isOfficeBooking = profPrefs == LocationPreferences.office || (profPrefs == LocationPreferences.both && !_bookAsDelivery); // Dirección solo requerida cuando el profesional hace domicilios if (!isOfficeBooking && _currentAddress.isEmpty) { NotificationsService.showSnackBarError('Mueve el mapa para seleccionar tu dirección'); return; } setState(() => _requesting = true); try { final slot = _selectedHour!; final endSlot = slot.add(minute: _professional!.professionalInfo.slotDurationMinutes); String pad(int n) => n.toString().padLeft(2, '0'); // Build payload matching CreateServiceDto exactly final payload = { 'professional_id': _professional!.user.id, // @IsDateString() expects ISO 8601 date — "yyyy-MM-dd" 'day': '${_selectedDay!.year}-${pad(_selectedDay!.month)}-${pad(_selectedDay!.day)}', 'range1_hour1': '${pad(slot.hour)}:${pad(slot.minute)}', 'range1_hour2': '${pad(endSlot.hour)}:${pad(endSlot.minute)}', 'address': isOfficeBooking ? _professional!.professionalInfo.address : _currentAddress, 'latitude': isOfficeBooking ? _professional!.professionalInfo.latitude : _mapCenter.latitude, 'longitude': isOfficeBooking ? _professional!.professionalInfo.longitude : _mapCenter.longitude, // @IsEnum(['office','delivery']) expects the string value, not an integer 'location_preference': isOfficeBooking ? 'office' : 'delivery', }; await ApiService.instance.post('/services', payload); NotificationsService.showSnackbar('Servicio solicitado exitosamente'); setState(() { _professional = null; _selectedDay = null; _selectedHour = null; }); } catch (_) { NotificationsService.showSnackBarError('Error al solicitar el servicio'); } finally { setState(() => _requesting = false); } } @override void dispose() { _debounce?.cancel(); _searchController.dispose(); _searchFocus.dispose(); _mapController?.dispose(); super.dispose(); } @override Widget build(BuildContext context) { if (user == null || _mapsLoading) { return const Center(child: CircularProgressIndicator()); } final isDark = context.watch().isDark; final cardBg = isDark ? const Color(0xFF1E293B) : Colors.white; final cardBorder = isDark ? const Color(0xFF334155) : const Color(0xFFE0E0E0); final textColor = isDark ? Colors.white : Colors.black87; final subtextColor = isDark ? Colors.white54 : Colors.black54; final hintColor = isDark ? Colors.white38 : Colors.grey; final inputFill = isDark ? const Color(0xFF0F172A) : Colors.white; // Marcadores de consultorios: profesionales con coordenadas reales y atención presencial final professionalsProvider = context.watch(); final officeMarkers = {}; for (final prof in professionalsProvider.professionals) { final lat = prof.professionalInfo.latitude; final lng = prof.professionalInfo.longitude; if (lat == 0.0 && lng == 0.0) continue; final prefs = prof.professionalInfo.locationPreferences; if (prefs == LocationPreferences.delivery) continue; officeMarkers.add(Marker( markerId: MarkerId(prof.professionalInfo.id), position: LatLng(lat, lng), icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueAzure), infoWindow: InfoWindow( title: prof.user.name, snippet: [ if (prof.professionalInfo.profession.isNotEmpty) prof.professionalInfo.profession, if (prof.professionalInfo.address.isNotEmpty) prof.professionalInfo.address, ].join(' • '), ), onTap: () => _selectProfessionalFromMarker(prof), )); } return MediaQuery( // Prevent keyboard from shifting the map+card layout on tablets data: MediaQuery.of(context).copyWith(viewInsets: EdgeInsets.zero), child: GestureDetector( onTap: () { _searchFocus.unfocus(); setState(() => _suggestions = []); }, child: Stack( children: [ // ── MAPA ── if (_mapsReady) GoogleMap( initialCameraPosition: CameraPosition(target: _mapCenter, zoom: 14), onMapCreated: (c) => _mapController = c, onCameraMove: (pos) => _mapCenter = pos.target, onCameraIdle: () => _reverseGeocode(_mapCenter), myLocationEnabled: true, myLocationButtonEnabled: false, zoomControlsEnabled: false, markers: officeMarkers, ) else Container( color: const Color(0xFFE8EDF0), child: Center( child: Padding( padding: const EdgeInsets.all(32), child: Column( mainAxisSize: MainAxisSize.min, children: const [ Icon(Icons.map_outlined, size: 56, color: Colors.grey), SizedBox(height: 12), Text( 'Google Maps no configurado.\nConfigura la API Key en el panel de administración.', textAlign: TextAlign.center, style: TextStyle(color: Colors.grey, fontSize: 14), ), ], ), ), ), ), // ── PIN central fijo ── if (_mapsReady) Positioned.fill( child: IgnorePointer( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: const [ Icon(Icons.location_on, color: Color(0xFF42A4EF), size: 48), SizedBox(height: 20), ], ), ), ), // ── BARRA DE BÚSQUEDA FLOTANTE ── Positioned( top: 16, left: 16, right: 16, child: Column( children: [ Container( decoration: BoxDecoration( color: cardBg, borderRadius: BorderRadius.circular(14), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(isDark ? 0.4 : 0.15), blurRadius: 14, offset: const Offset(0, 4), ), ], ), child: TextField( controller: _searchController, focusNode: _searchFocus, onChanged: _onSearchChanged, style: TextStyle(fontSize: 14, color: textColor), decoration: InputDecoration( hintText: 'Busca o mueve el mapa para fijar tu dirección', hintStyle: TextStyle(color: hintColor, fontSize: 13), prefixIcon: _geocoding ? const Padding( padding: EdgeInsets.all(13), child: SizedBox( width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2), ), ) : const Icon(Icons.search, color: Color(0xFF42A4EF)), suffixIcon: _searchController.text.isNotEmpty ? IconButton( icon: const Icon(Icons.close, size: 18, color: Colors.grey), onPressed: () { _searchController.clear(); setState(() { _suggestions = []; _currentAddress = ''; _detectedCity = ''; _cityMismatch = false; }); }, ) : null, border: InputBorder.none, contentPadding: const EdgeInsets.symmetric(vertical: 15, horizontal: 4), ), ), ), // Sugerencias if (_suggestions.isNotEmpty) Container( margin: const EdgeInsets.only(top: 4), decoration: BoxDecoration( color: cardBg, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow(color: Colors.black.withOpacity(isDark ? 0.4 : 0.12), blurRadius: 10), ], ), child: Column( children: _suggestions.take(5).toList().asMap().entries.map((e) { final item = e.value as Map; final addr = (item['formatted_address'] ?? item['description'] ?? item['name'] ?? '').toString(); final isLast = e.key == (_suggestions.length - 1).clamp(0, 4); return Column( children: [ InkWell( onTap: () => _selectSuggestion(addr), borderRadius: BorderRadius.circular(12), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 13), child: Row( children: [ const Icon(Icons.location_on_outlined, size: 18, color: Color(0xFF42A4EF)), const SizedBox(width: 10), Expanded( child: Text( addr, style: TextStyle(fontSize: 13, color: textColor), maxLines: 2, overflow: TextOverflow.ellipsis, ), ), ], ), ), ), if (!isLast) Divider(height: 1, indent: 16, color: isDark ? Colors.white12 : Colors.grey[100]), ], ); }).toList(), ), ), ], ), ), // ── MI UBICACIÓN ── if (_mapsReady) Positioned( right: 16, bottom: 210, child: FloatingActionButton.small( heroTag: 'myLoc', backgroundColor: Colors.white, elevation: 4, onPressed: _detectLocation, child: const Icon(Icons.my_location, color: Color(0xFF42A4EF), size: 20), ), ), // ── TARJETA INFERIOR ── Positioned( left: 0, right: 0, bottom: 0, child: Container( decoration: BoxDecoration( color: cardBg, borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(isDark ? 0.4 : 0.13), blurRadius: 18, offset: const Offset(0, -4), ), ], ), padding: const EdgeInsets.fromLTRB(20, 10, 20, 24), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Center( child: Container( width: 40, height: 4, decoration: BoxDecoration( color: Colors.grey[300], borderRadius: BorderRadius.circular(2), ), ), ), const SizedBox(height: 10), // Tipo de servicio (solo cuando el profesional acepta ambas modalidades) if (_professional?.professionalInfo.locationPreferences == LocationPreferences.both) ...[ Row( children: [ Expanded( child: GestureDetector( onTap: () => setState(() => _bookAsDelivery = false), child: AnimatedContainer( duration: const Duration(milliseconds: 180), padding: const EdgeInsets.symmetric(vertical: 8), decoration: BoxDecoration( color: !_bookAsDelivery ? const Color(0xFF42A4EF) : (isDark ? const Color(0xFF1E293B) : Colors.grey[100]), borderRadius: BorderRadius.circular(8), border: Border.all( color: !_bookAsDelivery ? const Color(0xFF42A4EF) : cardBorder, ), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.store_outlined, size: 14, color: !_bookAsDelivery ? Colors.white : subtextColor), const SizedBox(width: 5), Text('Consultorio', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: !_bookAsDelivery ? Colors.white : subtextColor)), ], ), ), ), ), const SizedBox(width: 8), Expanded( child: GestureDetector( onTap: () => setState(() => _bookAsDelivery = true), child: AnimatedContainer( duration: const Duration(milliseconds: 180), padding: const EdgeInsets.symmetric(vertical: 8), decoration: BoxDecoration( color: _bookAsDelivery ? const Color(0xFF42A4EF) : (isDark ? const Color(0xFF1E293B) : Colors.grey[100]), borderRadius: BorderRadius.circular(8), border: Border.all( color: _bookAsDelivery ? const Color(0xFF42A4EF) : cardBorder, ), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.home_outlined, size: 14, color: _bookAsDelivery ? Colors.white : subtextColor), const SizedBox(width: 5), Text('Domicilio', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: _bookAsDelivery ? Colors.white : subtextColor)), ], ), ), ), ), ], ), const SizedBox(height: 10), ], // Dirección: consultorio del profesional o dirección del cliente Builder(builder: (context) { final prefs = _professional?.professionalInfo.locationPreferences; final isOfficeBooking = prefs == LocationPreferences.office || (prefs == LocationPreferences.both && !_bookAsDelivery); if (isOfficeBooking) { // Mostrar dirección del consultorio del profesional final profAddress = _professional?.professionalInfo.address ?? ''; if (profAddress.isNotEmpty) { return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), margin: const EdgeInsets.only(bottom: 10), decoration: BoxDecoration( color: isDark ? const Color(0xFF42A4EF).withOpacity(0.08) : const Color(0xFFF0F8FF), borderRadius: BorderRadius.circular(8), border: Border.all(color: const Color(0xFF42A4EF).withOpacity(0.25)), ), child: Row( children: [ const Icon(Icons.store_outlined, size: 16, color: Color(0xFF42A4EF)), const SizedBox(width: 8), Expanded( child: Text( 'Consultorio: $profAddress', style: TextStyle(fontSize: 12, color: textColor), maxLines: 2, overflow: TextOverflow.ellipsis, ), ), ], ), ); } return const SizedBox.shrink(); } // Domicilio: mostrar dirección del cliente if (_currentAddress.isNotEmpty) { return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), margin: const EdgeInsets.only(bottom: 10), decoration: BoxDecoration( color: isDark ? const Color(0xFF42A4EF).withOpacity(0.12) : const Color(0xFFF0F8FF), borderRadius: BorderRadius.circular(8), border: Border.all(color: const Color(0xFF42A4EF).withOpacity(0.35)), ), child: Row( children: [ const Icon(Icons.location_on, size: 16, color: Color(0xFF42A4EF)), const SizedBox(width: 8), Expanded( child: Text( _currentAddress, style: TextStyle(fontSize: 12, color: textColor), maxLines: 2, overflow: TextOverflow.ellipsis, ), ), ], ), ); } return const SizedBox.shrink(); }), // Profesional GestureDetector( onTap: _selectProfessional, child: Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), decoration: BoxDecoration( border: Border.all( color: _professional != null ? const Color(0xFF42A4EF) : cardBorder, ), borderRadius: BorderRadius.circular(10), color: _professional != null ? const Color(0xFF42A4EF).withOpacity(0.1) : (isDark ? const Color(0xFF0F172A) : Colors.grey[50]), ), child: Row( children: [ Icon( Icons.person_outline, size: 20, color: _professional != null ? const Color(0xFF42A4EF) : subtextColor, ), const SizedBox(width: 10), Expanded( child: Text( _professional?.user.name ?? 'Seleccionar profesional', style: TextStyle( fontSize: 13, color: _professional != null ? textColor : subtextColor, ), ), ), Icon(Icons.chevron_right, color: subtextColor, size: 18), ], ), ), ), // Fecha y hora if (_selectedDay != null && _selectedHour != null) ...[ const SizedBox(height: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), decoration: BoxDecoration( border: Border.all(color: cardBorder), borderRadius: BorderRadius.circular(10), color: isDark ? const Color(0xFF0F172A) : Colors.grey[50], ), child: Row( children: [ const Icon(Icons.calendar_today_outlined, size: 17, color: Color(0xFF42A4EF)), const SizedBox(width: 8), Text( DateFormat('dd/MM/yyyy').format(_selectedDay!), style: TextStyle(fontSize: 13, color: textColor), ), const SizedBox(width: 14), const Icon(Icons.access_time, size: 17, color: Color(0xFF42A4EF)), const SizedBox(width: 6), Text( ScheduleEntity.getFormatTime(_selectedHour!) ?? '', style: TextStyle(fontSize: 13, color: textColor), ), const Spacer(), GestureDetector( onTap: () => setState(() { _selectedDay = null; _selectedHour = null; _professional = null; }), child: Icon(Icons.close, size: 18, color: subtextColor), ), ], ), ), ], const SizedBox(height: 12), // Aviso ciudad no disponible (solo aplica cuando el servicio es a domicilio) if (!_cityAvailable && _detectedCity.isNotEmpty && _bookAsDelivery) Container( width: double.infinity, margin: const EdgeInsets.only(bottom: 10), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), decoration: BoxDecoration( color: const Color(0xFFFFF3CD), borderRadius: BorderRadius.circular(10), border: Border.all(color: const Color(0xFFFFC107)), ), child: Row( children: [ const Icon(Icons.location_off_outlined, color: Color(0xFF856404), size: 20), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Servicio no disponible en tu ciudad', style: TextStyle( color: Color(0xFF856404), fontWeight: FontWeight.w600, fontSize: 13, ), ), Text( 'Actualmente no operamos en $_detectedCity. Mueve el mapa a una ciudad disponible.', style: const TextStyle(color: Color(0xFF856404), fontSize: 11), ), ], ), ), ], ), ), // Warning: dirección fuera de la ciudad del usuario (solo en modo domicilio) if (_cityMismatch && _detectedCity.isNotEmpty && _bookAsDelivery) Container( width: double.infinity, margin: const EdgeInsets.only(bottom: 10), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), decoration: BoxDecoration( color: const Color(0xFFFFEBEE), borderRadius: BorderRadius.circular(10), border: Border.all(color: const Color(0xFFEF9A9A)), ), child: Row( children: [ const Icon(Icons.location_off_outlined, color: Color(0xFFC62828), size: 20), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Dirección fuera de tu ciudad', style: TextStyle( color: Color(0xFFC62828), fontWeight: FontWeight.w600, fontSize: 13, ), ), Text( 'Solo puedes solicitar servicios en ${user?.city ?? 'tu ciudad'}. Mueve el mapa a una dirección de ${user?.city ?? 'tu ciudad'}.', style: const TextStyle(color: Color(0xFFC62828), fontSize: 11), ), ], ), ), ], ), ), SizedBox( width: double.infinity, child: ElevatedButton( onPressed: _requesting ? null : ((!_bookAsDelivery || (_cityAvailable && !_cityMismatch)) ? _requestService : null), style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF42A4EF), foregroundColor: Colors.white, disabledBackgroundColor: Colors.grey[200], padding: const EdgeInsets.symmetric(vertical: 15), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), elevation: 0, ), child: _requesting ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2), ) : const Text( 'Solicitar servicio', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600), ), ), ), ], ), ), ), ], ), ), ); } }