Initial commit du projet Flutter
This commit is contained in:
600
lib/services/connectivity_service.dart
Normal file
600
lib/services/connectivity_service.dart
Normal file
@@ -0,0 +1,600 @@
|
||||
// lib/services/connectivity_service.dart - VERSION AVEC SUPPORT ENTREPRISE
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../main.dart'; // Pour accéder à AuthController
|
||||
|
||||
class ConnectivityService extends ChangeNotifier {
|
||||
static final ConnectivityService _instance = ConnectivityService._internal();
|
||||
factory ConnectivityService() => _instance;
|
||||
ConnectivityService._internal();
|
||||
|
||||
bool _isConnected = true;
|
||||
bool _isChecking = false;
|
||||
Timer? _periodicTimer;
|
||||
bool _hasNetworkInterface = true;
|
||||
DateTime? _lastDisconnectionTime;
|
||||
int _reconnectionAttempts = 0;
|
||||
|
||||
bool get isConnected => _isConnected;
|
||||
bool get hasNetworkInterface => _hasNetworkInterface;
|
||||
DateTime? get lastDisconnectionTime => _lastDisconnectionTime;
|
||||
int get reconnectionAttempts => _reconnectionAttempts;
|
||||
|
||||
// Clé globale pour accéder au navigator
|
||||
static GlobalKey<NavigatorState>? navigatorKey;
|
||||
|
||||
/// Initialiser le service avec la clé navigator
|
||||
static void setNavigatorKey(GlobalKey<NavigatorState> key) {
|
||||
navigatorKey = key;
|
||||
print('✅ NavigatorKey configuré pour ConnectivityService');
|
||||
}
|
||||
|
||||
/// Démarrer la surveillance
|
||||
void startMonitoring() {
|
||||
print('🌐 Démarrage de la surveillance de connectivité');
|
||||
_periodicTimer?.cancel();
|
||||
_periodicTimer = Timer.periodic(Duration(seconds: 5), (timer) {
|
||||
checkConnectivity();
|
||||
});
|
||||
|
||||
// Vérification initiale après un délai
|
||||
Future.delayed(Duration(seconds: 2), () {
|
||||
checkConnectivity();
|
||||
});
|
||||
}
|
||||
|
||||
/// Vérifier la connectivité
|
||||
Future<void> checkConnectivity() async {
|
||||
if (_isChecking) return;
|
||||
_isChecking = true;
|
||||
|
||||
try {
|
||||
print('🔍 Vérification de la connectivité...');
|
||||
|
||||
// Étape 1: Vérifier les interfaces réseau
|
||||
_hasNetworkInterface = await _checkNetworkInterfaces();
|
||||
|
||||
if (!_hasNetworkInterface) {
|
||||
print('❌ Aucune interface réseau trouvée');
|
||||
_updateConnectivityStatus(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Étape 2: Tester la connectivité internet
|
||||
bool hasInternetAccess = await _testInternetConnectivity();
|
||||
print('🌍 Accès internet: $hasInternetAccess');
|
||||
|
||||
_updateConnectivityStatus(hasInternetAccess);
|
||||
} catch (e) {
|
||||
print('❌ Erreur lors de la vérification: $e');
|
||||
_updateConnectivityStatus(false);
|
||||
} finally {
|
||||
_isChecking = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Vérifier les interfaces réseau
|
||||
Future<bool> _checkNetworkInterfaces() async {
|
||||
try {
|
||||
final interfaces = await NetworkInterface.list(
|
||||
includeLinkLocal: false,
|
||||
type: InternetAddressType.any,
|
||||
);
|
||||
|
||||
bool hasActiveInterface = interfaces.any(
|
||||
(interface) =>
|
||||
!interface.name.toLowerCase().contains('lo') &&
|
||||
interface.addresses.isNotEmpty,
|
||||
);
|
||||
|
||||
print('📡 Interfaces réseau actives: $hasActiveInterface');
|
||||
if (hasActiveInterface) {
|
||||
print(
|
||||
' Interfaces trouvées: ${interfaces.map((i) => i.name).join(", ")}',
|
||||
);
|
||||
}
|
||||
|
||||
return hasActiveInterface;
|
||||
} catch (e) {
|
||||
print('❌ Erreur vérification interfaces: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Tester la connectivité internet avec plusieurs cibles
|
||||
Future<bool> _testInternetConnectivity() async {
|
||||
final testTargets = [
|
||||
{'host': '1.1.1.1', 'port': 53, 'name': 'Cloudflare DNS'},
|
||||
{'host': '8.8.8.8', 'port': 53, 'name': 'Google DNS'},
|
||||
{'host': 'google.com', 'port': 80, 'name': 'Google'},
|
||||
];
|
||||
|
||||
for (var target in testTargets) {
|
||||
try {
|
||||
final socket = await Socket.connect(
|
||||
target['host'] as String,
|
||||
target['port'] as int,
|
||||
timeout: Duration(seconds: 3),
|
||||
);
|
||||
socket.destroy();
|
||||
print('✅ Connexion réussie à ${target['name']}');
|
||||
return true;
|
||||
} catch (e) {
|
||||
print('⚠️ Échec connexion à ${target['name']}: $e');
|
||||
}
|
||||
}
|
||||
|
||||
print('❌ Toutes les tentatives de connexion ont échoué');
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Mettre à jour le statut de connectivité
|
||||
void _updateConnectivityStatus(bool isConnected) {
|
||||
if (isConnected != _isConnected) {
|
||||
print('🔄 Changement connectivité: $_isConnected -> $isConnected');
|
||||
|
||||
final previousStatus = _isConnected;
|
||||
_isConnected = isConnected;
|
||||
|
||||
if (!_isConnected) {
|
||||
_lastDisconnectionTime = DateTime.now();
|
||||
_reconnectionAttempts = 0;
|
||||
_showNoConnectionDialog();
|
||||
} else if (previousStatus == false && _isConnected) {
|
||||
// Reconnexion réussie
|
||||
print('✅ Reconnexion établie');
|
||||
_reconnectionAttempts = 0;
|
||||
_showReconnectionSuccess();
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// Afficher le dialog de perte de connexion
|
||||
void _showNoConnectionDialog() {
|
||||
if (navigatorKey?.currentState == null) {
|
||||
print('⚠️ NavigatorState non disponible');
|
||||
return;
|
||||
}
|
||||
|
||||
print('🚨 Affichage du popup de perte de connexion');
|
||||
|
||||
navigatorKey!.currentState!.push(
|
||||
PageRouteBuilder(
|
||||
opaque: false,
|
||||
barrierDismissible: false,
|
||||
barrierColor: Colors.black54,
|
||||
pageBuilder: (BuildContext context, _, __) {
|
||||
return WillPopScope(
|
||||
onWillPop: () async => false,
|
||||
child: Dialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: _buildDialogContent(context),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Construire le contenu du dialog de déconnexion
|
||||
Widget _buildDialogContent(BuildContext context) {
|
||||
return Container(
|
||||
padding: EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Icône animée
|
||||
TweenAnimationBuilder<double>(
|
||||
duration: Duration(milliseconds: 800),
|
||||
tween: Tween(begin: 0.0, end: 1.0),
|
||||
builder: (context, value, child) {
|
||||
return Transform.scale(
|
||||
scale: value,
|
||||
child: Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [Colors.red[400]!, Colors.red[600]!],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.red.withOpacity(0.3),
|
||||
blurRadius: 20,
|
||||
offset: Offset(0, 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Icon(
|
||||
Icons.wifi_off_rounded,
|
||||
color: Colors.white,
|
||||
size: 40,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
SizedBox(height: 24),
|
||||
|
||||
// Titre
|
||||
Text(
|
||||
'Problème de connexion',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF006699),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
||||
SizedBox(height: 16),
|
||||
|
||||
// Message détaillé
|
||||
Container(
|
||||
padding: EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red[50],
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.red[200]!, width: 1),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
_hasNetworkInterface
|
||||
? Icons.cloud_off_rounded
|
||||
: Icons.wifi_off_rounded,
|
||||
color: Colors.red[700],
|
||||
size: 24,
|
||||
),
|
||||
SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_getDetailedMessage(),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.red[900],
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(height: 24),
|
||||
|
||||
// Informations utilisateur (si connecté)
|
||||
Consumer<AuthController>(
|
||||
builder: (context, authController, child) {
|
||||
if (authController.isLoggedIn) {
|
||||
final isEnterprise = authController.isEnterpriseMember;
|
||||
return Container(
|
||||
padding: EdgeInsets.all(12),
|
||||
margin: EdgeInsets.only(bottom: 16),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
isEnterprise
|
||||
? Color(0xFF8B5CF6).withOpacity(0.1)
|
||||
: Color(0xFF006699).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color:
|
||||
isEnterprise
|
||||
? Color(0xFF8B5CF6).withOpacity(0.3)
|
||||
: Color(0xFF006699).withOpacity(0.3),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
isEnterprise ? Icons.business : Icons.person,
|
||||
color:
|
||||
isEnterprise
|
||||
? Color(0xFF8B5CF6)
|
||||
: Color(0xFF006699),
|
||||
size: 20,
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
authController.agentName ?? 'Agent',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black87,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (isEnterprise &&
|
||||
authController.enterprise?.nomEntreprise !=
|
||||
null) ...[
|
||||
SizedBox(height: 2),
|
||||
Text(
|
||||
authController.enterprise!.nomEntreprise,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
isEnterprise
|
||||
? Color(0xFF8B5CF6)
|
||||
: Color(0xFF006699),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
isEnterprise ? 'Entreprise' : 'Agent',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
return SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
|
||||
// Boutons d'action
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
_reconnectionAttempts++;
|
||||
checkConnectivity();
|
||||
},
|
||||
icon: Icon(Icons.refresh_rounded, size: 20),
|
||||
label: Text('Réessayer'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: EdgeInsets.symmetric(vertical: 16),
|
||||
side: BorderSide(color: Color(0xFF006699), width: 2),
|
||||
foregroundColor: Color(0xFF006699),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
_openWifiSettings();
|
||||
},
|
||||
icon: Icon(Icons.wifi_find_rounded, size: 20),
|
||||
label: Text('WiFi'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Color(0xFF006699),
|
||||
foregroundColor: Colors.white,
|
||||
padding: EdgeInsets.symmetric(vertical: 16),
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Compteur de tentatives
|
||||
if (_reconnectionAttempts > 0) ...[
|
||||
SizedBox(height: 12),
|
||||
Text(
|
||||
'Tentative ${_reconnectionAttempts + 1}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Message détaillé selon le type de problème
|
||||
String _getDetailedMessage() {
|
||||
if (!_hasNetworkInterface) {
|
||||
return 'Aucun réseau WiFi détecté. Veuillez vous connecter à un réseau WiFi pour utiliser WORTIS Agent.';
|
||||
} else {
|
||||
return 'Connexion internet indisponible. Vérifiez que votre réseau WiFi a accès à internet.';
|
||||
}
|
||||
}
|
||||
|
||||
/// Afficher un message de reconnexion réussie
|
||||
void _showReconnectionSuccess() {
|
||||
if (navigatorKey?.currentContext == null) return;
|
||||
|
||||
final context = navigatorKey!.currentContext!;
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(Icons.wifi_rounded, color: Colors.white, size: 20),
|
||||
),
|
||||
SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Connexion rétablie',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 2),
|
||||
Text(
|
||||
'Vous pouvez continuer à utiliser l\'application',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
backgroundColor: Colors.green[600],
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
duration: Duration(seconds: 3),
|
||||
margin: EdgeInsets.all(16),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Ouvrir les paramètres WiFi
|
||||
void _openWifiSettings() async {
|
||||
try {
|
||||
print('📱 Ouverture des paramètres WiFi');
|
||||
const platform = MethodChannel('com.wortis.agent/settings');
|
||||
|
||||
// Sortir du mode kiosque si activé
|
||||
try {
|
||||
await platform.invokeMethod('exitKioskMode');
|
||||
} catch (e) {
|
||||
print('⚠️ Mode kiosque non actif ou erreur: $e');
|
||||
}
|
||||
|
||||
// Ouvrir les paramètres WiFi
|
||||
await platform.invokeMethod('openWifiSettings');
|
||||
|
||||
// Fermer le dialog
|
||||
if (navigatorKey?.currentContext != null) {
|
||||
Navigator.of(navigatorKey!.currentContext!).pop();
|
||||
}
|
||||
} catch (e) {
|
||||
print('❌ Erreur ouverture paramètres WiFi: $e');
|
||||
|
||||
if (navigatorKey?.currentContext != null) {
|
||||
ScaffoldMessenger.of(navigatorKey!.currentContext!).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Impossible d\'ouvrir les paramètres WiFi'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Forcer un test manuel (pour debug)
|
||||
void forceCheck() {
|
||||
print('🔧 Test forcé de connectivité');
|
||||
checkConnectivity();
|
||||
}
|
||||
|
||||
/// Méthode de test pour forcer l'affichage du popup
|
||||
void showTestPopup() {
|
||||
print('🧪 Test d\'affichage du popup de déconnexion');
|
||||
_isConnected = false;
|
||||
_lastDisconnectionTime = DateTime.now();
|
||||
_reconnectionAttempts = 0;
|
||||
_showNoConnectionDialog();
|
||||
}
|
||||
|
||||
/// Simuler une reconnexion (pour test)
|
||||
void simulateReconnection() {
|
||||
print('🧪 Simulation de reconnexion');
|
||||
_updateConnectivityStatus(true);
|
||||
}
|
||||
|
||||
/// Obtenir les statistiques de connectivité
|
||||
Map<String, dynamic> getConnectivityStats() {
|
||||
return {
|
||||
'is_connected': _isConnected,
|
||||
'has_network_interface': _hasNetworkInterface,
|
||||
'last_disconnection': _lastDisconnectionTime?.toIso8601String(),
|
||||
'reconnection_attempts': _reconnectionAttempts,
|
||||
'is_checking': _isChecking,
|
||||
};
|
||||
}
|
||||
|
||||
/// Réinitialiser les statistiques
|
||||
void resetStats() {
|
||||
_reconnectionAttempts = 0;
|
||||
_lastDisconnectionTime = null;
|
||||
print('📊 Statistiques de connectivité réinitialisées');
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
print('🛑 Arrêt du service de connectivité');
|
||||
_periodicTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension pour des méthodes utilitaires
|
||||
extension ConnectivityServiceExtension on ConnectivityService {
|
||||
/// Vérifier si la connexion est stable
|
||||
bool get isStable {
|
||||
return _isConnected && _reconnectionAttempts == 0;
|
||||
}
|
||||
|
||||
/// Obtenir le temps depuis la dernière déconnexion
|
||||
Duration? get timeSinceLastDisconnection {
|
||||
if (_lastDisconnectionTime == null) return null;
|
||||
return DateTime.now().difference(_lastDisconnectionTime!);
|
||||
}
|
||||
|
||||
/// Obtenir un message de statut lisible
|
||||
String get statusMessage {
|
||||
if (_isConnected) {
|
||||
if (_reconnectionAttempts > 0) {
|
||||
return 'Connexion rétablie après $_reconnectionAttempts tentatives';
|
||||
}
|
||||
return 'Connecté';
|
||||
} else {
|
||||
if (!_hasNetworkInterface) {
|
||||
return 'Aucun réseau WiFi';
|
||||
}
|
||||
return 'Pas de connexion internet';
|
||||
}
|
||||
}
|
||||
}
|
||||
561
lib/services/wortis_api_service.dart
Normal file
561
lib/services/wortis_api_service.dart
Normal file
@@ -0,0 +1,561 @@
|
||||
// Enhanced lib/services/wortis_api_service.dart
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import '../models/service_model.dart';
|
||||
|
||||
class WortisApiService {
|
||||
static const String baseUrl = 'https://api.live.wortis.cg/tpe';
|
||||
|
||||
// Singleton pattern
|
||||
static final WortisApiService _instance = WortisApiService._internal();
|
||||
factory WortisApiService() => _instance;
|
||||
WortisApiService._internal();
|
||||
|
||||
// Retry configuration
|
||||
static const int maxRetries = 3;
|
||||
static const Duration retryDelay = Duration(seconds: 2);
|
||||
|
||||
Future<List<WortisService>> getServices() async {
|
||||
return _retryRequest<List<WortisService>>(() async {
|
||||
final response = await http
|
||||
.get(
|
||||
Uri.parse('$baseUrl/get_services_back'),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Connection': 'keep-alive',
|
||||
},
|
||||
)
|
||||
.timeout(Duration(seconds: 30));
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final Map<String, dynamic> jsonData = json.decode(response.body);
|
||||
final List<dynamic> servicesJson = jsonData['all_services'] ?? [];
|
||||
|
||||
return servicesJson
|
||||
.map((serviceJson) => WortisService.fromJson(serviceJson))
|
||||
.toList();
|
||||
} else {
|
||||
throw Exception('Erreur API: ${response.statusCode}');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Méthode pour grouper les services par secteur
|
||||
Map<String, List<WortisService>> groupServicesBySector(
|
||||
List<WortisService> services,
|
||||
) {
|
||||
Map<String, List<WortisService>> grouped = {};
|
||||
|
||||
for (var service in services) {
|
||||
String sector = service.secteurActivite;
|
||||
if (grouped[sector] == null) {
|
||||
grouped[sector] = [];
|
||||
}
|
||||
grouped[sector]!.add(service);
|
||||
}
|
||||
|
||||
return grouped;
|
||||
}
|
||||
|
||||
// Generic retry logic
|
||||
Future<T> _retryRequest<T>(Future<T> Function() request) async {
|
||||
Exception? lastException;
|
||||
|
||||
for (int attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
return await request();
|
||||
} on SocketException catch (e) {
|
||||
lastException = e;
|
||||
print('Tentative $attempt échouée (SocketException): $e');
|
||||
if (attempt < maxRetries) {
|
||||
await Future.delayed(retryDelay * attempt);
|
||||
}
|
||||
} on HttpException catch (e) {
|
||||
lastException = e;
|
||||
print('Tentative $attempt échouée (HttpException): $e');
|
||||
if (attempt < maxRetries) {
|
||||
await Future.delayed(retryDelay * attempt);
|
||||
}
|
||||
} on http.ClientException catch (e) {
|
||||
lastException = e;
|
||||
print('Tentative $attempt échouée (ClientException): $e');
|
||||
if (attempt < maxRetries) {
|
||||
await Future.delayed(retryDelay * attempt);
|
||||
}
|
||||
} catch (e) {
|
||||
// For other exceptions, don't retry
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
throw Exception(
|
||||
'Impossible de se connecter après $maxRetries tentatives: $lastException',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AuthApiService {
|
||||
static const String baseUrl = 'https://api.live.wortis.cg/tpe';
|
||||
static const int maxRetries = 3;
|
||||
static const Duration retryDelay = Duration(seconds: 2);
|
||||
|
||||
/// Test de connexion à l'API avec retry
|
||||
static Future<bool> testConnection() async {
|
||||
try {
|
||||
return await _retryRequest<bool>(() async {
|
||||
final response = await http
|
||||
.get(
|
||||
Uri.parse('http://google.com/'),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Connection': 'keep-alive',
|
||||
},
|
||||
)
|
||||
.timeout(Duration(seconds: 10));
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = jsonDecode(response.body);
|
||||
return data['success'] == true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
} catch (e) {
|
||||
print('Erreur test connexion après plusieurs tentatives: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Connexion utilisateur avec retry
|
||||
static Future<Map<String, dynamic>> login(String agentId, String pin) async {
|
||||
try {
|
||||
return await _retryRequest<Map<String, dynamic>>(() async {
|
||||
final response = await http
|
||||
.post(
|
||||
Uri.parse('$baseUrl/auth/login'),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Connection': 'keep-alive',
|
||||
},
|
||||
body: jsonEncode({'agent_id': agentId, 'pin': pin}),
|
||||
)
|
||||
.timeout(Duration(seconds: 15));
|
||||
|
||||
return jsonDecode(response.body);
|
||||
});
|
||||
} catch (e) {
|
||||
print('Erreur login API après retry: $e');
|
||||
return {'success': false, 'message': 'Erreur de connexion au serveur'};
|
||||
}
|
||||
}
|
||||
|
||||
/// Créer un utilisateur avec retry
|
||||
static Future<Map<String, dynamic>> createUser(
|
||||
String nom,
|
||||
String pin, {
|
||||
String role = 'agent',
|
||||
}) async {
|
||||
try {
|
||||
return await _retryRequest<Map<String, dynamic>>(() async {
|
||||
final response = await http
|
||||
.post(
|
||||
Uri.parse('$baseUrl/users/register'),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Connection': 'keep-alive',
|
||||
},
|
||||
body: jsonEncode({'nom': nom, 'pin': pin, 'role': role}),
|
||||
)
|
||||
.timeout(Duration(seconds: 15));
|
||||
|
||||
return jsonDecode(response.body);
|
||||
});
|
||||
} catch (e) {
|
||||
print('Erreur création utilisateur après retry: $e');
|
||||
return {'success': false, 'message': 'Erreur de création'};
|
||||
}
|
||||
}
|
||||
|
||||
/// Récupérer le solde d'un agent avec retry
|
||||
static Future<Map<String, dynamic>> getAgentBalance(String agentId) async {
|
||||
try {
|
||||
return await _retryRequest<Map<String, dynamic>>(() async {
|
||||
final response = await http
|
||||
.get(
|
||||
Uri.parse('$baseUrl/agent/$agentId/balance'),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Connection': 'keep-alive',
|
||||
},
|
||||
)
|
||||
.timeout(Duration(seconds: 15));
|
||||
|
||||
return jsonDecode(response.body);
|
||||
});
|
||||
} catch (e) {
|
||||
print('Erreur récupération solde après retry: $e');
|
||||
return {
|
||||
'success': false,
|
||||
'message': 'Erreur lors de la récupération du solde',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Recharger le solde d'un agent avec retry
|
||||
static Future<Map<String, dynamic>> rechargeAgent(
|
||||
String agentId,
|
||||
double montant,
|
||||
) async {
|
||||
try {
|
||||
return await _retryRequest<Map<String, dynamic>>(() async {
|
||||
final response = await http
|
||||
.post(
|
||||
Uri.parse('$baseUrl/agent/$agentId/recharge'),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Connection': 'keep-alive',
|
||||
},
|
||||
body: jsonEncode({'montant': montant}),
|
||||
)
|
||||
.timeout(Duration(seconds: 15));
|
||||
|
||||
return jsonDecode(response.body);
|
||||
});
|
||||
} catch (e) {
|
||||
print('Erreur recharge après retry: $e');
|
||||
return {'success': false, 'message': 'Erreur lors de la recharge'};
|
||||
}
|
||||
}
|
||||
|
||||
/// Mettre à jour le solde d'un agent avec retry
|
||||
static Future<Map<String, dynamic>> updateAgentBalance(
|
||||
String agentId,
|
||||
double nouveauSolde,
|
||||
) async {
|
||||
try {
|
||||
return await _retryRequest<Map<String, dynamic>>(() async {
|
||||
final response = await http
|
||||
.put(
|
||||
Uri.parse('$baseUrl/agent/$agentId/balance'),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Connection': 'keep-alive',
|
||||
},
|
||||
body: jsonEncode({'solde': nouveauSolde}),
|
||||
)
|
||||
.timeout(Duration(seconds: 15));
|
||||
|
||||
return jsonDecode(response.body);
|
||||
});
|
||||
} catch (e) {
|
||||
print('Erreur mise à jour solde après retry: $e');
|
||||
return {
|
||||
'success': false,
|
||||
'message': 'Erreur lors de la mise à jour du solde',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Enregistrer une transaction avec retry
|
||||
static Future<Map<String, dynamic>> recordTransaction({
|
||||
required String agentId,
|
||||
required String serviceId,
|
||||
required String serviceName,
|
||||
required double montant,
|
||||
required double commission,
|
||||
required String typeTransaction,
|
||||
required Map<String, dynamic> detailsTransaction,
|
||||
}) async {
|
||||
try {
|
||||
return await _retryRequest<Map<String, dynamic>>(() async {
|
||||
final response = await http
|
||||
.post(
|
||||
Uri.parse('$baseUrl/transactions'),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Connection': 'keep-alive',
|
||||
},
|
||||
body: jsonEncode({
|
||||
'agent_id': agentId,
|
||||
'service_id': serviceId,
|
||||
'service_name': serviceName,
|
||||
'montant': montant,
|
||||
'commission': commission,
|
||||
'type_transaction': typeTransaction,
|
||||
'details': detailsTransaction,
|
||||
'date_transaction': DateTime.now().toIso8601String(),
|
||||
}),
|
||||
)
|
||||
.timeout(Duration(seconds: 15));
|
||||
|
||||
return jsonDecode(response.body);
|
||||
});
|
||||
} catch (e) {
|
||||
print('Erreur enregistrement transaction après retry: $e');
|
||||
return {
|
||||
'success': false,
|
||||
'message': 'Erreur lors de l\'enregistrement de la transaction',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Generic retry logic for AuthApiService
|
||||
static Future<T> _retryRequest<T>(Future<T> Function() request) async {
|
||||
Exception? lastException;
|
||||
|
||||
for (int attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
return await request();
|
||||
} on SocketException catch (e) {
|
||||
lastException = e;
|
||||
print('AuthAPI - Tentative $attempt échouée (SocketException): $e');
|
||||
if (attempt < maxRetries) {
|
||||
await Future.delayed(retryDelay * attempt);
|
||||
}
|
||||
} on HttpException catch (e) {
|
||||
lastException = e;
|
||||
print('AuthAPI - Tentative $attempt échouée (HttpException): $e');
|
||||
if (attempt < maxRetries) {
|
||||
await Future.delayed(retryDelay * attempt);
|
||||
}
|
||||
} on http.ClientException catch (e) {
|
||||
lastException = e;
|
||||
print('AuthAPI - Tentative $attempt échouée (ClientException): $e');
|
||||
if (attempt < maxRetries) {
|
||||
await Future.delayed(retryDelay * attempt);
|
||||
}
|
||||
} catch (e) {
|
||||
// For other exceptions, don't retry
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
throw Exception(
|
||||
'Impossible de se connecter après $maxRetries tentatives: $lastException',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ApiService {
|
||||
static const String baseUrl = 'https://api.live.wortis.cg/tpe';
|
||||
static const int maxRetries = 3;
|
||||
static const Duration retryDelay = Duration(seconds: 2);
|
||||
|
||||
/// Récupérer les champs d'un service avec retry amélioré
|
||||
Future<Map<String, dynamic>> fetchServiceFields(String serviceName) async {
|
||||
return _retryRequest<Map<String, dynamic>>(() async {
|
||||
// Fix: Remove double slash in URL
|
||||
final url = '$baseUrl/service/${Uri.encodeComponent(serviceName)}';
|
||||
print('Fetching service fields from: $url');
|
||||
|
||||
final response = await http
|
||||
.get(
|
||||
Uri.parse(url),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Connection': 'keep-alive',
|
||||
'User-Agent': 'WortisApp/1.0',
|
||||
},
|
||||
)
|
||||
.timeout(Duration(seconds: 30));
|
||||
|
||||
print('Response status: ${response.statusCode}');
|
||||
print('Response headers: ${response.headers}');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return jsonDecode(response.body);
|
||||
} else {
|
||||
throw Exception(
|
||||
'Erreur API: ${response.statusCode} - ${response.body}',
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Vérifier les données avec GET et retry
|
||||
Future<Map<String, dynamic>> verifyDataGet(
|
||||
String url,
|
||||
Map<String, dynamic> params,
|
||||
) async {
|
||||
return _retryRequest<Map<String, dynamic>>(() async {
|
||||
final uri = Uri.parse(url).replace(
|
||||
queryParameters: params.map(
|
||||
(key, value) => MapEntry(key, value.toString()),
|
||||
),
|
||||
);
|
||||
|
||||
final response = await http
|
||||
.get(
|
||||
uri,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Connection': 'keep-alive',
|
||||
},
|
||||
)
|
||||
.timeout(Duration(seconds: 30));
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return jsonDecode(response.body);
|
||||
} else {
|
||||
throw Exception(
|
||||
'Données non trouvées - Status: ${response.statusCode}',
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Vérifier les données avec POST et retry
|
||||
Future<Map<String, dynamic>> verifyDataPost(
|
||||
String url,
|
||||
Map<String, dynamic> data,
|
||||
String operationId,
|
||||
) async {
|
||||
return _retryRequest<Map<String, dynamic>>(() async {
|
||||
final response = await http
|
||||
.post(
|
||||
Uri.parse(url),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Connection': 'keep-alive',
|
||||
},
|
||||
body: jsonEncode(data),
|
||||
)
|
||||
.timeout(Duration(seconds: 30));
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return jsonDecode(response.body);
|
||||
} else {
|
||||
throw Exception(
|
||||
'Données non trouvées - Status: ${response.statusCode}',
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Soumettre les données du formulaire avec retry
|
||||
Future<void> submitFormData(
|
||||
BuildContext context,
|
||||
String url,
|
||||
Map<String, dynamic> data,
|
||||
Map<String, dynamic>? serviceData,
|
||||
dynamic additionalData,
|
||||
bool isCardPayment,
|
||||
) async {
|
||||
try {
|
||||
await _retryRequest<void>(() async {
|
||||
print(url);
|
||||
final response = await http
|
||||
.post(
|
||||
Uri.parse(url),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Connection': 'keep-alive',
|
||||
},
|
||||
body: jsonEncode(data),
|
||||
)
|
||||
.timeout(Duration(seconds: 30));
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final responseData = jsonDecode(response.body);
|
||||
|
||||
// Afficher un message de succès
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Row(
|
||||
children: [
|
||||
Icon(Icons.check_circle, color: Colors.white),
|
||||
SizedBox(width: 8),
|
||||
Text('Transaction effectuée avec succès'),
|
||||
],
|
||||
),
|
||||
backgroundColor: Colors.green,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Retourner à la page précédente
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
} else {
|
||||
throw Exception(
|
||||
'Erreur lors de la soumission - Status: ${response.statusCode}',
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
print('Erreur submitFormData après retry: $e');
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Row(
|
||||
children: [
|
||||
Icon(Icons.error, color: Colors.white),
|
||||
SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Erreur lors de la transaction: ${e.toString()}',
|
||||
maxLines: 2,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
backgroundColor: Colors.red,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
duration: Duration(seconds: 5),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Annuler l'opération en cours
|
||||
void cancelOperation() {
|
||||
print('Opération annulée');
|
||||
}
|
||||
|
||||
// Generic retry logic for ApiService
|
||||
Future<T> _retryRequest<T>(Future<T> Function() request) async {
|
||||
Exception? lastException;
|
||||
|
||||
for (int attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
return await request();
|
||||
} on SocketException catch (e) {
|
||||
lastException = e;
|
||||
print('API - Tentative $attempt échouée (SocketException): $e');
|
||||
if (attempt < maxRetries) {
|
||||
await Future.delayed(retryDelay * attempt);
|
||||
}
|
||||
} on HttpException catch (e) {
|
||||
lastException = e;
|
||||
print('API - Tentative $attempt échouée (HttpException): $e');
|
||||
if (attempt < maxRetries) {
|
||||
await Future.delayed(retryDelay * attempt);
|
||||
}
|
||||
} on http.ClientException catch (e) {
|
||||
lastException = e;
|
||||
print('API - Tentative $attempt échouée (ClientException): $e');
|
||||
if (attempt < maxRetries) {
|
||||
await Future.delayed(retryDelay * attempt);
|
||||
}
|
||||
} catch (e) {
|
||||
// For other exceptions, don't retry
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
throw Exception(
|
||||
'Impossible de se connecter après $maxRetries tentatives: $lastException',
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user