// ================================================================ // lib/screens/login_screen.dart // Pantalla de Login Mockeada — Hackathon MVP // ================================================================ // // PROPÓSITO: // Simular la selección de identidad de usuario para la demo. // En producción aquí iría: Google Sign-In, OTP por SMS, etc. // // FLUJO: // 1. Usuario ingresa un ID numérico (1-4 para los seed data) // 2. Selecciona su colonia en un Dropdown // 3. Presiona "Entrar" -> navega a HomeScreen con el usuario_id // // ATAJO DE HACKATHON: // El "ID de usuario" es manual para evitar un sistema de auth // completo. Para la demo, los IDs 1-4 son los del seed. // ================================================================ 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 { // ---------------------------------------------------------------- // ESTADO LOCAL // ---------------------------------------------------------------- // Controladores para los campos de email/registro final TextEditingController _emailController = TextEditingController(); final TextEditingController _nameController = TextEditingController(); final TextEditingController _direccionController = TextEditingController(); // Colonia seleccionada en el Dropdown (null = no seleccionada aún) String? _coloniaSeleccionada; // Lista de colonias cargadas desde el backend List _colonias = []; // Indica si estamos en modo registro o en modo login bool _esRegistro = false; // Estado de carga: mostramos spinner mientras cargamos colonias bool _cargandoColonias = true; // Estado de error al cargar colonias String? _errorColonias; // Estado del botón de login: evita doble tap bool _logueando = false; // Servicio de API (instancia local, sin inyección para el hackathon) final ApiService _apiService = ApiService(); // ---------------------------------------------------------------- // LIFECYCLE // ---------------------------------------------------------------- @override void initState() { super.initState(); _cargarColonias(); _verificarSesionExistente(); } @override void dispose() { // Siempre liberar controllers para evitar memory leaks _emailController.dispose(); _nameController.dispose(); _direccionController.dispose(); super.dispose(); } // ---------------------------------------------------------------- // VERIFICAR SESIÓN EXISTENTE // // Si el usuario ya se logueó antes (guardado en shared_preferences), // lo mandamos directo al home sin pasar por el login. // ATAJO: Esto simula "recordar sesión". No es auth real. // ---------------------------------------------------------------- Future _verificarSesionExistente() async { final prefs = await SharedPreferences.getInstance(); final usuarioIdGuardado = prefs.getInt('usuario_id'); if (usuarioIdGuardado != null && mounted) { // Ya hay sesión, ir al home directamente Navigator.pushReplacementNamed( context, '/home', arguments: usuarioIdGuardado, ); } } // ---------------------------------------------------------------- // CARGAR COLONIAS DESDE EL BACKEND // // Intenta cargar desde la API. Si falla (backend apagado), // usa una lista de fallback hardcodeada para no bloquear la demo. // ---------------------------------------------------------------- Future _cargarColonias() async { try { final colonias = await _apiService.obtenerColonias(); if (mounted) { setState(() { _colonias = colonias; _cargandoColonias = false; }); } } catch (e) { // FALLBACK: Lista hardcodeada por si el backend no está corriendo // Útil para desarrollar el frontend en paralelo al backend if (mounted) { setState(() { _colonias = [ 'Zona Centro', 'Col. Hidalgo', 'Col. Independencia', 'Col. Obrera', 'Col. San Juan', 'Fracc. Los Pinos', 'Col. Reforma', ]; _cargandoColonias = false; _errorColonias = 'Sin conexión al backend. Usando lista local.'; }); } } } // ---------------------------------------------------------------- // ACCIÓN: INICIAR SESIÓN // Valida el correo, llama al backend y navega. // ---------------------------------------------------------------- Future _iniciarSesion() async { final email = _emailController.text.trim(); if (email.isEmpty) { _mostrarError('Por favor ingresa tu correo.'); return; } setState(() => _logueando = true); try { final usuarioId = await _apiService.loginConCorreo(email); final prefs = await SharedPreferences.getInstance(); await prefs.setInt('usuario_id', usuarioId); await prefs.setString('email', email); if (mounted) { Navigator.pushReplacementNamed( context, '/home', arguments: usuarioId, ); } } catch (e) { _mostrarError('Error iniciando sesión. Revisa tu correo o regístrate.'); } finally { if (mounted) { setState(() => _logueando = false); } } } // ---------------------------------------------------------------- // ACCIÓN: REGISTRARSE // Valida los datos y crea un nuevo usuario en el backend. // ---------------------------------------------------------------- Future _registrarse() async { final nombre = _nameController.text.trim(); final email = _emailController.text.trim(); 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 (_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, direccion, _coloniaSeleccionada!, ); final prefs = await SharedPreferences.getInstance(); await prefs.setInt('usuario_id', usuarioId); await prefs.setString('email', email); await prefs.setString('colonia', _coloniaSeleccionada!); if (mounted) { Navigator.pushReplacementNamed( context, '/home', arguments: usuarioId, ); } } catch (e) { _mostrarError('Error registrando usuario. Intenta con otro correo.'); } finally { if (mounted) { setState(() => _logueando = false); } } } // ---------------------------------------------------------------- // HELPER: Mostrar mensaje de error con SnackBar // ---------------------------------------------------------------- void _mostrarError(String mensaje) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(mensaje), backgroundColor: Colors.red.shade700, behavior: SnackBarBehavior.floating, ), ); } // ================================================================ // UI // ================================================================ @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return Scaffold( backgroundColor: colorScheme.surface, body: SafeArea( child: Center( child: SingleChildScrollView( padding: const EdgeInsets.all(32.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // ------------------------------------------------ // HEADER: Ícono y título // ------------------------------------------------ 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( 'Ingresa tus datos para recibir notificaciones de tu camión', textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Colors.grey.shade600, ), ), const SizedBox(height: 48), // ------------------------------------------------ // FORMULARIO: Correo / Registro // ------------------------------------------------ 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), if (_esRegistro) ...[ TextField( controller: _nameController, 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, colonia', prefixIcon: const Icon(Icons.home_outlined), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), ), ), ), const SizedBox(height: 20), ], if (_esRegistro) if (_cargandoColonias) const Center( child: Padding( padding: EdgeInsets.symmetric(vertical: 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( initialValue: _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((colonia) { return DropdownMenuItem( value: colonia, child: Text(colonia), ); }).toList(), onChanged: (valor) { setState(() => _coloniaSeleccionada = valor); }, ), ], const SizedBox(height: 32), // ------------------------------------------------ // BOTÓN: Entrar / Registrarse // Muestra spinner mientras _logueando == true // ------------------------------------------------ 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), TextButton( onPressed: _logueando ? null : () { setState(() { _esRegistro = !_esRegistro; // Clear fields when switching modes _nameController.clear(); _direccionController.clear(); _coloniaSeleccionada = null; }); }, 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 ? 'Regístrate con tu correo, nombre y dirección para recibir avisos de recolección.' : '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, ), ), ], ), ), ), ), ); } }