Restructuration majeure du projet: migration de flutt vers app, ajout de l'API et mise à jour du site web

This commit is contained in:
d6soft
2025-05-16 09:19:03 +02:00
parent b5aafc424b
commit 5c2620de30
391 changed files with 19780 additions and 7233 deletions

View File

@@ -0,0 +1,269 @@
import 'dart:async';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/foundation.dart';
import 'package:geosector_app/core/data/models/user_model.dart';
import 'package:geosector_app/core/constants/app_keys.dart';
import 'package:retry/retry.dart';
import 'package:universal_html/html.dart' as html;
class ApiService {
final Dio _dio = Dio();
late final String _baseUrl;
late final String _appIdentifier;
String? _sessionId;
// Détermine l'environnement actuel (DEV, REC, PROD) en fonction de l'URL
String _determineEnvironment() {
if (!kIsWeb) {
// En mode non-web, utiliser l'environnement de développement par défaut
return 'DEV';
}
final currentUrl = html.window.location.href.toLowerCase();
if (currentUrl.contains('dapp.geosector.fr')) {
return 'DEV';
} else if (currentUrl.contains('rapp.geosector.fr')) {
return 'REC';
} else {
return 'PROD';
}
}
// Configure l'URL de base API et l'identifiant d'application selon l'environnement
void _configureEnvironment() {
final env = _determineEnvironment();
switch (env) {
case 'DEV':
_baseUrl = AppKeys.baseApiUrlDev;
_appIdentifier = AppKeys.appIdentifierDev;
break;
case 'REC':
_baseUrl = AppKeys.baseApiUrlRec;
_appIdentifier = AppKeys.appIdentifierRec;
break;
default: // PROD
_baseUrl = AppKeys.baseApiUrlProd;
_appIdentifier = AppKeys.appIdentifierProd;
}
debugPrint('GEOSECTOR 🔗 Environnement: $env, API: $_baseUrl');
}
ApiService() {
// Configurer l'environnement
_configureEnvironment();
// Configurer Dio
_dio.options.baseUrl = _baseUrl;
_dio.options.connectTimeout = AppKeys.connectionTimeout;
_dio.options.receiveTimeout = AppKeys.receiveTimeout;
// Ajouter les en-têtes par défaut avec l'identifiant d'application adapté à l'environnement
final headers = Map<String, String>.from(AppKeys.defaultHeaders);
headers['X-App-Identifier'] = _appIdentifier;
_dio.options.headers.addAll(headers);
// Ajouter des intercepteurs pour l'authentification par session
_dio.interceptors.add(InterceptorsWrapper(onRequest: (options, handler) {
// Ajouter le session_id comme token Bearer aux en-têtes si disponible
if (_sessionId != null) {
options.headers[AppKeys.sessionHeader] = 'Bearer $_sessionId';
}
return handler.next(options);
}, onError: (DioException error, handler) {
// Gérer les erreurs d'authentification (401)
if (error.response?.statusCode == 401) {
// Session expirée ou invalide
_sessionId = null;
}
return handler.next(error);
}));
}
// Définir l'ID de session
void setSessionId(String? sessionId) {
_sessionId = sessionId;
}
// Obtenir l'environnement actuel (utile pour le débogage)
String getCurrentEnvironment() {
return _determineEnvironment();
}
// Obtenir l'URL API actuelle (utile pour le débogage)
String getCurrentApiUrl() {
return _baseUrl;
}
// Obtenir l'identifiant d'application actuel (utile pour le débogage)
String getCurrentAppIdentifier() {
return _appIdentifier;
}
// Vérifier la connectivité réseau
Future<bool> hasInternetConnection() async {
final connectivityResult = await (Connectivity().checkConnectivity());
return connectivityResult != ConnectivityResult.none;
}
// Méthode POST générique
Future<Response> post(String path, {dynamic data}) async {
try {
return await _dio.post(path, data: data);
} catch (e) {
rethrow;
}
}
// Méthode GET générique
Future<Response> get(String path, {Map<String, dynamic>? queryParameters}) async {
try {
return await _dio.get(path, queryParameters: queryParameters);
} catch (e) {
rethrow;
}
}
// Méthode PUT générique
Future<Response> put(String path, {dynamic data}) async {
try {
return await _dio.put(path, data: data);
} catch (e) {
rethrow;
}
}
// Méthode DELETE générique
Future<Response> delete(String path) async {
try {
return await _dio.delete(path);
} catch (e) {
rethrow;
}
}
// Authentification avec PHP session
Future<Map<String, dynamic>> login(String username, String password, {required String type}) async {
try {
final response = await _dio.post(AppKeys.loginEndpoint, data: {
'username': username,
'password': password,
'type': type, // Ajouter le type de connexion (user ou admin)
});
// Vérifier la structure de la réponse
final data = response.data as Map<String, dynamic>;
final status = data['status'] as String?;
// Afficher le message en cas d'erreur
if (status != 'success') {
final message = data['message'] as String?;
debugPrint('Erreur d\'authentification: $message');
}
// Si le statut est 'success', récupérer le session_id
if (status == 'success' && data.containsKey('session_id')) {
final sessionId = data['session_id'];
// Définir la session pour les futures requêtes
if (sessionId != null) {
setSessionId(sessionId);
}
}
return data;
} catch (e) {
rethrow;
}
}
// Déconnexion
Future<void> logout() async {
try {
if (_sessionId != null) {
await _dio.post(AppKeys.logoutEndpoint);
_sessionId = null;
}
} catch (e) {
// Même en cas d'erreur, on réinitialise la session
_sessionId = null;
rethrow;
}
}
// Utilisateurs
Future<List<UserModel>> getUsers() async {
try {
final response = await retry(
() => _dio.get('/users'),
retryIf: (e) => e is SocketException || e is TimeoutException,
);
return (response.data as List)
.map((json) => UserModel.fromJson(json))
.toList();
} catch (e) {
// Gérer les erreurs
rethrow;
}
}
Future<UserModel> getUserById(int id) async {
try {
final response = await _dio.get('/users/$id');
return UserModel.fromJson(response.data);
} catch (e) {
rethrow;
}
}
Future<UserModel> createUser(UserModel user) async {
try {
final response = await _dio.post('/users', data: user.toJson());
return UserModel.fromJson(response.data);
} catch (e) {
rethrow;
}
}
Future<UserModel> updateUser(UserModel user) async {
try {
final response = await _dio.put('/users/${user.id}', data: user.toJson());
return UserModel.fromJson(response.data);
} catch (e) {
rethrow;
}
}
Future<void> deleteUser(String id) async {
try {
await _dio.delete('/users/$id');
} catch (e) {
rethrow;
}
}
// Espace réservé pour les futures méthodes de gestion des profils
// Espace réservé pour les futures méthodes de gestion des données
// Synchronisation en batch
Future<Map<String, dynamic>> syncData({
List<UserModel>? users,
}) async {
try {
final Map<String, dynamic> payload = {
if (users != null) 'users': users.map((u) => u.toJson()).toList(),
};
final response = await _dio.post('/sync', data: payload);
return response.data;
} catch (e) {
rethrow;
}
}
}

