import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../../core/theme/app_theme.dart'; import '../bloc/auth_bloc.dart'; import '../bloc/auth_event.dart'; import '../bloc/auth_state.dart'; import '../widgets/privacy_notice_card.dart'; /// Pantalla de inicio de sesión — MVP WasteNotify. /// /// Diseño: Limpio, institucional, con paleta verde-tierra. /// Incluye mensajería preventiva integrada de forma no intrusiva. /// /// Credenciales de demo: /// Ciudadano: ciudadano@ejemplo.com / password123 /// Operador: operador@ejemplo.com / operador456 /// Teléfono: 5551234567 / pass1234 class LoginScreen extends StatefulWidget { const LoginScreen({super.key}); @override State createState() => _LoginScreenState(); } class _LoginScreenState extends State with SingleTickerProviderStateMixin { final _formKey = GlobalKey(); final _identifierController = TextEditingController(); final _passwordController = TextEditingController(); 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(); } void _submit(BuildContext context) { if (_formKey.currentState?.validate() ?? false) { context.read().add( AuthLoginRequested( identifier: _identifierController.text, password: _passwordController.text, ), ); } } @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), const ScheduleWarningBanner(), const SizedBox(height: 32), _buildForm(context), const SizedBox(height: 24), const PrivacyNoticeCard(), const SizedBox(height: 32), _buildDemoHint(), const SizedBox(height: 24), ], ), ), ), ), ), ], ), ), ), ); } Widget _buildHeader() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Logo / ícono principal Container( width: 60, height: 60, decoration: BoxDecoration( gradient: const LinearGradient( colors: [AppTheme.leafGreen, AppTheme.forestGreen], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: AppTheme.leafGreen.withOpacity(0.35), blurRadius: 12, offset: const Offset(0, 4), ), ], ), child: const Icon( Icons.recycling_rounded, color: Colors.white, size: 34, ), ), const SizedBox(height: 20), RichText( text: const TextSpan( style: TextStyle( fontSize: 28, fontWeight: FontWeight.w800, color: AppTheme.charcoal, height: 1.15, ), children: [ TextSpan(text: 'Waste'), TextSpan( text: 'Notify', style: TextStyle(color: AppTheme.leafGreen), ), ], ), ), const SizedBox(height: 8), Text( 'Notificaciones de recolección\nsin rastreo, sin riesgos.', style: TextStyle( fontSize: 14.5, color: AppTheme.midGray, height: 1.5, ), ), ], ); } Widget _buildForm(BuildContext context) { return Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // --- Campo: Email o Teléfono --- TextFormField( controller: _identifierController, keyboardType: TextInputType.emailAddress, textInputAction: TextInputAction.next, autocorrect: false, decoration: const InputDecoration( labelText: 'Correo electrónico o teléfono', hintText: 'ej. juan@correo.com o 5551234567', prefixIcon: Icon(Icons.person_outline_rounded), ), validator: (value) { if (value == null || value.trim().isEmpty) { return 'Ingresa tu correo o número de teléfono'; } final trimmed = value.trim(); final isEmail = RegExp( r'^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$', ).hasMatch(trimmed); final isPhone = RegExp(r'^\d{7,15}$').hasMatch(trimmed); if (!isEmail && !isPhone) { return 'Ingresa un correo válido o un número de 7-15 dígitos'; } return null; }, ), const SizedBox(height: 16), // --- Campo: Contraseña --- TextFormField( controller: _passwordController, obscureText: _obscurePassword, textInputAction: TextInputAction.done, onFieldSubmitted: (_) => _submit(context), decoration: InputDecoration( labelText: 'Contraseña', hintText: 'Mínimo 6 caracteres', prefixIcon: const Icon(Icons.lock_outline_rounded), suffixIcon: IconButton( icon: Icon( _obscurePassword ? Icons.visibility_off_outlined : Icons.visibility_outlined, color: AppTheme.midGray, ), onPressed: () => setState(() => _obscurePassword = !_obscurePassword), tooltip: _obscurePassword ? 'Mostrar contraseña' : 'Ocultar contraseña', ), ), validator: (value) { if (value == null || value.isEmpty) { return 'Ingresa tu contraseña'; } if (value.length < 6) { return 'La contraseña debe tener al menos 6 caracteres'; } return null; }, ), const SizedBox(height: 8), // --- Olvidé mi contraseña --- Align( alignment: Alignment.centerRight, child: TextButton( onPressed: () { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Recuperación de contraseña — Próximamente'), ), ); }, child: const Text( '¿Olvidaste tu contraseña?', style: TextStyle(fontSize: 13), ), ), ), const SizedBox(height: 16), // --- Botón Principal --- BlocBuilder( builder: (context, state) { final isLoading = state is AuthLoading; return AnimatedContainer( duration: const Duration(milliseconds: 200), decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), boxShadow: isLoading ? [] : [ BoxShadow( color: AppTheme.leafGreen.withOpacity(0.4), blurRadius: 16, offset: const Offset(0, 6), ), ], ), child: ElevatedButton( onPressed: isLoading ? null : () => _submit(context), style: ElevatedButton.styleFrom( backgroundColor: isLoading ? AppTheme.mintGreen.withOpacity(0.7) : AppTheme.leafGreen, disabledBackgroundColor: AppTheme.mintGreen.withOpacity(0.6), ), child: isLoading ? const SizedBox( height: 22, width: 22, child: CircularProgressIndicator( strokeWidth: 2.5, valueColor: AlwaysStoppedAnimation(Colors.white), ), ) : const Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.login_rounded, size: 20), SizedBox(width: 10), Text('Ingresar de forma segura'), ], ), ), ); }, ), ], ), ); } Widget _buildDemoHint() { return Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: AppTheme.sandBeige, borderRadius: BorderRadius.circular(10), border: Border.all(color: const Color(0xFFFFE082), width: 1), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.science_outlined, size: 15, color: AppTheme.earthBrown), const SizedBox(width: 6), Text( 'Demo MVP — Credenciales de prueba', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w700, color: AppTheme.earthBrown, ), ), ], ), const SizedBox(height: 6), _DemoCredential( label: 'Ciudadano', user: 'ciudadano@ejemplo.com', pass: 'password123', ), _DemoCredential( label: 'Operador', user: 'operador@ejemplo.com', pass: 'operador456', ), _DemoCredential( label: 'Teléfono', user: '5551234567', pass: 'pass1234', ), ], ), ); } } class _DemoCredential extends StatelessWidget { final String label; final String user; final String pass; const _DemoCredential({ required this.label, required this.user, required this.pass, }); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(top: 3), child: Text( '$label: $user / $pass', style: const TextStyle( fontSize: 11.5, color: AppTheme.earthBrown, fontFamily: 'monospace', ), ), ); } }