import 'dart:io'; import 'package:city_repository/city_repository.dart'; import 'package:flutter/material.dart'; import 'package:geolocator/geolocator.dart'; import 'package:injector/injector.dart'; import 'package:intl/intl.dart'; import 'package:intl_phone_field/helpers.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:image_picker/image_picker.dart'; import 'package:prosappco/blocs/auth_bloc/auth_bloc.dart'; import 'package:prosappco/blocs/my_user_bloc/my_user_bloc.dart'; import 'package:prosappco/blocs/profile_bloc/profile_bloc.dart'; import 'package:prosappco/components/birthday_picker.dart'; import 'package:prosappco/components/gender_dropdown.dart'; import 'package:prosappco/screens/lists/city_list_screen.dart'; import 'package:prosappco/screens/profile/components/profile_item.dart'; import 'package:prosappco/screens/profile/profile_register_email_screen.dart'; import 'package:prosappco/screens/profile/profile_register_phone_screen.dart'; import 'package:prosappco/screens/profile/profile_update_password_screen.dart'; import 'package:prosappco/utils/nominatim_geocoder.dart'; const _kPrimary = Color(0xFF1565C0); extension _Th on BuildContext { ThemeData get _t => Theme.of(this); Color get onSurface => _t.colorScheme.onSurface; Color get muted => _t.colorScheme.onSurface.withOpacity(0.55); Color get card => _t.cardColor; bool get isDark => _t.brightness == Brightness.dark; Color get shadowSm => isDark ? Colors.transparent : Colors.black.withOpacity(0.05); } class ProfileScreen extends StatefulWidget { const ProfileScreen({super.key}); @override State createState() => _ProfileScreenState(); } class _ProfileScreenState extends State { final TextEditingController _nameController = TextEditingController(); final TextEditingController _cityController = TextEditingController(); final TextEditingController _emailController = TextEditingController(); final TextEditingController _phoneController = TextEditingController(); final TextEditingController _birthdayController = TextEditingController(); final TextEditingController _genderController = TextEditingController(); XFile? _imageFile; bool isLoading = false; bool _isLocating = false; late final AuthBloc authBloc; @override void initState() { super.initState(); authBloc = Injector.appInstance.get(); } @override void dispose() { _nameController.dispose(); _cityController.dispose(); _emailController.dispose(); _phoneController.dispose(); _birthdayController.dispose(); _genderController.dispose(); super.dispose(); } Future _useMyLocation() async { setState(() => _isLocating = true); try { bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (!serviceEnabled) throw Exception('Location services disabled'); LocationPermission permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { permission = await Geolocator.requestPermission(); } if (permission == LocationPermission.denied || permission == LocationPermission.deniedForever) { throw Exception('Location permission denied'); } final position = await Geolocator.getCurrentPosition(); final cityName = await NominatimGeocoder.reverseGeocodeCity( position.latitude, position.longitude); if (cityName == null) throw Exception('City not resolved'); final cities = await Injector.appInstance.get().getCities(); final normalized = removeDiacritics(cityName.toLowerCase().trim()); CityUi? match; for (final c in cities) { if (removeDiacritics(c.cityName.toLowerCase().trim()) == normalized) { match = c; break; } } if (match == null) { for (final c in cities) { final cName = removeDiacritics(c.cityName.toLowerCase()); if (cName.contains(normalized) || normalized.contains(cName)) { match = c; break; } } } if (!mounted) return; if (match != null) { setState(() => _cityController.text = match!.cityName); } else { ScaffoldMessenger.of(context).showSnackBar(const SnackBar( content: Text( 'No encontramos tu ciudad en la lista, selecciónala manualmente'), )); } } catch (_) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar(const SnackBar( content: Text('No se pudo obtener tu ubicación, selecciona tu ciudad manualmente'), )); } finally { if (mounted) setState(() => _isLocating = false); } } @override Widget build(BuildContext context) { return BlocProvider( create: (_) => authBloc, child: BlocListener( listener: (context, state) { if (state is UpdateUserInfoLoading) { setState(() => isLoading = true); } else if (state is UpdateUserInfoSuccess) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Información actualizada'))); setState(() => isLoading = false); } else if (state is UpdateUserInfoFailure) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Error al actualizar'))); setState(() => isLoading = false); } }, child: Scaffold( appBar: AppBar( title: const Text('Mi perfil'), backgroundColor: _kPrimary, foregroundColor: Colors.white, elevation: 0, ), body: BlocBuilder( builder: (context, state) { if (state.status == MyUserStatus.success) { _nameController.text = state.user!.name ?? ''; _cityController.text = state.user!.city ?? ''; _emailController.text = state.user!.email ?? ''; _phoneController.text = state.user!.phone ?? ''; _birthdayController.text = state.user!.birthday ?? ''; _genderController.text = state.user!.gender ?? ''; return Column( children: [ Expanded( child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _avatarSection(state, context), const SizedBox(height: 8), _formSection(state, context), ], ), ), ), const Divider(height: 1, thickness: 0.5), Padding( padding: const EdgeInsets.symmetric( vertical: 12, horizontal: 16), child: _saveButton(state, context), ), ], ); } else { return const Center(child: CircularProgressIndicator()); } }, ), ), ), ); } Widget _avatarSection(MyUserState state, BuildContext context) { return Container( color: _kPrimary, padding: const EdgeInsets.only(bottom: 28), child: Center( child: Stack( children: [ GestureDetector( onTap: () async { final picker = ImagePicker(); final image = await picker.pickImage( source: ImageSource.gallery, maxHeight: 500, maxWidth: 500, imageQuality: 40, ); if (image != null) setState(() => _imageFile = image); }, child: Hero( tag: 'picture-profile', child: _avatarContainer(state), ), ), Positioned( bottom: 0, right: 0, child: Container( width: 30, height: 30, decoration: BoxDecoration( color: Colors.white, shape: BoxShape.circle, boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.15), blurRadius: 4) ], ), child: const Icon(Icons.camera_alt_outlined, size: 16, color: _kPrimary), ), ), ], ), ), ); } Widget _avatarContainer(MyUserState state) { ImageProvider? imageProvider; if (_imageFile?.path != null && _imageFile!.path.isNotEmpty) { imageProvider = FileImage(File(_imageFile!.path)); } else if (state.user?.picture != null && state.user!.picture!.isNotEmpty) { imageProvider = NetworkImage(state.user!.picture!); } return Container( width: 96, height: 96, decoration: BoxDecoration( color: Colors.white.withOpacity(0.2), shape: BoxShape.circle, border: Border.all(color: Colors.white, width: 3), image: imageProvider == null ? null : DecorationImage(image: imageProvider, fit: BoxFit.cover), ), child: imageProvider == null ? const Icon(CupertinoIcons.person, color: Colors.white70, size: 44) : null, ); } Widget _formSection(MyUserState state, BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const SizedBox(height: 16), _field( controller: _nameController, label: 'Nombre', icon: Icons.person_outline_rounded, validator: (v) => (v == null || v.isEmpty) ? 'Ingresa tu nombre' : null, ), const SizedBox(height: 14), TextFormField( controller: _cityController, readOnly: true, onTap: () async { final cityName = await Navigator.push( context, CupertinoPageRoute(builder: (_) => const CityListScreen()), ); if (cityName != null) _cityController.text = cityName; }, decoration: _inputDeco( 'Ciudad', Icons.location_city_outlined, hint: 'Selecciona tu ciudad', suffixIcon: _isLocating ? const Padding( padding: EdgeInsets.all(12), child: SizedBox( width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2), ), ) : IconButton( icon: const Icon(Icons.my_location_outlined), tooltip: 'Usar mi ubicación', onPressed: _useMyLocation, ), ), ), if (_birthdayController.text.isEmpty && _genderController.text.isEmpty) ...[ const SizedBox(height: 14), BirthdayPicker( onDateSelected: (d) => _birthdayController.text = DateFormat('dd/MM/yyyy').format(d), controller: _birthdayController, ), const SizedBox(height: 14), GenderDropdown(controller: _genderController), ], const SizedBox(height: 20), Container( decoration: BoxDecoration( color: context.card, borderRadius: BorderRadius.circular(12), boxShadow: [BoxShadow(color: context.shadowSm, blurRadius: 8)], ), child: Column( children: [ ProfileItem( title: 'Inicio de sesión con correo', subtitle: _emailController.text, leading: Icons.email_outlined, onTap: () { if (!_validateProfile(state, context)) return; Navigator.push( context, CupertinoPageRoute( builder: (_) => _emailController.text.isEmpty ? ProfileRegisterEmailScreen() : ProfileUpdatePasswordScreen( email: _emailController.text), ), ); }, ), Divider( height: 1, thickness: 0.5, color: context.onSurface.withOpacity(0.08)), ProfileItem( title: 'Inicio de sesión con celular', subtitle: _phoneController.text, leading: Icons.phone_iphone_rounded, onTap: () { if (!_validateProfile(state, context)) return; if (_phoneController.text.isEmpty) { Navigator.push( context, CupertinoPageRoute( builder: (_) => const ProfileRegisterPhoneScreen()), ); } }, ), ], ), ), const SizedBox(height: 16), ], ), ); } bool _validateProfile(MyUserState state, BuildContext context) { if (state.user!.name == null || state.user!.name == '') { ScaffoldMessenger.of(context).clearSnackBars(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Por favor, ingresa tu nombre'))); return false; } if (state.user!.city == null || state.user!.city == '') { ScaffoldMessenger.of(context).clearSnackBars(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Por favor, ingresa tu ciudad'))); return false; } return true; } Widget _saveButton(MyUserState state, BuildContext context) { return ElevatedButton( onPressed: () { if (isLoading) return; if (_nameController.text.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Por favor, ingresa tu nombre'))); return; } if (_cityController.text.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Por favor, ingresa tu ciudad'))); return; } final myUser = state.user!.copyWith( name: _nameController.text, city: _cityController.text, nickname: _nameController.text.trim().toLowerCase(), email: _emailController.text, phone: _phoneController.text, birthday: _birthdayController.text, gender: _genderController.text, ); context .read() .add(UpdateUserInfo(myUser: myUser, filePicture: _imageFile?.path)); ScaffoldMessenger.of(context).clearSnackBars(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Actualizando información...'))); Navigator.pop(context); }, style: ElevatedButton.styleFrom( backgroundColor: _kPrimary, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 15), elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), child: isLoading ? const SizedBox( height: 20, width: 20, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white)) : const Text('Guardar cambios', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), ); } static InputDecoration _inputDeco(String label, IconData icon, {String? hint, Widget? suffixIcon}) { return InputDecoration( labelText: label, hintText: hint, prefixIcon: Icon(icon), suffixIcon: suffixIcon, border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: Colors.grey.shade300), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: _kPrimary, width: 2), ), ); } Widget _field({ required TextEditingController controller, required String label, required IconData icon, String? Function(String?)? validator, }) { return TextFormField( controller: controller, decoration: _inputDeco(label, icon), validator: validator, ); } }