View File

@@ -0,0 +1,51 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:geosector_app/core/repositories/user_repository.dart';
import 'package:geosector_app/presentation/widgets/loading_overlay.dart';
/// Service qui gère les opérations d'authentification avec affichage d'un overlay de chargement
class AuthService {
final UserRepository _userRepository;
AuthService(this._userRepository);
/// Méthode de connexion avec affichage d'un overlay de chargement
Future<bool> login(BuildContext context, String username, String password,
{required String type}) async {
return await LoadingOverlay.show(
context: context,
spinnerSize: 80.0, // Spinner plus grand
strokeWidth: 6.0, // Trait plus épais
future: _userRepository.login(username, password, type: type),
);
}
/// Méthode de déconnexion avec affichage d'un overlay de chargement
/// et redirection vers la page de démarrage
Future<bool> logout(BuildContext context) async {
final bool result = await LoadingOverlay.show(
context: context,
spinnerSize: 80.0, // Spinner plus grand
strokeWidth: 6.0, // Trait plus épais
future: _userRepository.logout(),
);
// Si la déconnexion a réussi, rediriger vers la page de démarrage
if (result && context.mounted) {
// Utiliser GoRouter pour naviguer vers la page de démarrage
GoRouter.of(context).go('/');
}
return result;
}
/// Vérifie si un utilisateur est connecté
bool isLoggedIn() {
return _userRepository.isLoggedIn;
}
/// Récupère le rôle de l'utilisateur connecté
int getUserRole() {
return _userRepository.getUserRole();
}
}

View File

