// ================================================================ // lib/screens/login_screen.dart (v2) // ================================================================ // // CAMBIOS v2: // - loginConCorreo ahora devuelve también el nombre del usuario // - El nombre se guarda en SharedPreferences para mostrarlo en home // - Mensaje de error más específico (viene del backend) // ================================================================ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../services/api_service.dart'; class LoginScreen extends StatefulWidget { const LoginScreen({super.key}); @override State createState() => _LoginScreenState(); } class _LoginScreenState extends State { final TextEditingController _emailController = TextEditingController(); final TextEditingController _passwordController = TextEditingController(); final TextEditingController _nameController = TextEditingController(); final TextEditingController _direccionController = TextEditingController(); String? _coloniaSeleccionada; List _colonias = []; bool _esRegistro = false; bool _cargandoColonias = true; String? _errorColonias; bool _logueando = false; bool _mostrarPassword = false; final ApiService _apiService = ApiService(); @override void initState() { super.initState(); _cargarColonias(); _verificarSesionExistente(); } @override void dispose() { _emailController.dispose(); _passwordController.dispose(); _nameController.dispose(); _direccionController.dispose(); super.dispose(); } // ---------------------------------------------------------------- // Si ya hay sesión guardada, saltamos el login directamente. // ---------------------------------------------------------------- Future _verificarSesionExistente() async { final prefs = await SharedPreferences.getInstance(); final usuarioId = prefs.getInt('usuario_id'); if (usuarioId != null && mounted) { Navigator.pushReplacementNamed(context, '/home', arguments: usuarioId); } } Future _cargarColonias() async { try { final colonias = await _apiService.obtenerColonias(); if (mounted) { setState(() { _colonias = colonias; _cargandoColonias = false; }); } } catch (e) { if (mounted) { setState(() { _colonias = [ 'Zona Centro', 'Las Arboledas', 'Trojes', 'San Juanico', 'Los Olivos', 'Rancho Seco', 'Las Insurgentes', ]; _cargandoColonias = false; _errorColonias = 'Sin conexión al backend. Usando lista local.'; }); } } } // ---------------------------------------------------------------- // LOGIN // ---------------------------------------------------------------- Future _iniciarSesion() async { final email = _emailController.text.trim(); final password = _passwordController.text; if (email.isEmpty) { _mostrarError('Por favor ingresa tu correo.'); return; } if (password.isEmpty) { _mostrarError('Por favor ingresa tu contraseña.'); return; } setState(() => _logueando = true); try { // v2: devuelve {usuario_id, nombre} final resultado = await _apiService.loginConCorreo(email, password); final prefs = await SharedPreferences.getInstance(); await prefs.setInt('usuario_id', resultado['usuario_id']); await prefs.setString('nombre', resultado['nombre'] ?? ''); await prefs.setString('email', email); if (mounted) { Navigator.pushReplacementNamed(context, '/home', arguments: resultado['usuario_id']); } } catch (e) { // Muestra el mensaje de error que viene del backend (más específico) final mensaje = e.toString().replaceFirst('Exception: ', ''); _mostrarError(mensaje); } finally { if (mounted) setState(() => _logueando = false); } } // ---------------------------------------------------------------- // REGISTRO // ---------------------------------------------------------------- Future _registrarse() async { final nombre = _nameController.text.trim(); final email = _emailController.text.trim(); final password = _passwordController.text; final direccion = _direccionController.text.trim(); if (nombre.isEmpty) { _mostrarError('Por favor ingresa tu nombre.'); return; } if (email.isEmpty) { _mostrarError('Por favor ingresa tu correo.'); return; } if (password.isEmpty) { _mostrarError('Por favor ingresa tu contraseña.'); return; } if (password.length < 6) { _mostrarError('La contraseña debe tener al menos 6 caracteres.'); return; } if (_coloniaSeleccionada == null) { _mostrarError('Por favor selecciona tu colonia.'); return; } if (direccion.isEmpty) { _mostrarError('Por favor ingresa tu dirección.'); return; } setState(() => _logueando = true); try { final usuarioId = await _apiService.registrarUsuario( nombre, email, password, direccion, _coloniaSeleccionada!, ); final prefs = await SharedPreferences.getInstance(); await prefs.setInt('usuario_id', usuarioId); await prefs.setString('nombre', nombre); await prefs.setString('email', email); if (mounted) { Navigator.pushReplacementNamed(context, '/home', arguments: usuarioId); } } catch (e) { final mensaje = e.toString().replaceFirst('Exception: ', ''); _mostrarError(mensaje); } finally { if (mounted) setState(() => _logueando = false); } } void _mostrarError(String mensaje) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(mensaje), backgroundColor: Colors.red.shade700, behavior: SnackBarBehavior.floating, ), ); } // ================================================================ // BUILD // ================================================================ @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return Scaffold( body: SafeArea( child: SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 32), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const SizedBox(height: 24), // ── LOGO ────────────────────────────────────────── Icon(Icons.recycling_rounded, size: 80, color: colorScheme.primary), const SizedBox(height: 16), Text( 'Recolección\nInteligente', textAlign: TextAlign.center, style: Theme.of(context).textTheme.headlineMedium?.copyWith( fontWeight: FontWeight.bold, color: colorScheme.primary, ), ), const SizedBox(height: 8), Text( _esRegistro ? 'Regístrate para recibir alertas del camión' : 'Inicia sesión para ver el estado del camión', textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: Colors.grey.shade600), ), const SizedBox(height: 40), // ── CAMPOS COMUNES ──────────────────────────────── TextField( controller: _emailController, keyboardType: TextInputType.emailAddress, decoration: InputDecoration( labelText: 'Correo electrónico', hintText: 'usuario@ejemplo.com', prefixIcon: const Icon(Icons.email_outlined), border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), ), ), const SizedBox(height: 16), TextField( controller: _passwordController, obscureText: !_mostrarPassword, decoration: InputDecoration( labelText: 'Contraseña', hintText: '••••••••', prefixIcon: const Icon(Icons.lock_outline), suffixIcon: IconButton( icon: Icon(_mostrarPassword ? Icons.visibility_off : Icons.visibility), onPressed: () => setState(() => _mostrarPassword = !_mostrarPassword), ), border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), ), ), const SizedBox(height: 16), // ── CAMPOS EXTRA (solo en registro) ─────────────── if (_esRegistro) ...[ TextField( controller: _nameController, textCapitalization: TextCapitalization.words, decoration: InputDecoration( labelText: 'Nombre completo', prefixIcon: const Icon(Icons.person_outline), border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), ), ), const SizedBox(height: 16), TextField( controller: _direccionController, decoration: InputDecoration( labelText: 'Dirección', hintText: 'Calle, número', prefixIcon: const Icon(Icons.home_outlined), border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), ), ), const SizedBox(height: 16), if (_cargandoColonias) const Center(child: Padding(padding: EdgeInsets.all(16), child: CircularProgressIndicator())) else ...[ if (_errorColonias != null) Padding( padding: const EdgeInsets.only(bottom: 8), child: Text('⚠️ $_errorColonias', style: TextStyle(fontSize: 12, color: Colors.orange.shade700)), ), DropdownButtonFormField( value: _coloniaSeleccionada, hint: const Text('Selecciona tu colonia'), decoration: InputDecoration( prefixIcon: const Icon(Icons.location_city_outlined), border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), ), items: _colonias.map((c) => DropdownMenuItem(value: c, child: Text(c))).toList(), onChanged: (valor) => setState(() => _coloniaSeleccionada = valor), ), ], const SizedBox(height: 8), // Indicador de fortaleza de contraseña _PasswordStrengthIndicator(password: _passwordController.text), const SizedBox(height: 8), ], const SizedBox(height: 24), // ── BOTÓN PRINCIPAL ─────────────────────────────── SizedBox( height: 56, child: ElevatedButton( onPressed: _logueando ? null : (_esRegistro ? _registrarse : _iniciarSesion), style: ElevatedButton.styleFrom( backgroundColor: colorScheme.primary, foregroundColor: colorScheme.onPrimary, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), child: _logueando ? const SizedBox( height: 24, width: 24, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), ) : Text( _esRegistro ? 'Registrarse' : 'Iniciar sesión', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), ), ), const SizedBox(height: 12), // ── TOGGLE LOGIN/REGISTRO ───────────────────────── TextButton( onPressed: _logueando ? null : () => setState(() { _esRegistro = !_esRegistro; _nameController.clear(); _direccionController.clear(); _coloniaSeleccionada = null; _passwordController.clear(); }), child: Text( _esRegistro ? '¿Ya tienes cuenta? Inicia sesión' : '¿No tienes cuenta? Regístrate', style: TextStyle(color: colorScheme.primary, fontWeight: FontWeight.w600), ), ), const SizedBox(height: 16), Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: colorScheme.primaryContainer.withValues(alpha: 0.3), borderRadius: BorderRadius.circular(8), ), child: Text( _esRegistro ? 'Tu contraseña se almacena de forma segura (bcrypt). Mínimo 6 caracteres.' : 'Inicia sesión con tu correo para ver el estado del camión y recibir notificaciones.', style: TextStyle(fontSize: 12, color: colorScheme.primary), textAlign: TextAlign.center, ), ), ], ), ), ), ); } } // ================================================================ // WIDGET: Indicador de fortaleza de contraseña // Muestra una barra de color que se llena según la complejidad. // Solo se muestra en el modo registro. // ================================================================ class _PasswordStrengthIndicator extends StatelessWidget { final String password; const _PasswordStrengthIndicator({required this.password}); // Retorna (nivel 0-3, etiqueta, color) (int, String, Color) _evaluar() { if (password.isEmpty) return (0, '', Colors.grey); int puntos = 0; if (password.length >= 8) puntos++; if (password.contains(RegExp(r'[A-Z]'))) puntos++; if (password.contains(RegExp(r'[0-9]'))) puntos++; if (password.contains(RegExp(r'[!@#\$%^&*]'))) puntos++; if (puntos <= 1) return (1, 'Débil', Colors.red); if (puntos == 2) return (2, 'Regular', Colors.orange); if (puntos == 3) return (3, 'Buena', Colors.lightGreen); return (4, 'Excelente', Colors.green); } @override Widget build(BuildContext context) { if (password.isEmpty) return const SizedBox.shrink(); final (nivel, etiqueta, color) = _evaluar(); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: List.generate(4, (i) { return Expanded( child: Container( margin: const EdgeInsets.only(right: 4), height: 4, decoration: BoxDecoration( color: i < nivel ? color : Colors.grey.shade300, borderRadius: BorderRadius.circular(2), ), ), ); }), ), const SizedBox(height: 4), Text('Contraseña: $etiqueta', style: TextStyle(fontSize: 12, color: color)), ], ); } }