import 'dart:typed_data'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:prosapp_web_app/models/city.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/labels/custom_labels.dart'; import 'package:provider/provider.dart'; // ─── helpers ──────────────────────────────────────────────────────────────── String _avatarUrl(String name) { final encoded = Uri.encodeComponent(name.isEmpty ? 'U' : name); return 'https://ui-avatars.com/api/?name=$encoded&background=42A4EF&color=fff&size=200&bold=true&rounded=true'; } InputDecoration _field(String label, String hint, IconData icon, {Color? fill, Color? border}) => InputDecoration( labelText: label, hintText: hint, prefixIcon: Icon(icon, size: 20), filled: true, fillColor: fill, 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: border ?? Colors.grey.shade300)), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: const BorderSide(color: Color(0xFF42A4EF), width: 2)), ); // ─── ProfileView ───────────────────────────────────────────────────────────── class ProfileView extends StatefulWidget { const ProfileView({super.key}); @override State createState() => _ProfileViewState(); } class _ProfileViewState extends State { @override void initState() { super.initState(); // Inicializa el form provider con el usuario actual final auth = context.read(); final pfp = context.read(); pfp.user = auth.user; } @override Widget build(BuildContext context) { return LayoutBuilder(builder: (context, c) { final narrow = c.maxWidth < 700; const body = _ProfileBody(); return narrow ? ListView( physics: const ClampingScrollPhysics(), padding: const EdgeInsets.symmetric(vertical: 12), children: [body], ) : ListView( physics: const ClampingScrollPhysics(), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), children: [ Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 900), child: body), ) ], ); }); } } // ─── _ProfileBody ───────────────────────────────────────────────────────────── class _ProfileBody extends StatelessWidget { const _ProfileBody(); @override Widget build(BuildContext context) { return LayoutBuilder(builder: (context, c) { if (c.maxWidth < 700) { return const Column(children: [ _AvatarCard(), SizedBox(height: 12), _InfoCard(), SizedBox(height: 12), _EmailCard(), SizedBox(height: 24), ]); } return Row( crossAxisAlignment: CrossAxisAlignment.start, children: const [ SizedBox(width: 260, child: _AvatarCard()), SizedBox(width: 16), Expanded( child: Column(children: [ _InfoCard(), SizedBox(height: 12), _EmailCard(), ]), ), ], ); }); } } // ─── _AvatarCard ───────────────────────────────────────────────────────────── class _AvatarCard extends StatelessWidget { const _AvatarCard(); @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; final cardBg = isDark ? const Color(0xFF1E293B) : Colors.white; final user = context.watch().user!; final pfp = context.watch().user?.picture; final hasPic = pfp != null && pfp.isNotEmpty; final imgUrl = hasPic ? pfp : _avatarUrl(user.name); return Container( width: double.infinity, margin: const EdgeInsets.all(4), padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 16), decoration: BoxDecoration( color: cardBg, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.06), blurRadius: 6) ], ), child: Column(children: [ Text(user.name, style: CustomLabels.h2, textAlign: TextAlign.center), const SizedBox(height: 16), Stack(children: [ ClipOval( child: SizedBox( width: 130, height: 130, child: Image.network(imgUrl, fit: BoxFit.cover, errorBuilder: (_, __, ___) => Image.network( _avatarUrl(user.name), fit: BoxFit.cover)), ), ), Positioned( bottom: 4, right: 4, child: _CameraButton(), ), ]), const SizedBox(height: 12), if (user.phone != null && user.phone!.isNotEmpty) Text(user.phone!, style: TextStyle( color: isDark ? Colors.white54 : Colors.black45, fontSize: 13)), ]), ); } } class _CameraButton extends StatelessWidget { @override Widget build(BuildContext context) { final pfp = context.watch(); return GestureDetector( onTap: () async { final result = await FilePicker.platform.pickFiles(withData: true); if (result == null) return; final bytes = result.files.first.bytes; if (bytes == null) return; NotificationsService.showBusyIndicator(context); await pfp.uploadPicture(bytes); if (context.mounted) { Navigator.pop(context); context.read().refreshUser(); } }, child: Container( width: 34, height: 34, decoration: BoxDecoration( color: const Color(0xFF42A4EF), shape: BoxShape.circle, border: Border.all(color: Colors.white, width: 2), ), child: const Icon(Icons.camera_alt_outlined, size: 16, color: Colors.white), ), ); } } // ─── _InfoCard ──────────────────────────────────────────────────────────────── class _InfoCard extends StatelessWidget { const _InfoCard(); @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; final cardBg = isDark ? const Color(0xFF1E293B) : Colors.white; final fillColor = isDark ? const Color(0xFF0F172A) : const Color(0xFFF8FAFF); final borderColor = isDark ? const Color(0xFF334155) : const Color(0xFFE0E7FF); final pfp = context.watch(); final cities = context.watch(); final user = pfp.user!; // Ciudad que coincide con la guardada String? currentCity; if (user.city != null && user.city!.isNotEmpty && cities.cities.isNotEmpty) { final saved = _normalize(user.city!); currentCity = cities.cities .map((c) => c.cityName) .cast() .firstWhere( (n) => _normalize(n!) == saved, orElse: () => null); } return Container( width: double.infinity, margin: const EdgeInsets.all(4), padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: cardBg, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.06), blurRadius: 6) ], ), child: Form( key: pfp.formKey, autovalidateMode: AutovalidateMode.onUserInteraction, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Información personal', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 15, color: isDark ? Colors.white : Colors.black87)), const Divider(height: 20), // Nombre TextFormField( initialValue: user.name, validator: (v) => (v == null || v.trim().length < 3) ? 'Mínimo 3 caracteres' : null, onChanged: (v) => pfp.copyUserWith(name: v), decoration: _field('Nombre', 'Tu nombre completo', Icons.person_outline, fill: fillColor, border: borderColor), ), const SizedBox(height: 12), // Teléfono (read-only) TextFormField( readOnly: true, initialValue: user.phone ?? '', onTap: (user.phone == null || user.phone!.isEmpty) ? () => NavigationService.navigateTo(Flurorouter.phoneRoute) : null, decoration: _field( 'Teléfono', 'Número de teléfono', Icons.phone, fill: fillColor, border: borderColor), ), const SizedBox(height: 12), // Ciudad cities.isLoading ? const Center( child: Padding( padding: EdgeInsets.symmetric(vertical: 16), child: CircularProgressIndicator(), ), ) : DropdownButtonFormField( key: ValueKey(cities.cities.length), value: currentCity, isExpanded: true, validator: (v) => v == null ? 'Selecciona tu ciudad' : null, decoration: _field( 'Ciudad', 'Selecciona tu ciudad', Icons.location_city_outlined, fill: fillColor, border: borderColor), items: cities.cities.map((City c) { return DropdownMenuItem( value: c.cityName, child: Text( '${c.cityName} – ${c.stateOfCity}', style: TextStyle( fontSize: 13, color: isDark ? Colors.white : Colors.black87), overflow: TextOverflow.ellipsis, ), ); }).toList(), onChanged: (v) { if (v != null) pfp.copyUserWith(city: v); }, ), const SizedBox(height: 20), SizedBox( width: double.infinity, child: ElevatedButton( onPressed: () async { await pfp.updateUserInfo(); if (context.mounted) { context.read().refreshUser(); NotificationsService.showSnackbar('Perfil actualizado'); } }, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF42A4EF), foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10)), elevation: 0, ), child: const Text('Guardar cambios', style: TextStyle(fontWeight: FontWeight.w600)), ), ), ], ), ), ); } String _normalize(String s) => s .toLowerCase() .trim() .replaceAll('á', 'a') .replaceAll('é', 'e') .replaceAll('í', 'i') .replaceAll('ó', 'o') .replaceAll('ú', 'u') .replaceAll('ü', 'u') .replaceAll('ñ', 'n'); } // ─── _EmailCard ─────────────────────────────────────────────────────────────── class _EmailCard extends StatefulWidget { const _EmailCard(); @override State<_EmailCard> createState() => _EmailCardState(); } class _EmailCardState extends State<_EmailCard> { final _emailCtrl = TextEditingController(); final _passCtrl = TextEditingController(); final _confirmCtrl = TextEditingController(); final _codeCtrl = TextEditingController(); 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)) { _err('Ingresa un correo válido'); return; } if (pass.length < 6) { _err('La contraseña debe tener mínimo 6 caracteres'); return; } if (pass != _confirmCtrl.text) { _err('Las contraseñas no coinciden'); return; } setState(() => _loading = true); try { await ApiService.instance .post('/auth/send-email-otp', {'email': email}); setState(() => _codeSent = true); NotificationsService.showSnackbar('Código enviado a $email'); } catch (_) { _err('No se pudo enviar el código. Verifica el correo.'); } finally { setState(() => _loading = false); } } Future _verify() async { final code = _codeCtrl.text.trim(); if (code.length != 6) { _err('Ingresa los 6 dígitos'); 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(); NotificationsService.showSnackbar('Correo vinculado correctamente'); } catch (_) { _err('Código incorrecto o expirado'); } finally { if (mounted) setState(() => _loading = false); } } void _err(String m) => NotificationsService.showSnackBarError(m); @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; final cardBg = isDark ? const Color(0xFF1E293B) : Colors.white; final fillColor = isDark ? const Color(0xFF0F172A) : const Color(0xFFF8FAFF); final borderColor = isDark ? const Color(0xFF334155) : const Color(0xFFE0E7FF); final user = context.watch().user!; final hasEmail = user.email != null && user.email!.isNotEmpty; return Container( width: double.infinity, margin: const EdgeInsets.all(4), padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: cardBg, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.06), blurRadius: 6) ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // ─ Cabecera ─ Row(children: [ Container( width: 38, height: 38, 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, ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( hasEmail ? 'Correo vinculado' : 'Vincular correo electrónico', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 14, color: isDark ? Colors.white : Colors.black87), ), Text( hasEmail ? user.email! : 'Inicia sesión también con email y contraseña', style: TextStyle( fontSize: 12, color: isDark ? Colors.white38 : Colors.black45), ), ], ), ), if (hasEmail) const Icon(Icons.verified, color: Color(0xFF10B981), size: 22), ]), // ─ Formulario (solo si no tiene email) ─ if (!hasEmail) ...[ const SizedBox(height: 16), const Divider(height: 1), const SizedBox(height: 16), if (!_codeSent) ...[ // Paso 1 TextField( controller: _emailCtrl, keyboardType: TextInputType.emailAddress, decoration: _field('Correo electrónico', 'tu@correo.com', Icons.email_outlined, fill: fillColor, border: borderColor), ), const SizedBox(height: 10), TextField( controller: _passCtrl, obscureText: _obscurePass, decoration: _field('Contraseña', 'Mínimo 6 caracteres', Icons.lock_outline, fill: fillColor, border: borderColor) .copyWith( suffixIcon: IconButton( icon: Icon( _obscurePass ? Icons.visibility_off_outlined : Icons.visibility_outlined, size: 18, color: Colors.grey), onPressed: () => setState(() => _obscurePass = !_obscurePass), ), ), ), const SizedBox(height: 10), TextField( controller: _confirmCtrl, obscureText: _obscureConfirm, decoration: _field('Confirmar contraseña', 'Repite la contraseña', Icons.lock_outline, fill: fillColor, border: borderColor) .copyWith( suffixIcon: IconButton( icon: Icon( _obscureConfirm ? Icons.visibility_off_outlined : Icons.visibility_outlined, size: 18, color: Colors.grey), onPressed: () => setState(() => _obscureConfirm = !_obscureConfirm), ), ), ), const SizedBox(height: 16), 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.all(12), decoration: BoxDecoration( color: const Color(0xFF42A4EF).withOpacity(0.08), borderRadius: BorderRadius.circular(10), border: Border.all( color: const Color(0xFF42A4EF).withOpacity(0.3)), ), 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: 30, fontWeight: FontWeight.bold, letterSpacing: 14), decoration: InputDecoration( counterText: '', hintText: '· · · · · ·', hintStyle: TextStyle( letterSpacing: 8, color: Colors.grey.shade400), 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)), ), ), ]), ], ], ], ), ); } }