@@ -0,0 +1,157 @@
import 'dart:async';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
/// Service qui gère la surveillance de l'état de connectivité de l'appareil
class ConnectivityService extends ChangeNotifier {
final Connectivity _connectivity = Connectivity();
late StreamSubscription<List<ConnectivityResult>> _connectivitySubscription;
List<ConnectivityResult> _connectionStatus = [ConnectivityResult.none];
bool _isInitialized = false;
/// Indique si l'appareil est connecté à Internet
bool get isConnected {
// Vérifie si la liste contient au moins un type de connexion autre que 'none'
return _connectionStatus.any((result) => result != ConnectivityResult.none);
}
/// Indique si l'appareil est connecté via WiFi
bool get isWifi => _connectionStatus.contains(ConnectivityResult.wifi);
/// Indique si l'appareil est connecté via données mobiles (4G, 5G, etc.)
bool get isMobile => _connectionStatus.contains(ConnectivityResult.mobile);
/// Retourne le type de connexion actuel (WiFi, données mobiles, etc.)
List<ConnectivityResult> get connectionStatus => _connectionStatus;
/// Retourne le premier type de connexion actif (pour compatibilité avec l'ancien code)
ConnectivityResult get primaryConnectionStatus {
// Retourne le premier type de connexion qui n'est pas 'none', ou 'none' si tous sont 'none'
return _connectionStatus.firstWhere(
(result) => result != ConnectivityResult.none,
orElse: () => ConnectivityResult.none
);
}
/// Obtient une description textuelle du type de connexion
String get connectionType {
// Si aucune connexion n'est disponible
if (!isConnected) {
return 'Aucune connexion';
}
// Utiliser le premier type de connexion actif
ConnectivityResult primaryStatus = primaryConnectionStatus;
switch (primaryStatus) {
case ConnectivityResult.wifi:
return 'WiFi';
case ConnectivityResult.mobile:
return 'Données mobiles';
case ConnectivityResult.ethernet:
return 'Ethernet';
case ConnectivityResult.bluetooth:
return 'Bluetooth';
case ConnectivityResult.vpn:
return 'VPN';
case ConnectivityResult.none:
return 'Aucune connexion';
default:
return 'Inconnu';
}
}
/// Constructeur du service de connectivité
ConnectivityService() {
_initConnectivity();
}
/// Initialise le service et commence à écouter les changements de connectivité
Future<void> _initConnectivity() async {
if (_isInitialized) return;
try {
// En version web, on considère par défaut que la connexion est disponible
// car la vérification de connectivité est moins fiable sur le web
if (kIsWeb) {
_connectionStatus = [ConnectivityResult.wifi]; // Valeur par défaut pour le web
} else {
_connectionStatus = await _connectivity.checkConnectivity();
}
// S'abonner aux changements de connectivité
_connectivitySubscription = _connectivity.onConnectivityChanged.listen(_updateConnectionStatus);
_isInitialized = true;
} catch (e) {
debugPrint('Erreur lors de l\'initialisation du service de connectivité: $e');
// En cas d'erreur en version web, on suppose que la connexion est disponible
// car l'application web ne peut pas fonctionner sans connexion de toute façon
if (kIsWeb) {
_connectionStatus = [ConnectivityResult.wifi];
_isInitialized = true;
}
}
notifyListeners();
}
/// Met à jour l'état de la connexion lorsqu'il change
void _updateConnectionStatus(List<ConnectivityResult> results) {
// Vérifier si la liste des résultats a changé
bool hasChanged = false;
// Si les listes ont des longueurs différentes, elles sont différentes
if (_connectionStatus.length != results.length) {
hasChanged = true;
} else {
// Vérifier si les éléments sont différents
for (int i = 0; i < _connectionStatus.length; i++) {
if (i >= results.length || _connectionStatus[i] != results[i]) {
hasChanged = true;
break;
}
}
}
if (hasChanged) {
_connectionStatus = results;
notifyListeners();
}
}
/// Vérifie manuellement l'état actuel de la connexion
Future<List<ConnectivityResult>> checkConnectivity() async {
try {
// En version web, on considère par défaut que la connexion est disponible
if (kIsWeb) {
// En version web, on peut tenter de faire une requête réseau légère pour vérifier la connectivité
// mais pour l'instant, on suppose que la connexion est disponible
final results = [ConnectivityResult.wifi];
_updateConnectionStatus(results);
return results;
} else {
// Version mobile - utiliser l'API standard
final results = await _connectivity.checkConnectivity();
_updateConnectionStatus(results);
return results;
}
} catch (e) {
debugPrint('Erreur lors de la vérification de la connectivité: $e');
// En cas d'erreur, on conserve l'état actuel
return _connectionStatus;
}
}
@override
void dispose() {
try {
_connectivitySubscription.cancel();
} catch (e) {
debugPrint('Erreur lors de l\'annulation de l\'abonnement de connectivité: $e');
}
super.dispose();
}
}

View File

