import 'dart:typed_data'; import 'package:file_picker/file_picker.dart'; import 'package:prosapp_web_app/models/city.dart'; import 'package:prosapp_web_app/models/usuario.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/profile_form_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: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/labels/custom_labels.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; class ProfileView extends StatefulWidget { const ProfileView({super.key}); @override State createState() => _ProfileViewState(); } class _ProfileViewState extends State { Usuario? user; List cities = []; @override void initState() { super.initState(); final authProvider = Provider.of(context, listen: false); final profileFormProvider = Provider.of(context, listen: false); final citiesProvider = Provider.of(context, listen: false); profileFormProvider.user = authProvider.user; setState(() { cities = citiesProvider.cities; user = authProvider.user; }); } @override Widget build(BuildContext context) { return LayoutBuilder(builder: (context, constraints) { if (constraints.maxWidth < 700) { return ListView( physics: const ClampingScrollPhysics(), children: const [SizedBox(height: 10), _ProfileViewBody()], ); } else { return Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), child: ListView( physics: const ClampingScrollPhysics(), children: const [SizedBox(height: 10), _ProfileViewBody()], ), ); } }); } } class _ProfileViewBody extends StatelessWidget { const _ProfileViewBody(); @override Widget build(BuildContext context) { return LayoutBuilder(builder: (context, constraints) { if (constraints.maxWidth < 700) { return const Column( children: [ _AvatarContainer(containerFull: true), _ProfileViewForm(), _EmailLinkCard(), ], ); } else { return Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 900), child: const Column( children: [ IntrinsicHeight( child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox(width: 250, child: _AvatarContainer(containerFull: true)), Expanded(child: _ProfileViewForm()), ], ), ), _EmailLinkCard(), ], ), ), ); } }); } } class _ProfileViewForm extends StatelessWidget { const _ProfileViewForm(); @override Widget build(BuildContext context) { final profileFormProvider = Provider.of(context); final citiesProvider = Provider.of(context); final cities = citiesProvider.cities; final user = profileFormProvider.user!; final isDark = Theme.of(context).brightness == Brightness.dark; final textColor = isDark ? Colors.white : Colors.black87; // Normaliza el valor actual para que coincida exactamente con la lista final cityNames = cities.map((c) => c.cityName).toList(); String? currentCity; if (user.city != null && user.city!.isNotEmpty) { final saved = user.city!.toLowerCase().trim(); currentCity = cityNames.firstWhere( (n) => n.toLowerCase().trim() == saved, orElse: () => '', ); if (currentCity!.isEmpty) currentCity = null; } return WhiteCard( title: 'Información general', child: Form( key: profileFormProvider.formKey, autovalidateMode: AutovalidateMode.always, child: Column( children: [ const SizedBox(height: 10), TextFormField( initialValue: user.name, validator: (value) { if (value == null || value.isEmpty) { return 'El nombre es obligatorio'; } if (value.trim().length < 4) { return 'El nombre debe tener al menos 4 caracteres'; } return null; }, onChanged: (value) { profileFormProvider.copyUserWith(name: value); }, decoration: CustomInputs.formInputDecoration( hint: 'Nombre de usuario', label: 'Nombre', icon: Icons.person_outline, ), ), const SizedBox(height: 20), TextFormField( readOnly: true, onTap: user.phone == null || user.phone!.isEmpty ? () { NavigationService.navigateTo(Flurorouter.phoneRoute); } : null, initialValue: user.phone ?? '', decoration: CustomInputs.formInputDecoration( hint: 'Número de teléfono', label: 'Teléfono', icon: Icons.phone, ), ), const SizedBox(height: 20), TextFormField( readOnly: true, onTap: user.email == null || user.email!.isEmpty ? () { NavigationService.navigateTo(Flurorouter.emailRoute); } : null, initialValue: user.email ?? '', decoration: CustomInputs.formInputDecoration( hint: 'Correo del usuario', label: 'Correo', icon: Icons.email_outlined, ), ), const SizedBox(height: 20), if (citiesProvider.isLoading) const Padding( padding: EdgeInsets.symmetric(vertical: 16), child: Center(child: CircularProgressIndicator()), ) else DropdownButtonFormField( validator: (value) { if (value == null) return 'La ciudad es obligatoria'; return null; }, value: currentCity, isExpanded: true, decoration: CustomInputs.formInputDecoration( hint: 'Selecciona tu ciudad', label: 'Ciudad', icon: Icons.location_city_outlined, ), items: cities.map((City ciudad) { return DropdownMenuItem( value: ciudad.cityName, child: Text( '${ciudad.cityName} - ${ciudad.stateOfCity}', style: TextStyle( color: textColor, fontWeight: FontWeight.normal, ), overflow: TextOverflow.ellipsis, ), ); }).toList(), onChanged: (value) { if (value != null) profileFormProvider.copyUserWith(city: value); }, ), const SizedBox(height: 20), ConstrainedBox( constraints: const BoxConstraints(maxWidth: 130), child: ElevatedButton( onPressed: () async { await profileFormProvider.updateUserInfo(); Provider.of(context, listen: false) .refreshUser(); }, 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('Guardar', style: TextStyle(color: Colors.white)), ), ), const SizedBox(height: 10), ], ), ), ); } } String _uiAvatarUrl(String name) { final encoded = Uri.encodeComponent(name.isEmpty ? 'Usuario' : name); return 'https://ui-avatars.com/api/?name=$encoded&background=42A4EF&color=fff&size=200&bold=true&rounded=true'; } class _AvatarContainer extends StatelessWidget { final bool containerFull; const _AvatarContainer({required this.containerFull}); @override Widget build(BuildContext context) { final user = Provider.of(context).user!; final profileFormProvider = Provider.of(context); final picUrl = profileFormProvider.user!.picture; final hasPicture = picUrl != null && picUrl.isNotEmpty; final userName = profileFormProvider.user!.name; final imageUrl = hasPicture ? picUrl! : _uiAvatarUrl(userName); final image = Image.network(imageUrl, fit: BoxFit.cover, errorBuilder: (_, __, ___) => Image.network(_uiAvatarUrl(userName), fit: BoxFit.cover)); return WhiteCard( width: containerFull ? null : 250, child: SizedBox( width: double.infinity, child: Column( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: [ Text( user.name, style: CustomLabels.h2, textAlign: TextAlign.center, ), const SizedBox(height: 10), SizedBox( width: 160, height: 160, child: Stack( children: [ SizedBox( width: 200, height: 200, child: ClipOval(child: image), ), Positioned( bottom: 5, right: 5, child: Container( width: 45, height: 45, decoration: BoxDecoration( borderRadius: BorderRadius.circular(100), border: Border.all(color: Colors.white, width: 5), ), child: FloatingActionButton( onPressed: () async { FilePickerResult? result = await FilePicker.platform.pickFiles( withData: true, ); if (result != null) { PlatformFile file = result.files.first; Uint8List? fileBytes = file.bytes; if (fileBytes != null) { NotificationsService.showBusyIndicator(context); await profileFormProvider .uploadPicture(fileBytes); Provider.of(context, listen: false) .refreshUser(); Navigator.pop(context); } } else {} }, backgroundColor: Colors.indigo, elevation: 0, child: const Icon( Icons.camera_alt_outlined, size: 20, color: Colors.white, ), ), ), ) ], ), ), const SizedBox(height: 20), ], ), ), ); } } // ── Sección vinculación correo ─────────────────────────────────────────────── class _EmailLinkCard extends StatefulWidget { const _EmailLinkCard(); @override State<_EmailLinkCard> createState() => _EmailLinkCardState(); } class _EmailLinkCardState extends State<_EmailLinkCard> { final _emailCtrl = TextEditingController(); final _passCtrl = TextEditingController(); final _confirmCtrl = TextEditingController(); final _codeCtrl = TextEditingController(); bool _expanded = false; bool _codeSent = false; bool _loading = false; bool _obscurePass = true; bool _obscureConfirm = true; @override void dispose() { _emailCtrl.dispose(); _passCtrl.dispose(); _confirmCtrl.dispose(); _codeCtrl.dispose(); super.dispose(); } Future _sendCode() async { final email = _emailCtrl.text.trim(); final pass = _passCtrl.text; if (!RegExp(r'^[\w.+-]+@[\w-]+\.\w+$').hasMatch(email)) { _snack('Ingresa un correo válido', error: true); return; } if (pass.length < 6) { _snack('La contraseña debe tener al menos 6 caracteres', error: true); return; } if (pass != _confirmCtrl.text) { _snack('Las contraseñas no coinciden', error: true); return; } setState(() => _loading = true); try { await ApiService.instance.post('/auth/send-email-otp', {'email': email}); setState(() => _codeSent = true); _snack('Código enviado a $email'); } catch (_) { _snack('Error al enviar el código', error: true); } finally { setState(() => _loading = false); } } Future _verify() async { final code = _codeCtrl.text.trim(); if (code.length != 6) { _snack('Ingresa el código de 6 dígitos', error: true); return; } setState(() => _loading = true); try { await ApiService.instance.post('/auth/link-email-otp', { 'email': _emailCtrl.text.trim(), 'password': _passCtrl.text, 'code': code, }); await context.read().refreshUser(); setState(() { _codeSent = false; _expanded = false; _codeCtrl.clear(); }); _snack('Correo vinculado correctamente'); } catch (_) { _snack('Código incorrecto o expirado', error: true); } finally { setState(() => _loading = false); } } void _snack(String m, {bool error = false}) => error ? NotificationsService.showSnackBarError(m) : NotificationsService.showSnackbar(m); @override Widget build(BuildContext context) { final user = context.watch().user!; final hasEmail = user.email != null && user.email!.isNotEmpty; final isDark = Theme.of(context).brightness == Brightness.dark; final fillColor = isDark ? const Color(0xFF0F172A) : const Color(0xFFF8FAFF); final borderColor = isDark ? const Color(0xFF334155) : const Color(0xFFE0E7FF); InputDecoration fd(String label, String hint, IconData icon) => InputDecoration( labelText: label, hintText: hint, prefixIcon: Icon(icon, size: 20), filled: true, fillColor: fillColor, contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), border: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide.none), enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: borderColor)), focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: const BorderSide(color: Color(0xFF42A4EF), width: 2)), ); return WhiteCard( child: Theme( data: Theme.of(context).copyWith(dividerColor: Colors.transparent), child: ExpansionTile( initiallyExpanded: hasEmail, onExpansionChanged: (v) { if (!hasEmail) setState(() => _expanded = v); }, tilePadding: EdgeInsets.zero, childrenPadding: EdgeInsets.zero, leading: Container( width: 36, height: 36, decoration: BoxDecoration( color: hasEmail ? const Color(0xFF10B981).withOpacity(0.12) : const Color(0xFF42A4EF).withOpacity(0.12), shape: BoxShape.circle, ), child: Icon( hasEmail ? Icons.mark_email_read_outlined : Icons.email_outlined, color: hasEmail ? const Color(0xFF10B981) : const Color(0xFF42A4EF), size: 18, ), ), title: Text( hasEmail ? 'Correo vinculado' : 'Vincular correo electrónico', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: isDark ? Colors.white : Colors.black87, ), ), subtitle: Text( hasEmail ? user.email! : 'Agrega un correo y contraseña para iniciar sesión sin SMS', style: TextStyle(fontSize: 12, color: isDark ? Colors.white38 : Colors.black45), ), trailing: hasEmail ? const Icon(Icons.verified, color: Color(0xFF10B981), size: 20) : null, children: [ if (!hasEmail) ...[ const Divider(height: 1), const SizedBox(height: 16), if (!_codeSent) ...[ TextField( controller: _emailCtrl, keyboardType: TextInputType.emailAddress, decoration: fd('Correo electrónico', 'tu@correo.com', Icons.email_outlined), ), const SizedBox(height: 10), TextField( controller: _passCtrl, obscureText: _obscurePass, decoration: fd('Contraseña', 'Mínimo 6 caracteres', Icons.lock_outline).copyWith( suffixIcon: IconButton( icon: Icon(_obscurePass ? Icons.visibility_off_outlined : Icons.visibility_outlined, size: 18), onPressed: () => setState(() => _obscurePass = !_obscurePass), ), ), ), const SizedBox(height: 10), TextField( controller: _confirmCtrl, obscureText: _obscureConfirm, decoration: fd('Confirmar contraseña', 'Repite la contraseña', Icons.lock_outline).copyWith( suffixIcon: IconButton( icon: Icon(_obscureConfirm ? Icons.visibility_off_outlined : Icons.visibility_outlined, size: 18), onPressed: () => setState(() => _obscureConfirm = !_obscureConfirm), ), ), ), const SizedBox(height: 14), SizedBox( width: double.infinity, child: ElevatedButton.icon( onPressed: _loading ? null : _sendCode, icon: _loading ? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2)) : const Icon(Icons.send_outlined, size: 18), label: Text(_loading ? 'Enviando...' : 'Enviar código de verificación'), style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF42A4EF), foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), elevation: 0, ), ), ), ] else ...[ // Paso 2: ingresar código Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), decoration: BoxDecoration( color: const Color(0xFF42A4EF).withOpacity(0.08), borderRadius: BorderRadius.circular(10), border: Border.all(color: const Color(0xFF42A4EF).withOpacity(0.25)), ), child: Row(children: [ const Icon(Icons.mark_email_read_outlined, color: Color(0xFF42A4EF), size: 18), const SizedBox(width: 8), Expanded(child: Text( 'Código enviado a ${_emailCtrl.text.trim()}', style: const TextStyle(fontSize: 13, color: Color(0xFF42A4EF)), )), ]), ), const SizedBox(height: 14), TextField( controller: _codeCtrl, keyboardType: TextInputType.number, textAlign: TextAlign.center, maxLength: 6, autofocus: true, style: const TextStyle(fontSize: 28, fontWeight: FontWeight.bold, letterSpacing: 12), decoration: InputDecoration( counterText: '', hintText: '······', hintStyle: const TextStyle(letterSpacing: 12, color: Colors.grey), filled: true, fillColor: fillColor, border: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide.none), enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: borderColor)), focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: const BorderSide(color: Color(0xFF42A4EF), width: 2)), ), ), const SizedBox(height: 14), Row(children: [ Expanded( child: OutlinedButton( onPressed: _loading ? null : () => setState(() { _codeSent = false; _codeCtrl.clear(); }), style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 13), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), side: BorderSide(color: borderColor), ), child: const Text('← Volver'), ), ), const SizedBox(width: 10), Expanded( child: ElevatedButton( onPressed: _loading ? null : _verify, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF10B981), foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 13), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), elevation: 0, ), child: _loading ? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2)) : const Text('Verificar', style: TextStyle(fontWeight: FontWeight.w600)), ), ), ]), ], const SizedBox(height: 8), ], ], ), ), ); } }