// ================================================================ // 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 // ---------------------------------------------------------------- // Controlador para el TextField del ID de usuario final TextEditingController _idController = TextEditingController(); // Colonia seleccionada en el Dropdown (null = no seleccionada aún) String? _coloniaSeleccionada; // Lista de colonias cargadas desde el backend List _colonias = []; // 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 _idController.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, guarda y navega. // ---------------------------------------------------------------- Future _iniciarSesion() async { // Validación básica del ID final idTexto = _idController.text.trim(); if (idTexto.isEmpty) { _mostrarError('Por favor ingresa tu ID de usuario.'); return; } final usuarioId = int.tryParse(idTexto); if (usuarioId == null || usuarioId <= 0) { _mostrarError('El ID debe ser un número positivo (ej: 1, 2, 3, 4).'); return; } if (_coloniaSeleccionada == null) { _mostrarError('Por favor selecciona tu colonia.'); return; } setState(() => _logueando = true); // Guardar la sesión en shared_preferences para no pedir login de nuevo final prefs = await SharedPreferences.getInstance(); await prefs.setInt('usuario_id', usuarioId); await prefs.setString('colonia', _coloniaSeleccionada!); // Navegar a la pantalla principal pasando el usuario_id como argumento if (mounted) { Navigator.pushReplacementNamed( context, '/home', arguments: usuarioId, ); } } // ---------------------------------------------------------------- // 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), // ------------------------------------------------ // CAMPO: ID de Usuario // NOTA PARA EL EQUIPO: Para la demo usa IDs 1 al 4 // (son los que creó el seed del backend) // ------------------------------------------------ TextField( controller: _idController, keyboardType: TextInputType.number, decoration: InputDecoration( labelText: 'ID de Usuario', hintText: 'Ej: 1, 2, 3 ó 4', helperText: 'Usa los IDs del seed del backend (1-4)', prefixIcon: const Icon(Icons.person_outline), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), ), ), ), const SizedBox(height: 20), // ------------------------------------------------ // DROPDOWN: Selección de Colonia // ------------------------------------------------ if (_cargandoColonias) const Center( child: Padding( padding: EdgeInsets.symmetric(vertical: 16), child: CircularProgressIndicator(), ), ) else ...[ // Aviso si se usó el fallback local if (_errorColonias != null) Padding( padding: const EdgeInsets.only(bottom: 8), child: Text( '⚠️ $_errorColonias', style: TextStyle( fontSize: 12, color: Colors.orange.shade700, ), ), ), // El DropdownButtonFormField necesita que los items // vengan de _colonias, que se cargó en initState. DropdownButtonFormField( value: _coloniaSeleccionada, hint: const Text('Selecciona tu colonia'), decoration: InputDecoration( prefixIcon: const Icon(Icons.location_city_outlined), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), ), ), // Convierte cada String de _colonias en un DropdownMenuItem items: _colonias.map((colonia) { return DropdownMenuItem( value: colonia, child: Text(colonia), ); }).toList(), onChanged: (valor) { setState(() => _coloniaSeleccionada = valor); }, ), ], const SizedBox(height: 32), // ------------------------------------------------ // BOTÓN: Entrar // Muestra spinner mientras _logueando == true // ------------------------------------------------ SizedBox( height: 56, child: ElevatedButton( onPressed: _logueando ? null : _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, ), ) : const Text( 'Entrar', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), ), ), const SizedBox(height: 16), // Nota informativa para jueces/demos Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: colorScheme.primaryContainer.withOpacity(0.3), borderRadius: BorderRadius.circular(8), ), child: Text( '🧪 Demo: Usa IDs del 1 al 4. Corre primero POST /api/seed en el backend.', style: TextStyle( fontSize: 12, color: colorScheme.primary, ), textAlign: TextAlign.center, ), ), ], ), ), ), ), ); } }