461 lines
16 KiB
Dart
461 lines
16 KiB
Dart
// ================================================================
|
|
// 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<LoginScreen> createState() => _LoginScreenState();
|
|
}
|
|
|
|
class _LoginScreenState extends State<LoginScreen> {
|
|
// ----------------------------------------------------------------
|
|
// 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<String> _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<void> _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<void> _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<void> _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<void> _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<String>(
|
|
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,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|