From e56807b75eee2bc8a99595eb817b8d0aa35ad9eb Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Sat, 27 Jun 2026 08:18:02 -0500 Subject: [PATCH] =?UTF-8?q?feat:=20redise=C3=B1o=20dashboard=20estilo=20In?= =?UTF-8?q?Drive=20con=20mapa=20completo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mapa ocupa toda la pantalla, pin central fijo, barra de busqueda flotante con autocomplete, tarjeta inferior para profesional/fecha. Al mover el mapa hace geocodificacion inversa y actualiza la direccion. Co-Authored-By: Claude Sonnet 4.6 --- lib/ui/views/dashboard_view.dart | 870 +++++++++++++++++++------------ 1 file changed, 543 insertions(+), 327 deletions(-) diff --git a/lib/ui/views/dashboard_view.dart b/lib/ui/views/dashboard_view.dart index 1103919..a94d8e3 100644 --- a/lib/ui/views/dashboard_view.dart +++ b/lib/ui/views/dashboard_view.dart @@ -1,8 +1,10 @@ import 'dart:async'; import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:geolocator/geolocator.dart'; 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/services/api_service.dart'; import 'package:prosapp_web_app/models/schedules_entity.dart'; import 'package:prosapp_web_app/models/service.dart'; import 'package:prosapp_web_app/models/service_location_preferences.dart'; @@ -10,14 +12,11 @@ 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/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/ui/cards/white_card.dart'; -import 'package:prosapp_web_app/ui/inputs/custom_inputs.dart'; -import 'package:prosapp_web_app/ui/widgets/location_picker.dart'; -import 'package:flutter/material.dart'; import 'package:prosapp_web_app/utils/local_notifications.dart'; import 'package:prosapp_web_app/utils/network_utility.dart'; import 'package:prosapp_web_app/utils/time_of_day_extension.dart'; @@ -32,362 +31,579 @@ class DashboardView extends StatefulWidget { class _DashboardViewState extends State { Usuario? user; - List _placesList = []; - final TextEditingController _addressController = TextEditingController(); + + // Map + GoogleMapController? _mapController; + LatLng _mapCenter = const LatLng(4.6097, -74.0817); + 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; - UsuarioProfesional? selectedProfessional; - String? selectedProfessionalName; - DateTime? selectedDay; - TimeOfDay? selectedHour; - LatLng? _selectedLatLng; - String? _selectedCity; + + // Booking + UsuarioProfesional? _professional; + DateTime? _selectedDay; + TimeOfDay? _selectedHour; + bool _requesting = false; + + // Throttle reverse geocode on camera idle + DateTime _lastGeocode = DateTime(0); @override void initState() { super.initState(); - - final authProvider = Provider.of(context, listen: false); - - setState(() { - user = authProvider.user; - }); + user = Provider.of(context, listen: false).user; + _initMaps(); } - void placeAutoComplete(String query, String _coords) async { - Uri uri = Uri.https("admin.prosapp.co", "/autocomplete", { - "input": query, - "location": _coords, - }); - - String? response = await NetworkUtility.fetchUrl(uri); - - if (response != null) { - if (mounted) { - setState(() { - _placesList = jsonDecode(response.toString())['results']; - }); - } - } - } - - Future _selectProfessional(BuildContext context) async { + Future _initMaps() async { + final ok = await MapsService.load(); + if (!mounted) return; try { - List result = await NavigationService.navigateToFuture( - Flurorouter.professionalsRoute); + final data = await ApiService.instance.get('/settings/maps-key'); + _mapsApiKey = data['api_key'] as String? ?? ''; + } catch (_) {} + setState(() { + _mapsReady = ok; + _mapsLoading = false; + }); + if (ok) _detectLocation(); + } - final UsuarioProfesional selectedProfessional = result[0]; + Future _detectLocation() async { + try { + bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); + if (!serviceEnabled) return; - print('selectedProfessional: $selectedProfessional'); + LocationPermission permission = await Geolocator.checkPermission(); + if (permission == LocationPermission.denied) { + permission = await Geolocator.requestPermission(); + if (permission == LocationPermission.denied) return; + } + if (permission == LocationPermission.deniedForever) return; - setState(() { - selectedDay = result[1]; - selectedHour = result[2]; - selectedProfessionalName = selectedProfessional.user.name; - this.selectedProfessional = selectedProfessional; - _addressController.text = selectedProfessional.professionalInfo.address; + 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); + _mapController?.animateCamera(CameraUpdate.newLatLngZoom(latlng, 16)); + _reverseGeocode(latlng); + } catch (_) {} + } + + Future _reverseGeocode(LatLng pos) async { + if (_mapsApiKey == null || _mapsApiKey!.isEmpty) return; + final now = DateTime.now(); + if (now.difference(_lastGeocode).inMilliseconds < 800) return; + _lastGeocode = now; + + setState(() => _geocoding = true); + try { + final res = await http.get(Uri.parse( + 'https://maps.googleapis.com/maps/api/geocode/json' + '?latlng=${pos.latitude},${pos.longitude}' + '&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 address = results[0]['formatted_address'] as String; + if (mounted) { + setState(() { + _currentAddress = address; + _searchController.text = address; + }); + } + } + } + } catch (_) {} + if (mounted) setState(() => _geocoding = false); + } + + 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); + _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 { + final uri = Uri.https('admin.prosapp.co', '/autocomplete', { + 'input': value.replaceAll(' ', '_'), }); - } catch (e) { - print('debugeando $e'); + final response = await NetworkUtility.fetchUrl(uri); + if (response != null && mounted) { + setState(() => _suggestions = jsonDecode(response)['results'] ?? []); + } + }); + } + + void _selectSuggestion(String address) { + setState(() { + _currentAddress = address; + _searchController.text = address; + _suggestions = []; + }); + _searchFocus.unfocus(); + _geocodeAndMoveMap(address); + } + + Future _selectProfessional() async { + try { + final result = await NavigationService.navigateToFuture(Flurorouter.professionalsRoute); + final prof = result[0] as UsuarioProfesional; + setState(() { + _professional = prof; + _selectedDay = result[1]; + _selectedHour = result[2]; + }); + } catch (_) {} + } + + Future _requestService() async { + if (_currentAddress.isEmpty) { + NotificationsService.showSnackBarError('Mueve el mapa para seleccionar tu dirección'); + return; + } + if (_professional == null) { + NotificationsService.showSnackBarError('Selecciona un profesional'); + return; + } + if (_selectedDay == null || _selectedHour == null) { + NotificationsService.showSnackBarError('Selecciona fecha y hora'); + return; + } + setState(() => _requesting = true); + try { + final service = Service( + id: null, + professionalId: _professional!.user.id, + professionalScored: false, + userId: user!.id, + userScored: false, + address: _currentAddress, + aditionalAddress: '', + latitude: _mapCenter.latitude, + longitude: _mapCenter.longitude, + day: _selectedDay.toString(), + createdAt: DateTime.now().toIso8601String(), + description: '', + range1Hour1: _selectedHour!, + range1Hour2: _selectedHour!.add(hour: 2), + rate: '', + status: ServiceStatus.pending, + location: ServiceLocationPreferences.delivery, + ); + await ApiService.instance.post('/services', service.toDocument()); + NotificationsService.showSnackbar('Servicio solicitado exitosamente'); + if (_professional!.user.token != null) { + LocalNotifications.sendPushNotification( + _professional!.user.token!, + 'Nuevo servicio', + 'Tienes una nueva solicitud de servicio pendiente', + ); + } + setState(() { + _professional = null; + _selectedDay = null; + _selectedHour = null; + }); + } catch (_) { + NotificationsService.showSnackBarError('Error al solicitar el servicio'); + } finally { + setState(() => _requesting = false); } } @override void dispose() { _debounce?.cancel(); - _addressController.dispose(); + _searchController.dispose(); + _searchFocus.dispose(); + _mapController?.dispose(); super.dispose(); } - Future _openLocationPicker() async { - final result = await LocationPickerDialog.show( - context, - initialPosition: _selectedLatLng, - ); - if (result != null && mounted) { - setState(() { - _addressController.text = result.address; - _selectedLatLng = result.position; - _selectedCity = result.city; - }); - } - } - @override Widget build(BuildContext context) { - if (user == null) { - return const Center( - child: CircularProgressIndicator(), - ); + if (user == null || _mapsLoading) { + return const Center(child: CircularProgressIndicator()); } - bool isUserComplete() { - return user!.name != '' && - user!.email != '' && - user!.phone != '' && - user!.city != ''; - } - - final citiesProvider = Provider.of(context); - - if (citiesProvider.isLoading) { - return const Center( - child: CircularProgressIndicator(), - ); - } - - final _coords = citiesProvider.getCoordsOfCity(user!.city ?? ''); - - _createService() async { - print('debug ${selectedProfessional?.user.id ?? 'user.id'}'); - print('debug ${user!.id ?? 'user.id'}'); - print('debug ${_addressController.text ?? 'user.id'}'); - - try { - double latitude = 0.0; - double longitude = 0.0; - - print('debug 1'); - - Service service = Service( - id: null, - professionalId: selectedProfessional!.user.id, - professionalScored: false, - userId: user!.id, - userScored: false, - address: _addressController.text, - aditionalAddress: '', - latitude: latitude, - longitude: longitude, - day: selectedDay.toString(), - createdAt: DateTime.now().toIso8601String(), - description: '', - range1Hour1: selectedHour!, - range1Hour2: selectedHour!.add(hour: 2), - rate: '', - status: ServiceStatus.pending, - location: ServiceLocationPreferences.delivery, - ); - - print('debug 1'); - - await ApiService.instance.post('/services', service.toDocument()); - - print('debug 2'); - - NotificationsService.showSnackbar('Servicio solicitado exitosamente'); - - if (selectedProfessional != null) { - if (selectedProfessional!.user.token != null) { - LocalNotifications.sendPushNotification( - selectedProfessional!.user.token!, - 'Nuevo servicio', - 'Tienes una nueva solicitud de servicio pendiente', - ); - } - } - - _addressController.clear(); - setState(() { - selectedProfessional = null; - selectedProfessionalName = null; - selectedDay = null; - selectedHour = null; - }); - } catch (e) { - NotificationsService.showSnackBarError( - '$e Error al solicitar el servicio, intenta de nuevo'); - } - } - - return Center( - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 900), - child: Stack( - children: [ - ListView( - physics: const ClampingScrollPhysics(), - children: [ - WhiteCard( + return 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, + ) + else + Container( + color: const Color(0xFFE8EDF0), + child: Center( + child: Padding( + padding: const EdgeInsets.all(32), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 10), - GestureDetector( - onTap: isUserComplete() ? _openLocationPicker : () => - NotificationsService.showSnackBarError('Completa tu perfil para solicitar un servicio'), - child: AbsorbPointer( - child: TextFormField( - controller: _addressController, - decoration: CustomInputs.formInputDecoration( - hint: 'Toca para seleccionar tu dirección', - label: 'Dirección', - icon: Icons.location_on, - ), - ), - ), + 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), ), - const SizedBox(height: 10), - GestureDetector( - onTap: () => isUserComplete() - ? _selectProfessional(context) - : NotificationsService.showSnackBarError( - 'Completa tu perfil para solicitar un servicio'), - child: AbsorbPointer( - child: TextFormField( - controller: TextEditingController( - text: selectedProfessionalName, - ), - decoration: CustomInputs.formInputDecoration( - hint: 'Selecciona un Profesional', - label: 'Profesional', - icon: Icons.person_rounded, - ), - ), - ), - ), - const SizedBox(height: 10), - if (selectedDay != null && selectedHour != null) ...[ - TextFormField( - readOnly: true, - controller: TextEditingController( - text: selectedDay == null - ? '' - : DateFormat('dd/MM/yyyy').format(selectedDay!), - ), - decoration: CustomInputs.formInputDecoration( - hint: 'Fecha', - label: 'Fecha', - icon: Icons.calendar_month, - ), - ), - const SizedBox(height: 10), - TextFormField( - readOnly: true, - controller: TextEditingController( - text: selectedHour == null - ? '' - : ScheduleEntity.getFormatTime(selectedHour), - ), - decoration: CustomInputs.formInputDecoration( - hint: 'Hora', - label: 'Hora', - icon: Icons.watch_later_outlined, - ), - ), - const SizedBox(height: 10), - ], - const SizedBox(height: 10), - Center( - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 230), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ElevatedButton( - onPressed: () { - if (!isUserComplete()) { - NotificationsService.showSnackBarError( - 'Completa tu perfil para solicitar un servicio'); - - return; - } - - _createService(); - }, - style: ButtonStyle( - backgroundColor: WidgetStateProperty.all( - Colors.blue.shade400), - shape: WidgetStateProperty.all( - const RoundedRectangleBorder( - borderRadius: - BorderRadius.all(Radius.circular(5)), - )), - shadowColor: WidgetStateProperty.all( - Colors.transparent), - ), - child: const Text( - 'Solicitar cita', - style: TextStyle(color: Colors.white), - ), - ), - if (selectedDay != null && - selectedHour != null && - selectedProfessional != null) ...[ - const SizedBox(width: 10), - ElevatedButton( - onPressed: () async { - _addressController.clear(); - selectedProfessional = null; - selectedDay = null; - selectedHour = null; - - setState(() {}); - }, - style: ButtonStyle( - backgroundColor: WidgetStateProperty.all( - Colors.red, - ), - shape: WidgetStateProperty.all( - const RoundedRectangleBorder( - borderRadius: BorderRadius.all( - Radius.circular(5)))), - shadowColor: WidgetStateProperty.all( - Colors.transparent)), - child: const Icon( - Icons.close_rounded, - color: Colors.white, - ), - ), - ], - ], - ), - ), - ), - const SizedBox(height: 8), ], ), ), - ], + ), ), - Positioned( - left: 20, - right: 20, - top: 80, - child: _placesList.isNotEmpty - ? Material( - elevation: 5.0, - borderRadius: BorderRadius.circular(10), - child: Container( - padding: const EdgeInsets.all(8.0), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(10), - ), - child: ListView.builder( - shrinkWrap: true, - itemCount: _placesList.length, - itemBuilder: (context, index) { - return ListTile( - title: - Text(_placesList[index]['formatted_address']), - dense: true, - visualDensity: VisualDensity.compact, - onTap: () { + + // ── 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: Colors.white, + borderRadius: BorderRadius.circular(14), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.15), + blurRadius: 14, + offset: const Offset(0, 4), + ), + ], + ), + child: TextField( + controller: _searchController, + focusNode: _searchFocus, + onChanged: _onSearchChanged, + style: const TextStyle(fontSize: 14), + decoration: InputDecoration( + hintText: 'Busca o mueve el mapa para fijar tu dirección', + hintStyle: const TextStyle(color: Colors.grey, 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(() { - _addressController.text = - _placesList[index]['formatted_address']; - _placesList = []; + _suggestions = []; + _currentAddress = ''; }); }, - ); - }, - ), - ), - ) - : Container(), + ) + : 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: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow(color: Colors.black.withOpacity(0.12), blurRadius: 10), + ], + ), + child: Column( + children: _suggestions.take(5).toList().asMap().entries.map((e) { + final addr = e.value['formatted_address'] as String? ?? ''; + 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: const TextStyle(fontSize: 13), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ), + if (!isLast) Divider(height: 1, indent: 16, color: 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: Colors.white, + borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(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), + + // Dirección actual + if (_currentAddress.isNotEmpty) + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + margin: const EdgeInsets.only(bottom: 10), + decoration: BoxDecoration( + color: 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: const TextStyle(fontSize: 12, color: Colors.black87), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + + // 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) + : const Color(0xFFE0E0E0), + ), + borderRadius: BorderRadius.circular(10), + color: _professional != null ? const Color(0xFFF0F8FF) : Colors.grey[50], + ), + child: Row( + children: [ + Icon( + Icons.person_outline, + size: 20, + color: _professional != null ? const Color(0xFF42A4EF) : Colors.grey, + ), + const SizedBox(width: 10), + Expanded( + child: Text( + _professional?.user.name ?? 'Seleccionar profesional', + style: TextStyle( + fontSize: 13, + color: _professional != null ? Colors.black87 : Colors.grey, + ), + ), + ), + const Icon(Icons.chevron_right, color: Colors.grey, 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: const Color(0xFFE0E0E0)), + borderRadius: BorderRadius.circular(10), + color: 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: const TextStyle(fontSize: 13), + ), + const SizedBox(width: 14), + const Icon(Icons.access_time, size: 17, color: Color(0xFF42A4EF)), + const SizedBox(width: 6), + Text( + ScheduleEntity.getFormatTime(_selectedHour), + style: const TextStyle(fontSize: 13), + ), + const Spacer(), + GestureDetector( + onTap: () => setState(() { + _selectedDay = null; + _selectedHour = null; + _professional = null; + }), + child: const Icon(Icons.close, size: 18, color: Colors.grey), + ), + ], + ), + ), + ], + + const SizedBox(height: 12), + + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: _requesting ? null : _requestService, + 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), + ), + ), + ), + ], + ), + ), + ), + ], ), ); }