import 'package:sysfarma_mobile/src/core/models/tenant_session.dart'; import 'package:sysfarma_mobile/src/core/notifications/push_notification_service.dart'; import 'package:sysfarma_mobile/src/core/network/api_client.dart'; import 'package:sysfarma_mobile/src/core/storage/tenant_session_store.dart'; import 'package:sysfarma_mobile/src/core/theme/app_tokens.dart'; import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; class LoginScreen extends ConsumerStatefulWidget { const LoginScreen({super.key}); @override ConsumerState createState() => _LoginScreenState(); } class _LoginScreenState extends ConsumerState { final _formKey = GlobalKey(); final _rucController = TextEditingController(); final _emailController = TextEditingController(); final _passwordController = TextEditingController(); bool _isSubmitting = false; bool _obscurePassword = true; bool _showRucStep = true; String? _resolvedBaseUrl; String? _resolvedCompanyName; @override void initState() { super.initState(); _hydrateStoredSession(); WidgetsBinding.instance.addPostFrameCallback((_) => _debugAutoLogin()); } @override void dispose() { _rucController.dispose(); _emailController.dispose(); _passwordController.dispose(); super.dispose(); } Future _hydrateStoredSession() async { final store = ref.read(tenantSessionStoreProvider); final tenant = await store.readTenantConfig(); if (!mounted) return; if (tenant.ruc != null && tenant.baseUrl != null) { _rucController.text = tenant.ruc!; _resolvedBaseUrl = tenant.baseUrl; _resolvedCompanyName = tenant.companyName; _emailController.clear(); _passwordController.clear(); setState(() { _showRucStep = false; }); } } Future _debugAutoLogin() async { const autoLogin = bool.fromEnvironment('MOBILE_WEB_AUTO_LOGIN', defaultValue: false); const testMode = bool.fromEnvironment('MOBILE_WEB_TEST_MODE', defaultValue: false); if (!kIsWeb || !autoLogin) return; if (_isSubmitting) return; const baseUrl = String.fromEnvironment('MOBILE_WEB_BASE_URL', defaultValue: ''); const email = String.fromEnvironment('MOBILE_WEB_EMAIL', defaultValue: ''); const password = String.fromEnvironment('MOBILE_WEB_PASSWORD', defaultValue: ''); const ruc = String.fromEnvironment('MOBILE_WEB_RUC', defaultValue: ''); const companyName = String.fromEnvironment('MOBILE_WEB_COMPANY', defaultValue: ''); if (baseUrl.isEmpty || email.isEmpty || password.isEmpty) return; if (testMode && !mounted) return; _resolvedBaseUrl = baseUrl; _resolvedCompanyName = companyName.isNotEmpty ? companyName : _resolvedCompanyName; if (ruc.isNotEmpty) { _rucController.text = ruc; } setState(() { _showRucStep = false; _isSubmitting = true; }); final apiClient = ref.read(apiClientProvider); final sessionStore = ref.read(tenantSessionStoreProvider); final login = await apiClient.login( baseUrl: baseUrl, email: email, password: password, ); if (!mounted) return; if (!login.success) { _showMessage(login.message ?? 'No se pudo iniciar sesion.', isError: true); setState(() => _isSubmitting = false); return; } final data = login.data ?? const {}; final company = Map.from(data['company'] as Map? ?? const {}); final token = data['token']?.toString(); if (token == null || token.isEmpty) { _showMessage('El API no devolvio un token valido.', isError: true); setState(() => _isSubmitting = false); return; } final resolvedCompanyName = _resolvedCompanyName ?? company['trade_name']?.toString() ?? company['name']?.toString(); final userMap = Map.from(data['user'] as Map? ?? const {}); final userType = userMap['type']?.toString(); final activeEstablishmentId = int.tryParse( '${data['active_establishment_id'] ?? userMap['active_establishment_id'] ?? userMap['establishment_id'] ?? ''}', ); await sessionStore.saveSession( TenantSession( baseUrl: ApiClient.normalizeBaseUrl(baseUrl), token: token, companyName: resolvedCompanyName, ruc: ruc.isNotEmpty ? ruc : null, userType: userType, activeEstablishmentId: activeEstablishmentId, ), ); ref.invalidate(currentSessionProvider); if (!mounted) return; setState(() => _isSubmitting = false); context.go('/home'); } Future _resolveRuc() async { final ruc = _rucController.text.trim(); if (ruc.isEmpty) return; if (ruc.length != 11) { _showMessage('El RUC debe tener 11 digitos.', isError: true); return; } setState(() => _isSubmitting = true); final api = ref.read(apiClientProvider); // Try resolving on last known URL, or fall back to discovery URL String? discoveryUrl = _resolvedBaseUrl; if (discoveryUrl == null || discoveryUrl.isEmpty) { final session = await ref.read(tenantSessionStoreProvider).readSession(); discoveryUrl = session?.baseUrl; } discoveryUrl ??= ApiClient.defaultDiscoveryUrl; if (discoveryUrl.contains('enterfarmaplus.test')) { discoveryUrl = 'https://demo.sysfarma.pe'; } final response = await api.resolveRuc( ruc: ruc, baseUrl: discoveryUrl, ); if (!mounted) return; if (!response.success) { _showMessage(response.message ?? 'No se encontro la empresa.', isError: true); setState(() => _isSubmitting = false); return; } final data = response.data ?? {}; _resolvedBaseUrl = data['base_url']?.toString(); _resolvedCompanyName = data['company_name']?.toString(); if (_resolvedBaseUrl == null || _resolvedBaseUrl!.isEmpty) { _showMessage('No se obtuvo la URL de la empresa.', isError: true); setState(() => _isSubmitting = false); return; } // Validate with bootstrap final bootstrap = await api.bootstrap(baseUrl: _resolvedBaseUrl); if (!mounted) return; if (!bootstrap.success) { _showMessage(bootstrap.message ?? 'No se pudo conectar con la empresa.', isError: true); setState(() => _isSubmitting = false); return; } _emailController.clear(); _passwordController.clear(); setState(() { _isSubmitting = false; _showRucStep = false; }); } Future _submit() async { final isValid = _formKey.currentState?.validate() ?? false; if (!isValid || _isSubmitting) return; if (_resolvedBaseUrl == null || _resolvedBaseUrl!.isEmpty) { _showMessage('Primero debes conectar tu empresa con el RUC.', isError: true); return; } setState(() => _isSubmitting = true); final apiClient = ref.read(apiClientProvider); final sessionStore = ref.read(tenantSessionStoreProvider); final login = await apiClient.login( baseUrl: _resolvedBaseUrl!, email: _emailController.text.trim(), password: _passwordController.text, ); if (!mounted) return; if (!login.success) { _showMessage(login.message ?? 'No se pudo iniciar sesion.', isError: true); setState(() => _isSubmitting = false); return; } final data = login.data ?? const {}; final company = Map.from(data['company'] as Map? ?? const {}); final token = data['token']?.toString(); if (token == null || token.isEmpty) { _showMessage('El API no devolvio un token valido.', isError: true); setState(() => _isSubmitting = false); return; } final companyName = _resolvedCompanyName ?? company['trade_name']?.toString() ?? company['name']?.toString(); final userMap = Map.from(data['user'] as Map? ?? const {}); final userType = userMap['type']?.toString(); final activeEstablishmentId = int.tryParse( '${data['active_establishment_id'] ?? userMap['active_establishment_id'] ?? userMap['establishment_id'] ?? ''}', ); await sessionStore.saveSession( TenantSession( baseUrl: ApiClient.normalizeBaseUrl(_resolvedBaseUrl!), token: token, companyName: companyName, ruc: _rucController.text.trim(), userType: userType, activeEstablishmentId: activeEstablishmentId, ), ); await ref.read(pushNotificationServiceProvider).registerCurrentToken(); ref.invalidate(currentSessionProvider); if (!mounted) return; setState(() => _isSubmitting = false); context.go('/home'); } void _showMessage(String message, {bool isError = false}) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(message), backgroundColor: isError ? const Color(0xFFDC2626) : null, ), ); } void _showTenantGuide() { showModalBottomSheet( context: context, builder: (context) { return SafeArea( child: SingleChildScrollView( padding: const EdgeInsets.fromLTRB(20, 8, 20, 24), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Como conectar tu empresa', style: Theme.of(context).textTheme.headlineSmall), const SizedBox(height: 12), Text( 'Ingresa el RUC (numero de 11 digitos) de tu empresa. La app se conectara automaticamente con tu cuenta.', style: Theme.of(context) .textTheme .bodyMedium ?.copyWith(color: AppTokens.secondary), ), const SizedBox(height: 18), const _LoginHintRow( icon: Icons.business_rounded, title: 'RUC de empresa', subtitle: 'Ejemplo: 20100130204 (11 digitos)', ), const SizedBox(height: 12), const _LoginHintRow( icon: Icons.alternate_email_rounded, title: 'Usuario actual', subtitle: 'Se reutiliza el mismo correo del panel web.', ), const SizedBox(height: 12), const _LoginHintRow( icon: Icons.verified_user_rounded, title: 'Sesion segura', subtitle: 'El token se guarda en almacenamiento seguro del dispositivo.', ), ], ), ), ); }, ); } @override Widget build(BuildContext context) { return Scaffold( body: Stack( children: [ // Fondo con gradiente de marca const Positioned.fill( child: DecoratedBox( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ Color(0xFFF3FFE8), Color(0xFFF5F6FF), Color(0xFFEEF0FF), ], ), ), ), ), // Círculos decorativos Positioned( top: -60, left: -40, child: Container( width: 220, height: 220, decoration: BoxDecoration( color: const Color(0xFF7BC043).withValues(alpha: 0.18), shape: BoxShape.circle, ), ), ), Positioned( bottom: -80, right: -60, child: Container( width: 280, height: 280, decoration: BoxDecoration( color: const Color(0xFF5F6CFB).withValues(alpha: 0.16), shape: BoxShape.circle, ), ), ), // Contenido SafeArea( child: Center( child: SingleChildScrollView( padding: const EdgeInsets.all(24), child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 460), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const _LoginBrandHeader(), const SizedBox(height: 28), _showRucStep ? _buildRucCard() : _buildCredentialsCard(), const SizedBox(height: 18), Center( child: Text( 'v0.1.0 · SysFarma © 2026', style: Theme.of(context) .textTheme .labelSmall ?.copyWith(color: AppTokens.secondary), ), ), ], ), ), ), ), ), ], ), ); } Widget _buildRucCard() { const testMode = bool.fromEnvironment('MOBILE_WEB_TEST_MODE', defaultValue: false); return Card( child: Padding( padding: const EdgeInsets.all(24), child: Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ if (kIsWeb && testMode) ...[ Align( alignment: Alignment.centerRight, child: TextButton.icon( onPressed: _isSubmitting ? null : _debugAutoLogin, icon: const Icon(Icons.play_circle_fill_rounded), label: const Text('Entrar demo'), ), ), const SizedBox(height: 8), ], Text( 'Conectar empresa', style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 8), Text( 'Ingresa el RUC de tu negocio para conectar.', style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: AppTokens.secondary, ), ), const SizedBox(height: 6), Align( alignment: Alignment.centerLeft, child: TextButton.icon( onPressed: _showTenantGuide, icon: const Icon(Icons.help_outline_rounded), label: const Text('Ayuda'), ), ), const SizedBox(height: 20), TextFormField( controller: _rucController, keyboardType: TextInputType.number, maxLength: 11, key: const ValueKey('ruc_field'), inputFormatters: [FilteringTextInputFormatter.digitsOnly], decoration: const InputDecoration( labelText: 'RUC', hintText: '20100130204', prefixIcon: Icon(Icons.business_rounded), counterText: '', ), validator: (value) { if (value == null || value.trim().isEmpty) { return 'Ingresa el RUC de la empresa.'; } if (value.trim().length != 11) { return 'El RUC debe tener 11 digitos.'; } return null; }, onFieldSubmitted: (_) => _resolveRuc(), ), const SizedBox(height: 22), FilledButton.icon( onPressed: _isSubmitting ? null : _resolveRuc, icon: _isSubmitting ? const SizedBox( width: 18, height: 18, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ) : const Icon(Icons.search_rounded), label: Text(_isSubmitting ? 'Buscando...' : 'Buscar empresa'), ), ], ), ), ), ); } Widget _buildCredentialsCard() { return Card( child: Padding( padding: const EdgeInsets.all(24), child: Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row( children: [ IconButton( onPressed: () => setState(() => _showRucStep = true), icon: const Icon(Icons.arrow_back_rounded), tooltip: 'Cambiar empresa', visualDensity: VisualDensity.compact, padding: EdgeInsets.zero, ), const SizedBox(width: 4), Text( 'Iniciar sesion', style: Theme.of(context).textTheme.titleLarge, ), ], ), const SizedBox(height: 12), Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: AppTokens.primary.withValues(alpha: 0.08), borderRadius: BorderRadius.circular(8), ), child: Row( children: [ Icon( Icons.check_circle_rounded, size: 20, color: Colors.green.shade600, ), const SizedBox(width: 8), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (_resolvedCompanyName != null && _resolvedCompanyName!.isNotEmpty) Text( _resolvedCompanyName!, style: Theme.of(context) .textTheme .titleSmall ?.copyWith( fontWeight: FontWeight.w600, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), Text( 'RUC: ${_rucController.text.trim()}', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: AppTokens.secondary, ), ), ], ), ), ], ), ), const SizedBox(height: 12), TextFormField( controller: _emailController, keyboardType: TextInputType.emailAddress, decoration: const InputDecoration( labelText: 'Correo', prefixIcon: Icon(Icons.alternate_email_rounded), ), validator: (value) { if (value == null || value.trim().isEmpty) { return 'Ingresa tu correo.'; } return null; }, ), const SizedBox(height: 14), TextFormField( key: const ValueKey('password_field'), controller: _passwordController, obscureText: _obscurePassword, decoration: InputDecoration( labelText: 'Contrasena', prefixIcon: const Icon(Icons.lock_rounded), suffixIcon: IconButton( onPressed: () => setState(() => _obscurePassword = !_obscurePassword), icon: Icon( _obscurePassword ? Icons.visibility_off_rounded : Icons.visibility_rounded, ), ), ), validator: (value) { if (value == null || value.isEmpty) { return 'Ingresa tu contrasena.'; } return null; }, onFieldSubmitted: (_) => _submit(), ), const SizedBox(height: 22), FilledButton.icon( onPressed: _isSubmitting ? null : _submit, icon: _isSubmitting ? const SizedBox( width: 18, height: 18, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ) : const Icon(Icons.arrow_forward_rounded), label: Text(_isSubmitting ? 'Conectando...' : 'Entrar'), ), ], ), ), ), ); } } class _LoginHintRow extends StatelessWidget { const _LoginHintRow({ required this.icon, required this.title, required this.subtitle, }); final IconData icon; final String title; final String subtitle; @override Widget build(BuildContext context) { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ CircleAvatar( backgroundColor: AppTokens.primary.withValues(alpha: 0.12), foregroundColor: AppTokens.primary, child: Icon(icon, size: 18), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: Theme.of(context).textTheme.titleSmall), const SizedBox(height: 4), Text( subtitle, style: Theme.of(context) .textTheme .bodyMedium ?.copyWith(color: AppTokens.secondary), ), ], ), ), ], ); } } class _LoginBrandHeader extends StatelessWidget { const _LoginBrandHeader(); @override Widget build(BuildContext context) { return Column( children: [ Container( width: 108, height: 108, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(28), boxShadow: [ BoxShadow( color: const Color(0xFF5F6CFB).withValues(alpha: 0.22), blurRadius: 32, offset: const Offset(0, 14), ), ], ), padding: const EdgeInsets.all(18), child: Image.asset( 'assets/icon/app_icon.png', fit: BoxFit.contain, ), ), const SizedBox(height: 18), ShaderMask( shaderCallback: (rect) => const LinearGradient( colors: [Color(0xFF0D47A1), Color(0xFF009688)], ).createShader(rect), child: Text( 'SysFarma', style: Theme.of(context).textTheme.displaySmall?.copyWith( color: Colors.white, fontWeight: FontWeight.w900, letterSpacing: -0.5, ), ), ), const SizedBox(height: 6), Text( 'Gestión comercial en la palma de tu mano', textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: AppTokens.secondary, fontWeight: FontWeight.w500, ), ), ], ); } }