import 'dart:typed_data'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:prosapp_web_app/models/pro_state.dart'; import 'package:prosapp_web_app/providers/auth_provider.dart'; import 'package:prosapp_web_app/providers/profile_form_provider.dart'; import 'package:prosapp_web_app/providers/theme_provider.dart'; import 'package:prosapp_web_app/services/api_service.dart'; import 'package:prosapp_web_app/services/notifications_service.dart'; import 'package:provider/provider.dart'; // ─── helpers ───────────────────────────────────────────────────────────────── String _initials(String name) { final parts = name.trim().split(' ').where((s) => s.isNotEmpty).toList(); if (parts.isEmpty) return 'U'; if (parts.length == 1) return parts[0][0].toUpperCase(); return (parts[0][0] + parts[1][0]).toUpperCase(); } InputDecoration _dec(bool isDark, Color border, String label, IconData icon) { final fill = isDark ? const Color(0xFF0F172A) : const Color(0xFFF9FAFB); final hint = isDark ? const Color(0xFF64748B) : const Color(0xFF9CA3AF); return InputDecoration( labelText: label, labelStyle: TextStyle(color: hint, fontSize: 13), prefixIcon: Icon(icon, size: 18, color: const Color(0xFF42A4EF)), filled: true, fillColor: fill, contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 13), border: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: border)), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: border)), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: const BorderSide(color: Color(0xFF42A4EF), width: 2)), disabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: border.withOpacity(0.5))), ); } // ─── ProfileView ────────────────────────────────────────────────────────────── class ProfileView extends StatefulWidget { const ProfileView({super.key}); @override State createState() => _ProfileViewState(); } class _ProfileViewState extends State { @override void initState() { super.initState(); final auth = context.read(); context.read().user = auth.user; } @override Widget build(BuildContext context) { return LayoutBuilder(builder: (_, c) { final wide = c.maxWidth >= 700; return ListView( padding: EdgeInsets.symmetric( horizontal: wide ? 24 : 12, vertical: 16), children: [ Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 900), child: Column( children: [ const _HeroCard(), const SizedBox(height: 16), if (wide) Row( crossAxisAlignment: CrossAxisAlignment.start, children: const [ Expanded(flex: 3, child: _InfoCard()), SizedBox(width: 14), Expanded(flex: 2, child: _EmailCard()), ], ) else ...[ const _InfoCard(), const SizedBox(height: 14), const _EmailCard(), ], const SizedBox(height: 24), ], ), ), ), ], ); }); } } // ─── _HeroCard ──────────────────────────────────────────────────────────────── class _HeroCard extends StatelessWidget { const _HeroCard(); @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 user = context.watch().user!; final pfp = context.watch().user?.picture; final hasPic = pfp != null && pfp.isNotEmpty; final proInt = enumToInt(user.proState); final proLabel = ['Sin solicitud', 'En revisión', 'Profesional', 'No aprobado'][proInt.clamp(0, 3)]; final proColor = [Colors.grey, const Color(0xFFF59E0B), const Color(0xFF10B981), const Color(0xFFEF4444)][proInt.clamp(0, 3)]; final proIcon = [Icons.person_outline, Icons.hourglass_empty_rounded, Icons.verified_rounded, Icons.cancel_outlined][proInt.clamp(0, 3)]; return Container( decoration: BoxDecoration( color: cardBg, borderRadius: BorderRadius.circular(16), border: Border.all(color: border), ), clipBehavior: Clip.antiAlias, child: Column( children: [ // Banner degradado Container( height: 90, decoration: const BoxDecoration( gradient: LinearGradient( colors: [Color(0xFF1565C0), Color(0xFF42A4EF)], begin: Alignment.topLeft, end: Alignment.bottomRight, ), ), ), // Avatar + info Transform.translate( offset: const Offset(0, -44), child: Column( children: [ // Avatar con botón cámara Stack( children: [ Container( width: 88, height: 88, decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( color: cardBg, width: 4), color: const Color(0xFF42A4EF), ), child: ClipOval( child: hasPic ? Image.network(pfp, fit: BoxFit.cover, errorBuilder: (_, __, ___) => _AvatarFallback(user.name)) : _AvatarFallback(user.name), ), ), Positioned( bottom: 2, right: 2, child: _CameraButton(), ), ], ), const SizedBox(height: 8), // Nombre Text(user.name, style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: isDark ? Colors.white : const Color(0xFF111827))), const SizedBox(height: 4), // Teléfono if (user.phone != null && user.phone!.isNotEmpty) Text(user.phone!, style: TextStyle( fontSize: 13, color: isDark ? const Color(0xFF94A3B8) : const Color(0xFF6B7280))), const SizedBox(height: 10), // Badges de estado Wrap( spacing: 8, runSpacing: 6, alignment: WrapAlignment.center, children: [ // Teléfono verificado _Badge( icon: Icons.phone_android, label: user.isPhoneVerified ? 'Tel. verificado' : 'Tel. sin verificar', color: user.isPhoneVerified ? const Color(0xFF10B981) : Colors.grey, ), // Email _Badge( icon: Icons.email_outlined, label: (user.email != null && user.email!.isNotEmpty) ? 'Email vinculado' : 'Sin email', color: (user.email != null && user.email!.isNotEmpty) ? const Color(0xFF42A4EF) : Colors.grey, ), // Pro state _Badge( icon: proIcon, label: proLabel, color: proColor, ), ], ), const SizedBox(height: 14), ], ), ), ], ), ); } } class _AvatarFallback extends StatelessWidget { final String name; const _AvatarFallback(this.name); @override Widget build(BuildContext context) { return Container( color: const Color(0xFF42A4EF), child: Center( child: Text( _initials(name), style: const TextStyle( color: Colors.white, fontSize: 28, fontWeight: FontWeight.bold), ), ), ); } } class _Badge extends StatelessWidget { final IconData icon; final String label; final Color color; const _Badge({required this.icon, required this.label, required this.color}); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(20), border: Border.all(color: color.withOpacity(0.3)), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 13, color: color), const SizedBox(width: 5), Text(label, style: TextStyle( fontSize: 11, color: color, fontWeight: FontWeight.w600)), ], ), ); } } 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, type: FileType.image); if (result == null || result.files.first.bytes == null) return; if (!context.mounted) return; NotificationsService.showBusyIndicator(context); await pfp.uploadPicture(result.files.first.bytes!); if (context.mounted) { Navigator.pop(context); context.read().refreshUser(); } }, child: Container( width: 30, height: 30, 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: 14, color: Colors.white), ), ); } } // ─── _InfoCard ──────────────────────────────────────────────────────────────── class _InfoCard extends StatefulWidget { const _InfoCard(); @override State<_InfoCard> createState() => _InfoCardState(); } class _InfoCardState extends State<_InfoCard> { List _cities = []; bool _loadingCities = true; bool _saving = false; String? _nameError; late TextEditingController _nameCtrl; @override void initState() { super.initState(); final pfp = context.read(); _nameCtrl = TextEditingController(text: pfp.user?.name ?? ''); _loadCities(); } @override void dispose() { _nameCtrl.dispose(); super.dispose(); } 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; _loadingCities = false; }); } catch (_) { if (mounted) setState(() => _loadingCities = false); } } Future _save() async { final name = _nameCtrl.text.trim(); if (name.length < 3) { setState(() => _nameError = 'Mínimo 3 caracteres'); return; } setState(() { _nameError = null; _saving = true; }); try { final pfp = context.read(); pfp.copyUserWith(name: name); await pfp.updateUserInfoNoValid(); if (mounted) context.read().refreshUser(); } catch (_) { if (mounted) NotificationsService.showSnackBarError('Error al guardar'); } finally { if (mounted) setState(() => _saving = false); } } @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 pfp = context.watch(); final user = pfp.user!; // Ciudad seleccionada en la lista String? selectedCity; if (user.city != null && user.city!.isNotEmpty && _cities.isNotEmpty) { final norm = user.city!.toLowerCase().trim(); selectedCity = _cities.firstWhere( (c) => c.toLowerCase().trim() == norm, orElse: () => _cities.firstWhere( (c) => c.toLowerCase().contains(norm) || norm.contains(c.toLowerCase()), orElse: () => '', ), ); if (selectedCity!.isEmpty) selectedCity = null; } return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: cardBg, borderRadius: BorderRadius.circular(16), border: Border.all(color: border), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ const Icon(Icons.person_outline, color: Color(0xFF42A4EF), size: 18), const SizedBox(width: 8), Text('Información personal', style: TextStyle( fontSize: 15, fontWeight: FontWeight.bold, color: textPrimary)), ]), const SizedBox(height: 16), // Nombre TextField( controller: _nameCtrl, style: TextStyle(color: textPrimary, fontSize: 14), onChanged: (_) { if (_nameError != null) setState(() => _nameError = null); }, decoration: _dec(isDark, border, 'Nombre completo', Icons.badge_outlined), ), if (_nameError != null) Padding( padding: const EdgeInsets.only(top: 4, left: 4), child: Text(_nameError!, style: const TextStyle( color: Colors.redAccent, fontSize: 12)), ), const SizedBox(height: 12), // Teléfono (solo lectura) TextField( readOnly: true, controller: TextEditingController(text: user.phone ?? ''), style: TextStyle(color: textSec, fontSize: 14), decoration: _dec(isDark, border, 'Teléfono', Icons.phone_outlined) .copyWith( suffixIcon: user.isPhoneVerified ? const Icon(Icons.verified_rounded, color: Color(0xFF10B981), size: 18) : null, ), ), const SizedBox(height: 12), // Ciudad _loadingCities ? const SizedBox( height: 48, child: Center( child: SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2)))) : _cities.isEmpty ? TextField( readOnly: true, controller: TextEditingController( text: user.city ?? ''), style: TextStyle(color: textPrimary, fontSize: 14), decoration: _dec(isDark, border, 'Ciudad', Icons.location_city_outlined), ) : DropdownButtonFormField( value: selectedCity, dropdownColor: cardBg, isExpanded: true, style: TextStyle(color: textPrimary, fontSize: 14), hint: Text('Selecciona tu ciudad', style: TextStyle(color: textSec, fontSize: 13)), decoration: _dec(isDark, border, 'Ciudad', Icons.location_city_outlined), items: _cities .map((c) => DropdownMenuItem( value: c, child: Text(c, style: TextStyle(color: textPrimary)))) .toList(), onChanged: (v) { if (v != null) pfp.copyUserWith(city: v); }, ), const SizedBox(height: 12), // Género DropdownButtonFormField( value: ['male', 'female', 'other'].contains(user.gender) ? user.gender : null, dropdownColor: cardBg, style: TextStyle(color: textPrimary, fontSize: 14), hint: Text('Selecciona género', style: TextStyle(color: textSec, fontSize: 13)), decoration: _dec(isDark, border, 'Género', Icons.wc_outlined), items: const [ DropdownMenuItem(value: 'male', child: Text('Masculino')), DropdownMenuItem( value: 'female', child: Text('Femenino')), DropdownMenuItem(value: 'other', child: Text('Otro')), ], onChanged: (v) { if (v != null) pfp.copyUserWith(gender: v); }, ), const SizedBox(height: 20), SizedBox( width: double.infinity, height: 46, child: ElevatedButton( onPressed: _saving ? null : _save, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF42A4EF), foregroundColor: Colors.white, disabledBackgroundColor: const Color(0xFF42A4EF).withOpacity(0.4), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10)), elevation: 0, ), child: _saving ? const SizedBox( width: 18, height: 18, child: CircularProgressIndicator( color: Colors.white, strokeWidth: 2)) : const Text('Guardar cambios', style: TextStyle( fontWeight: FontWeight.w600, fontSize: 14)), ), ), ], ), ); } } // ─── _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 { if (mounted) 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, }); if (mounted) { 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 = 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 user = context.watch().user!; final hasEmail = user.email != null && user.email!.isNotEmpty; return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: cardBg, borderRadius: BorderRadius.circular(16), border: Border.all(color: border), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header Row(children: [ 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, size: 17, color: hasEmail ? const Color(0xFF10B981) : const Color(0xFF42A4EF), ), ), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( hasEmail ? 'Correo vinculado' : 'Vincular correo', style: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: textPrimary), ), Text( hasEmail ? user.email! : 'Inicia sesión también con email y contraseña', style: TextStyle(fontSize: 12, color: textSec), overflow: TextOverflow.ellipsis, ), ], ), ), if (hasEmail) const Icon(Icons.verified_rounded, color: Color(0xFF10B981), size: 20), ]), if (!hasEmail) ...[ const SizedBox(height: 16), Divider(height: 1, color: border), const SizedBox(height: 16), if (!_codeSent) ...[ // Paso 1 TextField( controller: _emailCtrl, keyboardType: TextInputType.emailAddress, style: TextStyle(color: textPrimary, fontSize: 14), decoration: _dec(isDark, border, 'Correo electrónico', Icons.email_outlined), ), const SizedBox(height: 10), TextField( controller: _passCtrl, obscureText: _obscurePass, style: TextStyle(color: textPrimary, fontSize: 14), decoration: _dec(isDark, border, 'Contraseña', Icons.lock_outline) .copyWith( suffixIcon: IconButton( icon: Icon( _obscurePass ? Icons.visibility_off_outlined : Icons.visibility_outlined, size: 16, color: textSec), onPressed: () => setState(() => _obscurePass = !_obscurePass), ), ), ), const SizedBox(height: 10), TextField( controller: _confirmCtrl, obscureText: _obscureConfirm, style: TextStyle(color: textPrimary, fontSize: 14), decoration: _dec(isDark, border, 'Confirmar contraseña', Icons.lock_outline) .copyWith( suffixIcon: IconButton( icon: Icon( _obscureConfirm ? Icons.visibility_off_outlined : Icons.visibility_outlined, size: 16, color: textSec), onPressed: () => setState(() => _obscureConfirm = !_obscureConfirm), ), ), ), const SizedBox(height: 14), SizedBox( width: double.infinity, height: 44, child: ElevatedButton.icon( onPressed: _loading ? null : _sendCode, icon: _loading ? const SizedBox( width: 14, height: 14, child: CircularProgressIndicator( color: Colors.white, strokeWidth: 2)) : const Icon(Icons.send_outlined, size: 16), label: Text( _loading ? 'Enviando...' : 'Enviar código al correo', style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600)), style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF42A4EF), foregroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10)), elevation: 0, ), ), ), ] else ...[ // Paso 2 Container( padding: const EdgeInsets.all(11), 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: 16), const SizedBox(width: 8), Expanded( child: Text( 'Código enviado a ${_emailCtrl.text.trim()}', style: const TextStyle( fontSize: 12, color: Color(0xFF42A4EF)), ), ), ]), ), const SizedBox(height: 14), TextField( controller: _codeCtrl, keyboardType: TextInputType.number, textAlign: TextAlign.center, maxLength: 6, autofocus: true, style: TextStyle( fontSize: 28, fontWeight: FontWeight.bold, letterSpacing: 12, color: textPrimary), decoration: InputDecoration( counterText: '', hintText: '· · · · · ·', hintStyle: TextStyle( letterSpacing: 8, color: textSec, fontSize: 20), filled: true, fillColor: isDark ? const Color(0xFF0F172A) : const Color(0xFFF9FAFB), border: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: border)), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: border)), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: const BorderSide( color: Color(0xFF42A4EF), width: 2)), ), ), const SizedBox(height: 12), Row(children: [ Expanded( child: OutlinedButton( onPressed: _loading ? null : () => setState(() { _codeSent = false; _codeCtrl.clear(); }), style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 11), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10)), side: BorderSide(color: border), foregroundColor: textPrimary, ), child: const Text('← Volver', style: TextStyle(fontSize: 13)), ), ), 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: 11), 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, fontSize: 13)), ), ), ]), ], ], ], ), ); } }