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/models/schedules_entity.dart'; import 'package:prosapp_web_app/models/service.dart'; import 'package:prosapp_web_app/models/service_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/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/local_notifications.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(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; // 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(); 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 (_) {} setState(() { _mapsReady = ok; _mapsLoading = false; }); if (ok) _detectLocation(); } Future _detectLocation() async { try { bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (!serviceEnabled) return; LocationPermission permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { permission = await Geolocator.requestPermission(); if (permission == LocationPermission.denied) return; } if (permission == LocationPermission.deniedForever) 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); _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(' ', '_'), }); 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(); _searchController.dispose(); _searchFocus.dispose(); _mapController?.dispose(); super.dispose(); } @override Widget build(BuildContext context) { if (user == null || _mapsLoading) { return const Center(child: CircularProgressIndicator()); } 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( 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: 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(() { _suggestions = []; _currentAddress = ''; }); }, ) : 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), ), ), ), ], ), ), ), ], ), ); } }