import 'home_screen_placeholder.dart'; // 馃搷 Ajusta las carpetas '../' seg煤n la ubicaci贸n exacta en tu proyecto import '../../../../core/network/mysql_service.dart'; // 馃搷 Ajusta las carpetas '../' seg煤n tu proyecto import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import '../../../../core/router/app_router.dart'; import '../../../../core/theme/app_theme.dart'; import '../bloc/auth_bloc.dart'; import '../bloc/auth_state.dart'; import '../widgets/privacy_notice_card.dart'; class LoginScreen extends StatefulWidget { const LoginScreen({super.key}); @override State createState() => _LoginScreenState(); } class _LoginScreenState extends State with SingleTickerProviderStateMixin { final _formKey = GlobalKey(); final _identifierController = TextEditingController(); // Controlador para el correo final _passwordController = TextEditingController(); // Controlador para la contrase帽a bool _obscurePassword = true; late final AnimationController _animController; late final Animation _fadeAnim; late final Animation _slideAnim; @override void initState() { super.initState(); _animController = AnimationController( vsync: this, duration: const Duration(milliseconds: 700), ); _fadeAnim = CurvedAnimation(parent: _animController, curve: Curves.easeOut); _slideAnim = Tween(begin: const Offset(0, 0.06), end: Offset.zero) .animate(CurvedAnimation( parent: _animController, curve: Curves.easeOutCubic)); _animController.forward(); } @override void dispose() { _animController.dispose(); _identifierController.dispose(); _passwordController.dispose(); super.dispose(); } // 馃搷 CONSULTA REAL SELECT A TU TABLA DE MYSQL void _submit(BuildContext context) async { if (_formKey.currentState?.validate() ?? false) { try { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Validando credenciales en MySQL Celaya...')), ); // 1. Obtener la conexi贸n por el cable USB Mapped // 1. Obtener la conexi贸n final conn = await MySqlService().getConnection(); // 2. Modificamos el SELECT para traer tambi茅n la columna 'colonia' final result = await conn.execute( "SELECT email, contrasena_hash, rol, colonia FROM usuarios WHERE email = :email", { "email": _identifierController.text.trim(), }, ); if (!mounted) return; if (result.rows.isNotEmpty) { final usuarioEncontrado = result.rows.first.assoc(); final contrasenaEnBd = usuarioEncontrado['contrasena_hash']; final email = usuarioEncontrado['email']; final rol = usuarioEncontrado['rol']; final colonia = usuarioEncontrado[ 'colonia']; // 馃搷 Extraemos la colonia real de la BD if (contrasenaEnBd?.trim() == _passwordController.text.trim()) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("隆Bienvenido de nuevo, $email ($rol)!")), ); // 馃搷 Enviamos la colonia real extra铆da de MySQL directamente a la URL del Home context.go('/home?colonia=$colonia'); } else { // Contrase帽a mal mapeada en la BD ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Contrase帽a incorrecta para este usuario'), backgroundColor: Colors.orange), ); } } else { // El correo de plano no existe en MySQL Workbench ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('El correo electr贸nico no est谩 registrado'), backgroundColor: Colors.orange), ); } } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( "Error al conectar con MySQL: $e"), // 馃搷 CORREGIDO: Sin contra-barra para que pinte el error real backgroundColor: Colors.red, duration: const Duration(seconds: 5), ), ); } } } @override Widget build(BuildContext context) { return BlocListener( listener: (context, state) { if (state is AuthFailure) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Row( children: [ const Icon(Icons.error_outline, color: Colors.white, size: 18), const SizedBox(width: 10), Expanded(child: Text(state.message)), ], ), backgroundColor: AppTheme.errorRed, ), ); } }, child: Scaffold( backgroundColor: AppTheme.warmWhite, body: SafeArea( child: CustomScrollView( slivers: [ SliverFillRemaining( hasScrollBody: false, child: FadeTransition( opacity: _fadeAnim, child: SlideTransition( position: _slideAnim, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 28), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 48), _buildHeader(), const SizedBox(height: 32), _buildForm(context), const SizedBox(height: 24), const PrivacyNoticeCard(), const SizedBox(height: 32), _buildDemoHint(), const Spacer(), const SizedBox(height: 8), Center( child: Text( 'Sistema Municipal de Recolecci贸n Residencial\nCelaya, Guanajuato', textAlign: TextAlign.center, style: TextStyle( fontSize: 12, color: Colors.grey.shade600, height: 1.3), ), ), const SizedBox(height: 48), ], ), ), ), ), ), ], ), ), ), ); } Widget _buildHeader() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: const [ Text('Bienvenido a WasteNotify', style: TextStyle(fontSize: 26, fontWeight: FontWeight.bold)), SizedBox(height: 8), Text('Inicia sesi贸n para continuar', style: TextStyle(fontSize: 14, color: Colors.grey)), ], ); } Widget _buildForm(BuildContext context) { return Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ TextFormField( controller: _identifierController, keyboardType: TextInputType.emailAddress, decoration: const InputDecoration( labelText: 'Correo Electr贸nico o Tel茅fono', prefixIcon: Icon(Icons.email_outlined), border: OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(12))), ), validator: (value) => (value == null || value.trim().isEmpty) ? 'Por favor, ingresa tus datos de acceso' : null, ), const SizedBox(height: 20), TextFormField( controller: _passwordController, obscureText: _obscurePassword, decoration: InputDecoration( labelText: 'Contrase帽a de Acceso', prefixIcon: const Icon(Icons.lock_outline_rounded), border: const OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(12))), suffixIcon: IconButton( icon: Icon( _obscurePassword ? Icons.visibility : Icons.visibility_off), onPressed: () => setState(() => _obscurePassword = !_obscurePassword), ), ), validator: (value) => (value == null || value.isEmpty) ? 'Por favor, introduce tu contrase帽a' : null, ), const SizedBox(height: 32), BlocBuilder( builder: (context, state) { final isLoading = state is AuthLoading; return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ AnimatedContainer( duration: const Duration(milliseconds: 200), decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), boxShadow: isLoading ? [] : [ BoxShadow( color: AppTheme.leafGreen.withValues(alpha: 0.3), blurRadius: 16, offset: const Offset(0, 6), ), ], ), child: ElevatedButton( onPressed: isLoading ? null : () => _submit(context), style: ElevatedButton.styleFrom( backgroundColor: isLoading ? AppTheme.mintGreen.withValues(alpha: 0.7) : AppTheme.leafGreen, disabledBackgroundColor: AppTheme.mintGreen.withValues(alpha: 0.6), padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12)), ), child: isLoading ? const SizedBox( height: 20, width: 20, child: CircularProgressIndicator( color: Colors.white, strokeWidth: 2), ) : const Text('Iniciar Sesi贸n', style: TextStyle(color: Colors.white)), ), ), const SizedBox(height: 16), // 馃搷 BOT脫N DE REGISTRO CORREGIDO (child al final) TextButton( onPressed: () => context.push(AppRoutes.register), style: TextButton.styleFrom( foregroundColor: AppTheme.leafGreen, textStyle: const TextStyle( fontSize: 14, fontWeight: FontWeight.w500), ), child: const Text('驴No tienes cuenta? Reg铆strate aqu铆'), ), ], ); }, ), ], ), ); } // 馃搷 Cierre de la funci贸n _buildForm // Placeholders para evitar errores si no est谩n definidos Widget _buildDemoHint() => const SizedBox.shrink(); } // 馃搷 Cierre de la clase _LoginScreenState