@@ -0,0 +1,149 @@
import 'package:flutter/foundation.dart';
import 'package:hive_flutter/hive_flutter.dart';
// Importations conditionnelles pour le web vs non-web
import 'js_interface.dart';
import 'package:geosector_app/core/constants/app_keys.dart';
import 'package:geosector_app/core/services/hive_web_fix.dart';
import 'package:geosector_app/core/data/models/user_model.dart';
import 'package:geosector_app/core/data/models/amicale_model.dart';
import 'package:geosector_app/core/data/models/client_model.dart';
import 'package:geosector_app/core/data/models/operation_model.dart';
import 'package:geosector_app/core/data/models/sector_model.dart';
import 'package:geosector_app/core/data/models/passage_model.dart';
import 'package:geosector_app/core/data/models/membre_model.dart';
import 'package:geosector_app/core/data/models/user_sector_model.dart';
import 'package:geosector_app/core/data/models/region_model.dart';
import 'package:geosector_app/chat/models/chat_adapters.dart';
/// Service pour réinitialiser et recréer les Hive Boxes
/// Utilisé pour résoudre les problèmes d'incompatibilité après mise à jour des modèles
class HiveResetService {
/// Réinitialise complètement Hive et recrée les boîtes nécessaires
static Future<bool> resetAndRecreateHiveBoxes() async {
try {
debugPrint(
'HiveResetService: Début de la réinitialisation complète de Hive');
// Approche plus radicale pour le web : supprimer directement IndexedDB
if (kIsWeb) {
// Utiliser JavaScript pour supprimer complètement la base de données IndexedDB
evalJs('''
(function() {
return new Promise(function(resolve, reject) {
// Fermer toutes les connexions IndexedDB
if (window.indexedDB) {
console.log("Suppression complète d'IndexedDB...");
var request = indexedDB.deleteDatabase("geosector_app");
request.onsuccess = function() {
console.log("IndexedDB supprimé avec succès");
resolve(true);
};
request.onerror = function(event) {
console.log("Erreur lors de la suppression d'IndexedDB", event);
reject(event);
};
} else {
console.log("IndexedDB n'est pas disponible");
resolve(false);
}
});
})();
''');
// Attendre un peu pour s'assurer que la suppression est terminée
await Future.delayed(const Duration(milliseconds: 1000));
// Réinitialiser Hive
await Hive.initFlutter();
} else {
// Pour les plateformes mobiles, on utilise une approche différente
await Hive.deleteFromDisk();
await Hive.initFlutter();
}
// Réenregistrer tous les adaptateurs
_registerAdapters();
// Rouvrir les boîtes essentielles
await _reopenEssentialBoxes();
debugPrint(
'HiveResetService: Réinitialisation complète terminée avec succès');
return true;
} catch (e) {
debugPrint('HiveResetService: Erreur lors de la réinitialisation: $e');
return false;
}
}
/// Ferme toutes les boîtes Hive ouvertes
static Future<void> _closeAllBoxes() async {
final boxNames = [
AppKeys.usersBoxName,
AppKeys.amicaleBoxName,
AppKeys.clientsBoxName,
AppKeys.operationsBoxName,
AppKeys.sectorsBoxName,
AppKeys.passagesBoxName,
AppKeys.settingsBoxName,
AppKeys.membresBoxName,
AppKeys.userSectorBoxName,
AppKeys.chatConversationsBoxName,
AppKeys.chatMessagesBoxName,
AppKeys.regionsBoxName,
];
for (final boxName in boxNames) {
if (Hive.isBoxOpen(boxName)) {
debugPrint('HiveResetService: Fermeture de la boîte $boxName');
await Hive.box(boxName).close();
}
}
}
/// Enregistre tous les adaptateurs Hive
static void _registerAdapters() {
debugPrint('HiveResetService: Enregistrement des adaptateurs Hive');
// Enregistrer les adaptateurs pour les modèles principaux
Hive.registerAdapter(UserModelAdapter());
Hive.registerAdapter(AmicaleModelAdapter());
Hive.registerAdapter(ClientModelAdapter());
Hive.registerAdapter(OperationModelAdapter());
Hive.registerAdapter(SectorModelAdapter());
Hive.registerAdapter(PassageModelAdapter());
Hive.registerAdapter(MembreModelAdapter());
Hive.registerAdapter(UserSectorModelAdapter());
// Enregistrer les adaptateurs pour le chat
Hive.registerAdapter(ConversationModelAdapter());
Hive.registerAdapter(MessageModelAdapter());
Hive.registerAdapter(ParticipantModelAdapter());
Hive.registerAdapter(AnonymousUserModelAdapter());
Hive.registerAdapter(AudienceTargetModelAdapter());
Hive.registerAdapter(NotificationSettingsAdapter());
// Vérifier si RegionModelAdapter est disponible
try {
Hive.registerAdapter(RegionModelAdapter());
} catch (e) {
debugPrint('HiveResetService: RegionModelAdapter non disponible: $e');
}
}
/// Rouvre les boîtes essentielles
static Future<void> _reopenEssentialBoxes() async {
debugPrint('HiveResetService: Réouverture des boîtes essentielles');
// Ouvrir les boîtes essentielles au démarrage
await Hive.openBox<UserModel>(AppKeys.usersBoxName);
await Hive.openBox<AmicaleModel>(AppKeys.amicaleBoxName);
await Hive.openBox<ClientModel>(AppKeys.clientsBoxName);
await Hive.openBox(AppKeys.settingsBoxName);
// Ouvrir les boîtes de chat
await Hive.openBox<ConversationModel>(AppKeys.chatConversationsBoxName);
await Hive.openBox<MessageModel>(AppKeys.chatMessagesBoxName);
}
}

View File

@@ -0,0 +1,40 @@
import 'package:flutter/foundation.dart';
/// Service pour gérer l'état de réinitialisation de Hive
/// Permet de stocker l'information indiquant si Hive a été réinitialisé
/// et de notifier les widgets intéressés
class HiveResetStateService extends ChangeNotifier {
/// Indique si Hive a été réinitialisé
bool _wasReset = false;
/// Indique si le dialogue de réinitialisation a déjà été affiché
bool _dialogShown = false;
/// Getter pour savoir si Hive a été réinitialisé
bool get wasReset => _wasReset;
/// Getter pour savoir si le dialogue a déjà été affiché
bool get dialogShown => _dialogShown;
/// Marque Hive comme ayant été réinitialisé
void markAsReset() {
_wasReset = true;
notifyListeners();
}
/// Marque le dialogue comme ayant été affiché
void markDialogAsShown() {
_dialogShown = true;
notifyListeners();
}
/// Réinitialise l'état (à utiliser après une déconnexion par exemple)
void reset() {
_wasReset = false;
_dialogShown = false;
notifyListeners();
}
}
/// Instance globale du service
final hiveResetStateService = HiveResetStateService();

View File

