import 'dart:convert'; import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../../core/api/api_client.dart'; import '../../../core/api/api_endpoints.dart'; import '../../../core/auth/auth_provider.dart'; import '../../../core/router/app_router.dart'; import '../../../core/theme/app_colors.dart'; import '../../booking/data/booking_repository.dart'; import '../../wallet/data/wallet_repository.dart'; import '../../wash/data/wash_repository.dart'; import 'auth_widgets.dart'; /// Écran de connexion utilisateur. class LoginScreen extends ConsumerStatefulWidget { const LoginScreen({super.key}); @override ConsumerState createState() => _LoginScreenState(); } class _LoginScreenState extends ConsumerState { final _formKey = GlobalKey(); final _emailController = TextEditingController(text: 'marie.dupont@demo.local'); final _passwordController = TextEditingController(text: 'password'); bool _isCheckingHealth = false; String? _healthResult; bool _healthHasError = false; @override void dispose() { _emailController.dispose(); _passwordController.dispose(); super.dispose(); } Future _checkHealth() async { setState(() { _isCheckingHealth = true; _healthResult = null; _healthHasError = false; }); final apiClient = ref.read(apiClientProvider); final lines = [ 'URL de base : ${apiClient.baseUrl}', '', ]; var hasError = false; for (final entry in [ ('API', ApiEndpoints.health), ('Base de données', ApiEndpoints.healthDb), ]) { final label = entry.$1; final path = entry.$2; final url = '${apiClient.baseUrl}$path'; try { final response = await apiClient.get(path); lines.add('$label : OK (${response.statusCode})'); lines.add('URL : $url'); lines.add(_formatResponse(response.data)); } on DioException catch (error) { hasError = true; lines.add('$label : Erreur'); lines.add('URL : $url'); lines.add(_formatDioError(error)); } lines.add(''); } if (mounted) { setState(() { _isCheckingHealth = false; _healthHasError = hasError; _healthResult = lines.join('\n').trim(); }); } } String _formatResponse(dynamic data) { if (data is Map || data is List) { return const JsonEncoder.withIndent(' ').convert(data); } return data?.toString() ?? ''; } String _formatDioError(DioException error) { final response = error.response; if (response != null) { final body = response.data; if (body is Map || body is List) { return 'HTTP ${response.statusCode}\n${_formatResponse(body)}'; } return 'HTTP ${response.statusCode}: $body'; } return switch (error.type) { DioExceptionType.connectionTimeout || DioExceptionType.sendTimeout || DioExceptionType.receiveTimeout => 'Délai d\'attente dépassé', DioExceptionType.connectionError => 'Connexion impossible (${error.message ?? 'réseau injoignable'})', _ => error.message ?? 'Erreur réseau', }; } Future _submit() async { if (!_formKey.currentState!.validate()) return; final success = await ref.read(authProvider.notifier).login( _emailController.text.trim(), _passwordController.text, ); if (success && mounted) { ref.invalidate(washesProvider); ref.invalidate(bookingsProvider); ref.invalidate(walletProvider); ref.invalidate(walletTransactionsProvider); context.go(AppRoutes.home); } } @override Widget build(BuildContext context) { final authState = ref.watch(authProvider); return AuthScaffold( child: AuthFormCard( child: Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const AuthHeader(subtitle: 'Connectez-vous pour réserver et laver'), TextFormField( controller: _emailController, keyboardType: TextInputType.emailAddress, decoration: const InputDecoration( labelText: 'Email', prefixIcon: Icon(Icons.email_outlined), ), validator: (value) { if (value == null || value.isEmpty) { return 'Email requis'; } return null; }, ), const SizedBox(height: 16), TextFormField( controller: _passwordController, obscureText: true, decoration: const InputDecoration( labelText: 'Mot de passe', prefixIcon: Icon(Icons.lock_outline), ), validator: (value) { if (value == null || value.isEmpty) { return 'Mot de passe requis'; } return null; }, ), if (authState.error != null) ...[ const SizedBox(height: 12), Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: AppColors.error.withValues(alpha: 0.08), borderRadius: BorderRadius.circular(12), border: Border.all(color: AppColors.error.withValues(alpha: 0.3)), ), child: Row( children: [ const Icon(Icons.error_outline_rounded, color: AppColors.error, size: 20), const SizedBox(width: 8), Expanded( child: Text( authState.error!, style: const TextStyle(color: AppColors.error), ), ), ], ), ), ], const SizedBox(height: 24), ElevatedButton( onPressed: authState.isLoading ? null : _submit, child: authState.isLoading ? const SizedBox( height: 20, width: 20, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), ) : const Text('Se connecter'), ), const SizedBox(height: 12), TextButton( onPressed: () => context.push(AppRoutes.register), child: const Text('Créer un compte'), ), const SizedBox(height: 8), OutlinedButton.icon( onPressed: _isCheckingHealth ? null : _checkHealth, icon: _isCheckingHealth ? const SizedBox( height: 16, width: 16, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.monitor_heart_outlined, size: 18), label: const Text('Vérifier l\'API'), ), if (_healthResult != null) ...[ const SizedBox(height: 12), Container( width: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: (_healthHasError ? AppColors.error : AppColors.primary) .withValues(alpha: 0.08), borderRadius: BorderRadius.circular(8), border: Border.all( color: (_healthHasError ? AppColors.error : AppColors.primary) .withValues(alpha: 0.3), ), ), child: SelectableText( _healthResult!, style: Theme.of(context).textTheme.bodySmall?.copyWith( fontFamily: 'monospace', color: _healthHasError ? AppColors.error : AppColors.primaryDark, ), ), ), ], ], ), ), ), ); } }