Files
hackathon-sfc-a9a4cee231094…/lib/features/auth/presentation/screens/login_screen.dart
2026-05-23 08:22:39 -06:00

318 lines
12 KiB
Dart

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<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen>
with SingleTickerProviderStateMixin {
final _formKey = GlobalKey<FormState>();
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<double> _fadeAnim;
late final Animation<Offset> _slideAnim;
@override
void initState() {
super.initState();
_animController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 700),
);
_fadeAnim = CurvedAnimation(parent: _animController, curve: Curves.easeOut);
_slideAnim = Tween<Offset>(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<AuthBloc, AuthState>(
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<AuthBloc, AuthState>(
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