diff --git a/lib/main.dart b/lib/main.dart index c955401..37cae03 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:http/http.dart' as http; void main() { WidgetsFlutterBinding.ensureInitialized(); @@ -681,6 +682,102 @@ class AppCard extends StatelessWidget { } } + +// ======================================================= +// API SERVICE - CONEXIÓN CON BACKEND FASTAPI +// ======================================================= + +class ApiSession { + static String? token; + static Map? user; + + static String get role => (user?['role'] ?? '').toString().toLowerCase(); + static String get name => (user?['name'] ?? 'Usuario').toString(); + static String get email => (user?['email'] ?? '').toString(); + + static void save({required String accessToken, required Map userData}) { + token = accessToken; + user = userData; + } + + static void clear() { + token = null; + user = null; + } +} + +class ApiService { + // Chrome en la misma computadora donde corre FastAPI. + static const String apiBase = 'http://127.0.0.1:8000'; + + static Future> login({ + required String email, + required String password, + }) async { + final uri = Uri.parse('$apiBase/auth/login'); + + final response = await http.post( + uri, + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({ + 'email': email.trim().toLowerCase(), + 'password': password.trim(), + }), + ).timeout(const Duration(seconds: 8)); + + if (response.statusCode < 200 || response.statusCode >= 300) { + String message = 'No se pudo iniciar sesión.'; + try { + final data = jsonDecode(response.body); + message = data['detail']?.toString() ?? message; + } catch (_) {} + throw Exception(message); + } + + final data = jsonDecode(response.body) as Map; + final token = data['access_token']?.toString(); + final user = Map.from(data['user'] ?? {}); + + if (token == null || token.isEmpty || user.isEmpty) { + throw Exception('Respuesta inválida del servidor.'); + } + + ApiSession.save(accessToken: token, userData: user); + return data; + } + + static Map authHeaders() { + return { + 'Content-Type': 'application/json', + if (ApiSession.token != null) 'Authorization': 'Bearer ${ApiSession.token}', + }; + } + + static Future health() async { + try { + final response = await http + .get(Uri.parse('$apiBase/public/health')) + .timeout(const Duration(seconds: 4)); + return response.statusCode == 200; + } catch (_) { + return false; + } + } + + static Future> adminDashboard() async { + final response = await http.get( + Uri.parse('$apiBase/admin/dashboard'), + headers: authHeaders(), + ); + + if (response.statusCode != 200) { + throw Exception('No se pudo cargar dashboard admin'); + } + + return jsonDecode(response.body) as Map; + } +} + // ======================================================= // LOGIN // ======================================================= @@ -695,8 +792,14 @@ class LoginPage extends StatefulWidget { class _LoginPageState extends State { final email = TextEditingController(); final pass = TextEditingController(); + bool loading = false; - void entrar() { + bool correoValido(String value) { + final regex = RegExp(r'^[^@\s]+@[^@\s]+\.[^@\s]+$'); + return regex.hasMatch(value.trim()); + } + + Future entrar() async { final correo = email.text.trim().toLowerCase(); final password = pass.text.trim(); @@ -707,26 +810,53 @@ class _LoginPageState extends State { return; } - if (correo == 'operador@demo.com' && password == '123456') { - Navigator.pushReplacement( - context, - MaterialPageRoute(builder: (_) => const OperadorPage()), + if (!correoValido(correo)) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Ingresa un correo válido. Ejemplo: demo@correo.com')), ); return; } - if (correo == 'admin@demo.com' && password == '123456') { + setState(() => loading = true); + + try { + await ApiService.login(email: correo, password: password); + + if (!mounted) return; + + final role = ApiSession.role; + + if (role == 'operador') { + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (_) => const OperadorPage()), + ); + return; + } + + if (role == 'admin') { + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (_) => const AdminPage()), + ); + return; + } + Navigator.pushReplacement( context, - MaterialPageRoute(builder: (_) => const AdminPage()), + MaterialPageRoute(builder: (_) => const HomePage()), ); - return; + } catch (e) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error de login: ${e.toString().replaceFirst('Exception: ', '')}'), + duration: const Duration(seconds: 5), + ), + ); + } finally { + if (mounted) setState(() => loading = false); } - - Navigator.pushReplacement( - context, - MaterialPageRoute(builder: (_) => const HomePage()), - ); } @override @@ -770,6 +900,29 @@ class _LoginPageState extends State { textAlign: TextAlign.center, style: TextStyle(fontSize: 17, height: 1.35), ), + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(14), + border: Border.all(color: AppColors.green.withOpacity(0.25)), + ), + child: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.api, color: AppColors.green, size: 20), + SizedBox(width: 8), + Flexible( + child: Text( + 'Login conectado al backend FastAPI', + textAlign: TextAlign.center, + style: TextStyle(fontWeight: FontWeight.w800), + ), + ), + ], + ), + ), const SizedBox(height: 24), TextField( controller: email, @@ -792,9 +945,18 @@ class _LoginPageState extends State { height: 56, width: double.infinity, child: FilledButton.icon( - onPressed: entrar, - icon: const Icon(Icons.login), - label: const Text('Iniciar sesión', style: TextStyle(fontSize: 18)), + onPressed: loading ? null : () => entrar(), + icon: loading + ? const SizedBox( + width: 22, + height: 22, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.login), + label: Text( + loading ? 'Conectando...' : 'Iniciar sesión', + style: const TextStyle(fontSize: 18), + ), ), ), TextButton( @@ -902,6 +1064,7 @@ class _HomePageState extends State { IconButton( tooltip: 'Cerrar sesión', onPressed: () { + ApiSession.clear(); Navigator.pushReplacement( context, MaterialPageRoute(builder: (_) => const LoginPage()), @@ -1528,6 +1691,7 @@ class _DatosPageState extends State with SingleTickerProviderStateMix IconButton( tooltip: 'Cerrar sesión', onPressed: () { + ApiSession.clear(); Navigator.pushReplacement( context, MaterialPageRoute(builder: (_) => const LoginPage()), @@ -2579,6 +2743,7 @@ class _OperadorPageState extends State { tooltip: 'Cerrar sesión', onPressed: () { timer?.cancel(); + ApiSession.clear(); Navigator.pushReplacement( context, MaterialPageRoute(builder: (_) => const LoginPage()), diff --git a/pubspec.lock b/pubspec.lock index 4d35ed9..63d9e90 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -169,7 +169,7 @@ packages: source: hosted version: "2.0.0" http: - dependency: transitive + dependency: "direct main" description: name: http sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" diff --git a/pubspec.yaml b/pubspec.yaml index e4f518e..154f27a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -37,6 +37,7 @@ dependencies: cupertino_icons: ^1.0.8 flutter_map: ^8.3.0 latlong2: ^0.9.1 + http: ^1.6.0 dev_dependencies: flutter_test: