Integration fonctionnalités V1 ( resas + gestion machines

This commit is contained in:
bastien
2026-07-03 19:02:48 +02:00
parent 20f383dd62
commit bf191d6396
46 changed files with 4691 additions and 654 deletions
+155 -34
View File
@@ -1,9 +1,19 @@
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 {
@@ -17,6 +27,9 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController(text: 'marie.dupont@demo.local');
final _passwordController = TextEditingController(text: 'password');
bool _isCheckingHealth = false;
String? _healthResult;
bool _healthHasError = false;
@override
void dispose() {
@@ -25,6 +38,79 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
super.dispose();
}
Future<void> _checkHealth() async {
setState(() {
_isCheckingHealth = true;
_healthResult = null;
_healthHasError = false;
});
final apiClient = ref.read(apiClientProvider);
final lines = <String>[
'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<void> _submit() async {
if (!_formKey.currentState!.validate()) return;
@@ -34,6 +120,10 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
);
if (success && mounted) {
ref.invalidate(washesProvider);
ref.invalidate(bookingsProvider);
ref.invalidate(walletProvider);
ref.invalidate(walletTransactionsProvider);
context.go(AppRoutes.home);
}
}
@@ -42,32 +132,15 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
Widget build(BuildContext context) {
final authState = ref.watch(authProvider);
return Scaffold(
body: SafeArea(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Form(
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Icon(Icons.local_laundry_service, size: 72, color: Theme.of(context).colorScheme.primary),
const SizedBox(height: 16),
Text(
'Laverie Connectée',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.headlineMedium,
),
const SizedBox(height: 8),
Text(
'Connectez-vous pour réserver et laver',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 32),
TextFormField(
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(
@@ -98,10 +171,25 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
),
if (authState.error != null) ...[
const SizedBox(height: 12),
Text(
authState.error!,
style: TextStyle(color: Theme.of(context).colorScheme.error),
textAlign: TextAlign.center,
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),
@@ -111,7 +199,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(strokeWidth: 2),
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white),
)
: const Text('Se connecter'),
),
@@ -120,12 +208,45 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
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,
),
),
),
],
],
),
),
),
),
),
);
);
}
}