@@ -0,0 +1,182 @@
import 'dart:async';
import 'dart:js' as js;
import 'package:flutter/foundation.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:geosector_app/core/constants/app_keys.dart';
/// Service pour gérer les problèmes spécifiques à Hive en version web
class HiveWebFix {
/// Nettoie en toute sécurité les boîtes Hive en version web
/// Cette méthode est plus sûre que de supprimer directement IndexedDB
static Future<void> safeCleanHiveBoxes({List<String>? excludeBoxes}) async {
if (!kIsWeb) return;
try {
debugPrint(
'HiveWebFix: Nettoyage sécurisé des boîtes Hive en version web');
// Liste des boîtes à nettoyer
final boxesToClean = [
AppKeys.operationsBoxName,
AppKeys.sectorsBoxName,
AppKeys.passagesBoxName,
];
// Exclure certaines boîtes si spécifié
if (excludeBoxes != null) {
boxesToClean.removeWhere((box) => excludeBoxes.contains(box));
}
// Nettoyer chaque boîte individuellement au lieu de supprimer IndexedDB
for (final boxName in boxesToClean) {
try {
if (Hive.isBoxOpen(boxName)) {
debugPrint('HiveWebFix: Nettoyage de la boîte $boxName');
final box = Hive.box(boxName);
await box.clear();
debugPrint('HiveWebFix: Boîte $boxName nettoyée avec succès');
} else {
debugPrint(
'HiveWebFix: La boîte $boxName n\'est pas ouverte, ouverture temporaire');
final box = await Hive.openBox(boxName);
await box.clear();
await box.close();
debugPrint('HiveWebFix: Boîte $boxName nettoyée et fermée');
}
} catch (e) {
debugPrint(
'HiveWebFix: Erreur lors du nettoyage de la boîte $boxName: $e');
}
}
debugPrint('HiveWebFix: Nettoyage sécurisé terminé');
} catch (e) {
debugPrint('HiveWebFix: Erreur lors du nettoyage sécurisé: $e');
}
}
/// Vérifie l'intégrité des boîtes Hive et tente de les réparer si nécessaire
static Future<bool> checkAndRepairHiveBoxes() async {
if (!kIsWeb) return true;
try {
debugPrint('HiveWebFix: Vérification de l\'intégrité des boîtes Hive');
// Vérifier si IndexedDB est accessible
final isIndexedDBAvailable = js.context.hasProperty('indexedDB');
if (!isIndexedDBAvailable) {
debugPrint(
'HiveWebFix: IndexedDB n\'est pas disponible dans ce navigateur');
return false;
}
// Liste des boîtes essentielles
final essentialBoxes = [
AppKeys.usersBoxName,
AppKeys.settingsBoxName,
];
// Vérifier chaque boîte essentielle
for (final boxName in essentialBoxes) {
try {
if (!Hive.isBoxOpen(boxName)) {
debugPrint(
'HiveWebFix: Ouverture de la boîte essentielle $boxName');
await Hive.openBox(boxName);
}
// Vérifier si la boîte est accessible
final box = Hive.box(boxName);
// Tenter une opération simple pour vérifier l'intégrité
final length = box.length;
debugPrint(
'HiveWebFix: Boîte $boxName accessible avec $length éléments');
} catch (e) {
debugPrint('HiveWebFix: Erreur d\'accès à la boîte $boxName: $e');
// Tenter de réparer en réinitialisant Hive
try {
debugPrint(
'HiveWebFix: Tentative de réparation de la boîte $boxName');
// Fermer la boîte si elle est ouverte
if (Hive.isBoxOpen(boxName)) {
await Hive.box(boxName).close();
}
// Réouvrir la boîte
await Hive.openBox(boxName);
debugPrint('HiveWebFix: Boîte $boxName réparée avec succès');
} catch (repairError) {
debugPrint(
'HiveWebFix: Échec de la réparation de la boîte $boxName: $repairError');
return false;
}
}
}
debugPrint('HiveWebFix: Toutes les boîtes essentielles sont intègres');
return true;
} catch (e) {
debugPrint('HiveWebFix: Erreur lors de la vérification d\'intégrité: $e');
return false;
}
}
/// Réinitialise complètement Hive en cas de problème grave
/// À utiliser en dernier recours car cela supprimera toutes les données
static Future<void> resetHiveCompletely() async {
if (!kIsWeb) return;
try {
debugPrint('HiveWebFix: Réinitialisation complète de Hive');
// Fermer toutes les boîtes ouvertes
final boxesToClose = [
AppKeys.usersBoxName,
AppKeys.operationsBoxName,
AppKeys.sectorsBoxName,
AppKeys.passagesBoxName,
AppKeys.settingsBoxName,
];
for (final boxName in boxesToClose) {
if (Hive.isBoxOpen(boxName)) {
debugPrint('HiveWebFix: Fermeture de la boîte $boxName');
await Hive.box(boxName).close();
}
}
// Supprimer IndexedDB avec une approche plus sûre
js.context.callMethod('eval', [
'''
(function() {
return new Promise(function(resolve, reject) {
var request = indexedDB.deleteDatabase("geosector_app");
request.onsuccess = function() {
console.log("IndexedDB nettoyé avec succès");
resolve(true);
};
request.onerror = function(event) {
console.log("Erreur lors du nettoyage d'IndexedDB", event);
reject(event);
};
});
})();
'''
]);
// Attendre un peu pour s'assurer que la suppression est terminée
await Future.delayed(const Duration(milliseconds: 500));
// Réinitialiser Hive
await Hive.initFlutter();
// Réenregistrer les adaptateurs
// Note: Cette partie devrait être gérée par le code principal de l'application
debugPrint('HiveWebFix: Réinitialisation complète terminée');
} catch (e) {
debugPrint('HiveWebFix: Erreur lors de la réinitialisation complète: $e');
}
}
}

