import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:geolocator/geolocator.dart'; import 'package:http/http.dart' as http; import 'package:prosapp_web_app/providers/auth_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/navigation_service.dart'; import 'package:provider/provider.dart'; class SetupCityView extends StatefulWidget { const SetupCityView({super.key}); @override State createState() => _SetupCityViewState(); } class _SetupCityViewState extends State { final _searchCtrl = TextEditingController(); List _cities = []; // lista del backend List _filtered = []; // filtradas por búsqueda String? _selected; // ciudad seleccionada String? _detected; // ciudad detectada por GPS bool _loadingCities = true; bool _detectingGps = true; bool _saving = false; @override void initState() { super.initState(); _loadCities(); _detectCity(); _searchCtrl.addListener(_onSearch); } @override void dispose() { _searchCtrl.removeListener(_onSearch); _searchCtrl.dispose(); super.dispose(); } void _onSearch() { final q = _searchCtrl.text.toLowerCase(); setState(() { _filtered = q.isEmpty ? List.from(_cities) : _cities.where((c) => c.toLowerCase().contains(q)).toList(); }); } Future _loadCities() async { try { final data = await ApiService.instance.get('/locations/cities/all'); final list = (data as List).map((c) => (c['name'] as String)).toList(); if (mounted) { setState(() { _cities = list; _filtered = List.from(list); _loadingCities = false; // Si ya detectamos ciudad antes de que cargara la lista, preseleccionar if (_detected != null) _tryPreselect(_detected!); }); } } catch (_) { if (mounted) setState(() => _loadingCities = false); } } Future _detectCity() async { setState(() => _detectingGps = true); try { LocationPermission perm = await Geolocator.checkPermission(); if (perm == LocationPermission.denied) { perm = await Geolocator.requestPermission(); } if (perm == LocationPermission.deniedForever || perm == LocationPermission.denied) { if (mounted) setState(() => _detectingGps = false); return; } final pos = await Geolocator.getCurrentPosition( locationSettings: const LocationSettings( accuracy: LocationAccuracy.low, timeLimit: Duration(seconds: 8), ), ); final url = Uri.parse( 'https://nominatim.openstreetmap.org/reverse' '?format=json&lat=${pos.latitude}&lon=${pos.longitude}&zoom=10&addressdetails=1', ); final resp = await http.get(url, headers: {'User-Agent': 'ProsApp/1.0 (prosapp.co)'}); if (resp.statusCode == 200) { final json = jsonDecode(resp.body) as Map; final address = json['address'] as Map?; final city = address?['city'] as String? ?? address?['town'] as String? ?? address?['municipality'] as String? ?? address?['county'] as String?; if (city != null && city.isNotEmpty && mounted) { setState(() => _detected = city); if (!_loadingCities) _tryPreselect(city); } } } catch (_) { // GPS no disponible } finally { if (mounted) setState(() => _detectingGps = false); } } // Busca la mejor coincidencia de la ciudad detectada en la lista del backend void _tryPreselect(String detectedCity) { final normalized = detectedCity.toLowerCase().trim(); // Coincidencia exacta primero String? match = _cities.firstWhere( (c) => c.toLowerCase() == normalized, orElse: () => '', ); if (match!.isEmpty) { // Coincidencia parcial match = _cities.firstWhere( (c) => c.toLowerCase().contains(normalized) || normalized.contains(c.toLowerCase()), orElse: () => '', ); } if (match.isNotEmpty && mounted) { setState(() { _selected = match; _searchCtrl.text = match!; _filtered = [match!, ..._cities.where((c) => c != match).toList()]; }); } } Future _save() async { final city = _selected ?? _searchCtrl.text.trim(); if (city.isEmpty) return; setState(() => _saving = true); try { await context.read().updateCity(city); if (mounted) NavigationService.replaceTo(Flurorouter.dashboardRoute); } catch (_) { if (mounted) setState(() => _saving = false); } } void _skip() => NavigationService.replaceTo(Flurorouter.dashboardRoute); @override Widget build(BuildContext context) { final isDark = context.watch().isDark; final cardBg = isDark ? const Color(0xFF1E293B) : Colors.white; final border = isDark ? const Color(0xFF334155) : const Color(0xFFE5E7EB); final textPrimary = isDark ? Colors.white : const Color(0xFF111827); final textSec = isDark ? const Color(0xFF94A3B8) : const Color(0xFF6B7280); final inputFill = isDark ? const Color(0xFF0F172A) : const Color(0xFFF9FAFB); return Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 460), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 32), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // ── Header ───────────────────────────────────────────────────── Column( children: [ Container( width: 64, height: 64, decoration: BoxDecoration( color: const Color(0xFF42A4EF).withOpacity(0.12), shape: BoxShape.circle, ), child: const Icon(Icons.location_city_rounded, color: Color(0xFF42A4EF), size: 32), ), const SizedBox(height: 16), Text('¿En qué ciudad estás?', style: TextStyle( fontSize: 22, fontWeight: FontWeight.bold, color: textPrimary), textAlign: TextAlign.center), const SizedBox(height: 6), Text( 'Así te mostramos profesionales cerca de ti.', style: TextStyle(fontSize: 13, color: textSec), textAlign: TextAlign.center), ], ), const SizedBox(height: 20), // ── Ciudad detectada por GPS ──────────────────────────────────── if (_detectingGps) Container( padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 14), decoration: BoxDecoration( color: const Color(0xFF42A4EF).withOpacity(0.07), borderRadius: BorderRadius.circular(10), border: Border.all(color: const Color(0xFF42A4EF).withOpacity(0.2)), ), child: Row( children: [ const SizedBox( width: 14, height: 14, child: CircularProgressIndicator( strokeWidth: 2, color: Color(0xFF42A4EF)), ), const SizedBox(width: 10), Text('Detectando tu ubicación...', style: TextStyle(fontSize: 12, color: textSec)), ], ), ) else if (_detected != null) Container( padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 14), decoration: BoxDecoration( color: const Color(0xFF10B981).withOpacity(0.08), borderRadius: BorderRadius.circular(10), border: Border.all( color: const Color(0xFF10B981).withOpacity(0.3)), ), child: Row( children: [ const Icon(Icons.my_location, size: 16, color: Color(0xFF10B981)), const SizedBox(width: 8), Expanded( child: Text( 'Detectado: $_detected', style: const TextStyle( fontSize: 12, color: Color(0xFF10B981), fontWeight: FontWeight.w500), ), ), TextButton( onPressed: _detectCity, style: TextButton.styleFrom( padding: EdgeInsets.zero, minimumSize: Size.zero, tapTargetSize: MaterialTapTargetSize.shrinkWrap), child: const Text('Reintentar', style: TextStyle( fontSize: 11, color: Color(0xFF42A4EF))), ), ], ), ) else Align( alignment: Alignment.centerRight, child: TextButton.icon( icon: const Icon(Icons.my_location, size: 14), label: const Text('Detectar GPS', style: TextStyle(fontSize: 12)), onPressed: _detectCity, style: TextButton.styleFrom( foregroundColor: const Color(0xFF42A4EF), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4)), ), ), const SizedBox(height: 12), // ── Buscador ──────────────────────────────────────────────────── TextField( controller: _searchCtrl, style: TextStyle(color: textPrimary, fontSize: 14), decoration: InputDecoration( hintText: 'Buscar ciudad...', hintStyle: TextStyle(color: textSec, fontSize: 13), prefixIcon: const Icon(Icons.search, color: Color(0xFF42A4EF), size: 20), suffixIcon: _searchCtrl.text.isNotEmpty ? IconButton( icon: Icon(Icons.close, size: 16, color: textSec), onPressed: () { _searchCtrl.clear(); setState(() { _selected = null; _filtered = List.from(_cities); }); }, ) : null, filled: true, fillColor: inputFill, border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: border)), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: border)), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: const BorderSide( color: Color(0xFF42A4EF), width: 2)), contentPadding: const EdgeInsets.symmetric(vertical: 13, horizontal: 4), ), onChanged: (_) => setState(() => _selected = null), ), const SizedBox(height: 8), // ── Lista de ciudades ─────────────────────────────────────────── Expanded( child: _loadingCities ? const Center(child: CircularProgressIndicator()) : _filtered.isEmpty ? Center( child: Text('Sin resultados', style: TextStyle(color: textSec, fontSize: 13)), ) : Container( decoration: BoxDecoration( color: cardBg, borderRadius: BorderRadius.circular(12), border: Border.all(color: border), ), child: ClipRRect( borderRadius: BorderRadius.circular(12), child: ListView.separated( itemCount: _filtered.length, separatorBuilder: (_, __) => Divider(height: 1, color: border), itemBuilder: (_, i) { final city = _filtered[i]; final isSelected = city == _selected; return InkWell( onTap: () => setState(() { _selected = city; _searchCtrl.text = city; }), child: Container( color: isSelected ? const Color(0xFF42A4EF) .withOpacity(0.10) : Colors.transparent, padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 13), child: Row( children: [ Icon( Icons.location_on_outlined, size: 16, color: isSelected ? const Color(0xFF42A4EF) : textSec, ), const SizedBox(width: 10), Expanded( child: Text( city, style: TextStyle( fontSize: 14, color: isSelected ? const Color(0xFF42A4EF) : textPrimary, fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, ), ), ), if (isSelected) const Icon(Icons.check_circle, size: 18, color: Color(0xFF42A4EF)), if (city == _detected && !isSelected) Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 2), decoration: BoxDecoration( color: const Color(0xFF10B981) .withOpacity(0.1), borderRadius: BorderRadius.circular(4), ), child: const Text('GPS', style: TextStyle( fontSize: 9, color: Color(0xFF10B981), fontWeight: FontWeight.w600)), ), ], ), ), ); }, ), ), ), ), const SizedBox(height: 14), // ── Botón guardar ─────────────────────────────────────────────── SizedBox( height: 48, child: ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF42A4EF), foregroundColor: Colors.white, disabledBackgroundColor: const Color(0xFF42A4EF).withOpacity(0.4), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12)), elevation: 0, ), onPressed: (_selected != null || _searchCtrl.text.trim().isNotEmpty) && !_saving ? _save : null, child: _saving ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white)) : Text( _selected != null ? 'Guardar: $_selected' : 'Guardar ciudad', style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600)), ), ), const SizedBox(height: 8), // Omitir TextButton( onPressed: _skip, child: Text('Omitir por ahora', style: TextStyle(color: textSec, fontSize: 13)), ), ], ), ), ), ); } }