View File

@@ -0,0 +1,20 @@
/// Interface pour les fonctionnalités JavaScript
/// Importe conditionnellement dart:js pour le web ou un stub pour les autres plateformes
library js_interface;
import 'package:flutter/foundation.dart';
// Importation conditionnelle basée sur la plateforme
import 'js_stub.dart' if (dart.library.js) 'dart:js' as js;
/// Exporte le contexte JavaScript pour être utilisé dans d'autres fichiers
final context = js.context;
/// Fonction utilitaire pour évaluer du code JavaScript sur le web
/// Ne fait rien sur les plateformes non-web
dynamic evalJs(String code) {
if (kIsWeb) {
return js.context.callMethod('eval', [code]);
}
return null;
}

View File

@@ -0,0 +1,11 @@
/// Stub pour dart:js pour les plateformes non-web
/// Fournit une implémentation vide des fonctionnalités de dart:js
class JsContext {
dynamic callMethod(String method, [List<dynamic>? args]) {
// Ne fait rien sur les plateformes non-web
return null;
}
}
/// Contexte JavaScript stub
final JsContext context = JsContext();

View File

@@ -0,0 +1,164 @@
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:geolocator/geolocator.dart';
import 'package:latlong2/latlong.dart';
/// Service de géolocalisation pour gérer les permissions et l'accès à la position
class LocationService {
/// Vérifie si les services de localisation sont activés
static Future<bool> isLocationServiceEnabled() async {
// En version web, on considère que les services de localisation sont toujours activés
// car la vérification est gérée différemment par le navigateur
if (kIsWeb) {
return true;
}
return await Geolocator.isLocationServiceEnabled();
}
/// Vérifie et demande les permissions de localisation
/// Retourne true si l'autorisation est accordée, false sinon
static Future<bool> checkAndRequestPermission() async {
// En version web, on considère que les permissions sont toujours accordées
// car la gestion des permissions est différente et gérée par le navigateur
if (kIsWeb) {
return true;
}
try {
// Vérifier si les services de localisation sont activés
bool serviceEnabled = await isLocationServiceEnabled();
if (!serviceEnabled) {
// Les services de localisation ne sont pas activés, on ne peut pas demander la permission
return false;
}
// Vérifier le statut actuel de la permission
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
// Demander la permission
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
// La permission a été refusée
return false;
}
}
if (permission == LocationPermission.deniedForever) {
// La permission a été refusée définitivement
return false;
}
// La permission est accordée (whileInUse ou always)
return true;
} catch (e) {
debugPrint('Erreur lors de la vérification des permissions de localisation: $e');
// En cas d'erreur, on retourne false pour être sûr
return false;
}
}
/// Obtient la position actuelle de l'utilisateur
/// Retourne null si la position ne peut pas être obtenue
static Future<LatLng?> getCurrentPosition() async {
try {
// En version web, la géolocalisation fonctionne différemment
// et peut être bloquée par le navigateur si le site n'est pas en HTTPS
if (kIsWeb) {
try {
Position position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high,
);
return LatLng(position.latitude, position.longitude);
} catch (e) {
debugPrint('Erreur lors de l\'obtention de la position en version web: $e');
// En version web, en cas d'erreur, on peut retourner une position par défaut
// ou null selon les besoins de l'application
return null;
}
}
// Version mobile
// Vérifier si l'autorisation est accordée
bool hasPermission = await checkAndRequestPermission();
if (!hasPermission) {
return null;
}
// Obtenir la position actuelle
Position position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high,
);
return LatLng(position.latitude, position.longitude);
} catch (e) {
debugPrint('Erreur lors de l\'obtention de la position: $e');
return null;
}
}
/// Vérifie si l'application peut accéder à la position de l'utilisateur
/// Retourne un message d'erreur si l'accès n'est pas possible, null sinon
static Future<String?> getLocationErrorMessage() async {
// En version web, on considère qu'il n'y a pas d'erreur de localisation
// car la gestion des permissions est gérée par le navigateur
if (kIsWeb) {
return null;
}
try {
// Vérifier si les services de localisation sont activés
bool serviceEnabled = await isLocationServiceEnabled();
if (!serviceEnabled) {
return 'Les services de localisation sont désactivés. Veuillez les activer dans les paramètres de votre appareil.';
}
// Vérifier le statut actuel de la permission
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
return 'L\'accès à la localisation a été refusé. Cette application ne peut pas fonctionner sans cette autorisation.';
}
if (permission == LocationPermission.deniedForever) {
return 'L\'accès à la localisation a été définitivement refusé. Veuillez l\'autoriser dans les paramètres de votre appareil.';
}
return null; // Pas d'erreur
} catch (e) {
debugPrint('Erreur lors de la vérification des erreurs de localisation: $e');
// En cas d'erreur, on retourne null pour ne pas bloquer l'application
return null;
}
}
/// Ouvre les paramètres de l'application pour permettre à l'utilisateur de modifier les autorisations
static Future<void> openAppSettings() async {
// En version web, cette fonctionnalité n'est pas disponible
if (kIsWeb) {
debugPrint('Ouverture des paramètres de l\'application non disponible en version web');
return;
}
try {
await Geolocator.openAppSettings();
} catch (e) {
debugPrint('Erreur lors de l\'ouverture des paramètres de l\'application: $e');
}
}
/// Ouvre les paramètres de localisation de l'appareil
static Future<void> openLocationSettings() async {
// En version web, cette fonctionnalité n'est pas disponible
if (kIsWeb) {
debugPrint('Ouverture des paramètres de localisation non disponible en version web');
return;
}
try {
await Geolocator.openLocationSettings();
} catch (e) {
debugPrint('Erreur lors de l\'ouverture des paramètres de localisation: $e');
}
}
}

View File

@@ -0,0 +1,194 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:geosector_app/core/constants/app_keys.dart';
import 'package:geosector_app/core/repositories/passage_repository.dart';
import 'package:geosector_app/core/repositories/user_repository.dart';
/// Service pour charger et filtrer les données de passages
class PassageDataService {
final PassageRepository passageRepository;
final UserRepository userRepository;
PassageDataService({
required this.passageRepository,
required this.userRepository,
});
/// Charge les données de passage depuis Hive
///
/// [daysToShow] : Nombre de jours à afficher
/// [excludePassageTypes] : Types de passages à exclure
/// [userId] : ID de l'utilisateur pour filtrer les passages (null = utilisateur actuel)
/// [showAllPassages] : Si vrai, n'applique aucun filtrage par utilisateur
List<Map<String, dynamic>> loadPassageData({
required int daysToShow,
List<int> excludePassageTypes = const [2],
int? userId,
bool showAllPassages = false,
}) {
// Récupérer tous les passages
final passages = passageRepository.getAllPassages();
// Filtrer les passages pour exclure ceux avec fkType dans la liste d'exclusion
final filteredPassages = passages
.where((p) => !excludePassageTypes.contains(p.fkType))
.toList();
if (filteredPassages.isEmpty) {
return [];
}
// Déterminer si on filtre par utilisateur ou si on prend tous les passages
final passagesToUse = showAllPassages
? filteredPassages
: _filterPassagesByUser(filteredPassages, userId);
if (passagesToUse.isEmpty) {
debugPrint('Aucun passage trouvé après filtrage');
return [];
}
// Trouver la date du passage le plus récent
passagesToUse.sort((a, b) => b.passedAt.compareTo(a.passedAt));
final DateTime referenceDate = passagesToUse.first.passedAt;
debugPrint(
'Date de référence pour le graphique: ${DateFormat('dd/MM/yyyy').format(referenceDate)}');
// Définir la date de début (N jours avant la date de référence)
final startDate = referenceDate.subtract(Duration(days: daysToShow - 1));
debugPrint(
'Date de début pour le graphique: ${DateFormat('dd/MM/yyyy').format(startDate)}');
debugPrint(
'Plage de dates du graphique: ${DateFormat('dd/MM/yyyy').format(startDate)} - ${DateFormat('dd/MM/yyyy').format(referenceDate)}');
// Regrouper les passages par date et type
final Map<String, Map<int, int>> passagesByDateAndType = {};
// Initialiser le dictionnaire avec les N derniers jours
for (int i = daysToShow - 1; i >= 0; i--) {
final date = referenceDate.subtract(Duration(days: i));
final dateStr =
'${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
passagesByDateAndType[dateStr] = {};
}
// Ajouter tous les types de passage possibles pour chaque date
for (final dateStr in passagesByDateAndType.keys) {
for (final typeId in AppKeys.typesPassages.keys) {
// Exclure les types dans la liste d'exclusion
if (!excludePassageTypes.contains(typeId)) {
passagesByDateAndType[dateStr]![typeId] = 0;
}
}
}
// Parcourir les passages et les regrouper par date et type
for (final passage in passagesToUse) {
if (passage.passedAt
.isAfter(startDate.subtract(const Duration(days: 1))) &&
passage.passedAt
.isBefore(referenceDate.add(const Duration(days: 1)))) {
final dateStr =
'${passage.passedAt.year}-${passage.passedAt.month.toString().padLeft(2, '0')}-${passage.passedAt.day.toString().padLeft(2, '0')}';
final typeId = passage.fkType;
// Vérifier que le type n'est pas exclu
if (!excludePassageTypes.contains(typeId)) {
// Si la date existe dans notre dictionnaire, mettre à jour le compteur
if (passagesByDateAndType.containsKey(dateStr)) {
if (!passagesByDateAndType[dateStr]!.containsKey(typeId)) {
passagesByDateAndType[dateStr]![typeId] = 0;
}
passagesByDateAndType[dateStr]![typeId] =
(passagesByDateAndType[dateStr]![typeId] ?? 0) + 1;
}
}
}
}
// Convertir les données au format attendu par le graphique
final List<Map<String, dynamic>> result = [];
passagesByDateAndType.forEach((dateStr, typesCounts) {
typesCounts.forEach((typeId, count) {
result.add({
'date': dateStr,
'type_passage': typeId,
'nb': count,
});
});
});
return result;
}
/// Filtre les passages par utilisateur
List<dynamic> _filterPassagesByUser(List<dynamic> passages, int? userId) {
// Récupérer l'ID de l'utilisateur actuel si nécessaire
final int? currentUserId = userId ?? userRepository.getCurrentUser()?.id;
// Filtrer les passages pour l'utilisateur actuel
final userPassages = passages
.where((p) => currentUserId == null || p.fkUser == currentUserId)
.toList();
if (userPassages.isEmpty) {
debugPrint('Aucun passage trouvé pour l\'utilisateur $currentUserId');
}
return userPassages;
}
/// Charge et prépare les données pour le graphique en camembert
///
/// [excludePassageTypes] : Types de passages à exclure
/// [userId] : ID de l'utilisateur pour filtrer les passages (null = utilisateur actuel)
/// [showAllPassages] : Si vrai, n'applique aucun filtrage par utilisateur
Map<int, int> loadPassageDataForPieChart({
List<int> excludePassageTypes = const [2],
int? userId,
bool showAllPassages = false,
}) {
// Récupérer tous les passages
final passages = passageRepository.getAllPassages();
// Filtrer les passages pour exclure ceux avec fkType dans la liste d'exclusion
final filteredPassages = passages
.where((p) => !excludePassageTypes.contains(p.fkType))
.toList();
if (filteredPassages.isEmpty) {
return {};
}
// Déterminer si on filtre par utilisateur ou si on prend tous les passages
final passagesToUse = showAllPassages
? filteredPassages
: _filterPassagesByUser(filteredPassages, userId);
if (passagesToUse.isEmpty) {
debugPrint('Aucun passage trouvé après filtrage');
return {};
}
// Compter les passages par type
final Map<int, int> passagesByType = {};
// Initialiser les compteurs pour tous les types de passage
for (final typeId in AppKeys.typesPassages.keys) {
// Exclure les types dans la liste d'exclusion
if (!excludePassageTypes.contains(typeId)) {
passagesByType[typeId] = 0;
}
}
// Compter les passages par type
for (final passage in passagesToUse) {
final typeId = passage.fkType;
if (!excludePassageTypes.contains(typeId)) {
passagesByType[typeId] = (passagesByType[typeId] ?? 0) + 1;
}
}
return passagesByType;
}
}

View File

@@ -0,0 +1,96 @@
import 'dart:async';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:geosector_app/core/repositories/user_repository.dart';
class SyncService {
final UserRepository _userRepository;
StreamSubscription? _connectivitySubscription;
Timer? _periodicSyncTimer;
bool _isSyncing = false;
final Duration _syncInterval = const Duration(minutes: 15);
SyncService({
required UserRepository userRepository,
}) : _userRepository = userRepository {
_initConnectivityListener();
_initPeriodicSync();
}
// Initialiser l'écouteur de connectivité
void _initConnectivityListener() {
_connectivitySubscription = Connectivity()
.onConnectivityChanged
.listen((List<ConnectivityResult> results) {
// Vérifier si au moins un type de connexion est disponible
if (results.any((result) => result != ConnectivityResult.none)) {
// Lorsque la connexion est rétablie, déclencher une synchronisation
syncAll();
}
});
}
// Initialiser la synchronisation périodique
void _initPeriodicSync() {
_periodicSyncTimer = Timer.periodic(_syncInterval, (timer) {
syncAll();
});
}
// Synchroniser toutes les données
Future<void> syncAll() async {
if (_isSyncing) return;
_isSyncing = true;
try {
// Synchroniser les utilisateurs
await _userRepository.syncAllUsers();
} catch (e) {
// Gérer les erreurs de synchronisation
print('Erreur lors de la synchronisation: $e');
} finally {
_isSyncing = false;
}
}
// Synchroniser uniquement les données d'un utilisateur spécifique
Future<void> syncUserData(int userId) async {
try {
// Cette méthode pourrait être étendue à l'avenir pour synchroniser d'autres données utilisateur
await _userRepository.refreshFromServer();
} catch (e) {
print('Erreur lors de la synchronisation des données utilisateur: $e');
}
}
// Forcer le rafraîchissement depuis le serveur
Future<void> forceRefresh() async {
if (_isSyncing) return;
_isSyncing = true;
try {
// Rafraîchir depuis le serveur
await _userRepository.refreshFromServer();
} catch (e) {
print('Erreur lors du rafraîchissement forcé: $e');
} finally {
_isSyncing = false;
}
}
// Obtenir l'état de synchronisation
Map<String, dynamic> getSyncStatus() {
return {
'isSyncing': _isSyncing,
};
}
// Nettoyer les ressources
void dispose() {
_connectivitySubscription?.cancel();
_periodicSyncTimer?.cancel();
}
}