# GEOSECTOR v2.0 🚒 **Application de gestion des distributions de calendriers par secteurs gĂ©ographiques pour les amicales de pompiers** --- ## 🎯 Vue d'ensemble GEOSECTOR est une solution complĂšte dĂ©veloppĂ©e en Flutter qui rĂ©volutionne la gestion des campagnes de distribution de calendriers pour les amicales de pompiers. L'application combine gĂ©olocalisation, gestion multi-rĂŽles et synchronisation en temps rĂ©el pour optimiser les tournĂ©es et maximiser l'efficacitĂ© des Ă©quipes. ### 🏆 Points forts de la v2.0 - **Architecture moderne** sans Provider, basĂ©e sur l'injection de dĂ©pendances - **RĂ©activitĂ© native** avec ValueListenableBuilder et Hive - **Interface adaptative** selon les rĂŽles utilisateur - **Performance optimisĂ©e** avec un ApiService singleton - **Gestion avancĂ©e des permissions** multi-niveaux --- ## 📋 Table des matiĂšres 1. [FonctionnalitĂ©s](#-fonctionnalitĂ©s) 2. [Architecture technique](#-architecture-technique) 3. [Installation](#-installation-et-configuration) 4. [ModĂšles de donnĂ©es](#-modĂšles-de-donnĂ©es) 5. [Architecture des composants](#-architecture-des-composants) 6. [Gestion des rĂŽles](#-gestion-des-rĂŽles) 7. [Interface utilisateur](#-interface-utilisateur) 8. [API et synchronisation](#-api-et-synchronisation) 9. [Cartes et gĂ©olocalisation](#-cartes-et-gĂ©olocalisation) 10. [Tests et qualitĂ©](#-tests-et-qualitĂ©) 11. [DĂ©ploiement](#-dĂ©ploiement) 12. [Migration v1 → v2](#-migration-v1--v2) --- ## 🚀 FonctionnalitĂ©s ### 🎯 FonctionnalitĂ©s mĂ©tier #### Pour les **Distributeurs** (RĂŽle 1) - ✅ Visualisation des secteurs assignĂ©s sur carte interactive - ✅ Suivi GPS en temps rĂ©el des tournĂ©es - ✅ Enregistrement des passages avec gĂ©olocalisation - ✅ Gestion des stocks de calendriers - ✅ Historique des distributions - ✅ Chat intĂ©grĂ© avec l'Ă©quipe #### Pour les **Admins Amicale** (RĂŽle 2) - ✅ Gestion de leur amicale (informations, coordonnĂ©es) - ✅ Gestion des membres de l'amicale - ✅ Consultation des statistiques de l'amicale - ✅ Attribution des secteurs aux membres - ✅ Suivi des performances Ă©quipe #### Pour les **Super Admins** (RĂŽle 3+) - ✅ Gestion globale multi-amicales - ✅ Administration des utilisateurs et permissions - ✅ Configuration des paramĂštres systĂšme - ✅ Analytics avancĂ©es et reporting - ✅ Gestion des secteurs gĂ©ographiques ### 🔧 FonctionnalitĂ©s techniques - **đŸ—ș Cartographie avancĂ©e** : Flutter Map avec tuiles Mapbox - **📍 GĂ©olocalisation prĂ©cise** : Suivi GPS des Ă©quipes - **đŸ’Ÿ Stockage hybride** : Cache local Hive + synchronisation cloud - **💬 Communication** : Chat MQTT en temps rĂ©el - **🔐 SĂ©curitĂ©** : Authentification JWT + gestion fine des permissions - **đŸ“± Multi-plateforme** : iOS, Android, Web - **🌐 Mode hors-ligne** : Fonctionnement dĂ©gradĂ© sans connexion --- ## đŸ—ïž Architecture technique ### Stack technologique | Composant | Technologie | Version | Usage | |-----------|-------------|---------|-------| | **Framework** | Flutter | 3.32+ | Interface multi-plateforme | | **Langage** | Dart | 3.0+ | Logique applicative | | **Navigation** | GoRouter | 12.1.3 | Routing dĂ©claratif | | **Stockage local** | Hive | 2.2.3 | Base de donnĂ©es NoSQL locale | | **RĂ©activitĂ©** | ValueListenableBuilder | Native | Écoute des changements Hive | | **HTTP** | Dio | 5.4.0 | Client HTTP avec intercepteurs | | **Cartes** | Flutter Map | 6.1.0 | Rendu cartographique | | **GĂ©olocalisation** | Geolocator | 10.1.0 | Services de localisation | | **Chat** | MQTT5 Client | 4.2.0 | Messagerie temps rĂ©el | | **UI** | Material Design 3 | Native | Composants d'interface | ### đŸ›ïž Architecture en couches ```mermaid graph TD A[UI Layer - Widgets] --> B[Repository Layer - Business Logic] B --> C[Data Layer - Hive + API] A1[ValueListenableBuilder] --> A A2[Custom Widgets] --> A A3[Pages] --> A B1[UserRepository] --> B B2[AmicaleRepository] --> B B3[MembreRepository] --> B C1[Hive Boxes] --> C C2[API Service Singleton] --> C C3[Local Storage] --> C ``` ### 📁 Structure du projet ``` app/ ├── lib/ │ ├── core/ # Couche centrale │ │ ├── constants/ # Constantes globales │ │ │ ├── app_keys.dart # ClĂ©s des Box Hive │ │ │ └── api_endpoints.dart # Endpoints API │ │ ├── data/ │ │ │ └── models/ # ModĂšles Hive │ │ │ ├── user_model.dart # @HiveType(typeId: 3) │ │ │ ├── amicale_model.dart # @HiveType(typeId: 4) │ │ │ └── membre_model.dart # @HiveType(typeId: 5) │ │ ├── repositories/ # Logique mĂ©tier │ │ │ ├── user_repository.dart │ │ │ ├── amicale_repository.dart │ │ │ └── membre_repository.dart │ │ ├── services/ # Services externes │ │ │ ├── api_service.dart # HTTP Singleton │ │ │ ├── chat_service.dart # MQTT │ │ │ └── location_service.dart # GPS │ │ └── utils/ # Utilitaires │ │ ├── validators.dart │ │ └── formatters.dart │ ├── presentation/ # Interface utilisateur │ │ ├── admin/ # Pages administrateur │ │ │ ├── admin_dashboard_page.dart │ │ │ ├── admin_amicale_page.dart │ │ │ └── admin_statistics_page.dart │ │ ├── user/ # Pages utilisateur │ │ │ ├── user_dashboard_page.dart │ │ │ ├── map_page.dart │ │ │ └── distribution_page.dart │ │ ├── widgets/ # Composants rĂ©utilisables │ │ │ ├── tables/ │ │ │ │ ├── amicale_table_widget.dart │ │ │ │ ├── amicale_row_widget.dart │ │ │ │ ├── membre_table_widget.dart │ │ │ │ └── membre_row_widget.dart │ │ │ ├── forms/ │ │ │ │ ├── amicale_form.dart │ │ │ │ └── custom_text_field.dart │ │ │ └── common/ │ │ │ ├── dashboard_layout.dart │ │ │ └── loading_widget.dart │ │ └── theme/ │ │ └── app_theme.dart │ ├── app.dart # Configuration app │ └── main.dart # Point d'entrĂ©e ├── assets/ # Ressources statiques │ ├── images/ │ ├── icons/ │ └── fonts/ ├── test/ # Tests unitaires ├── integration_test/ # Tests d'intĂ©gration └── docs/ # Documentation ``` --- ## 🚀 Installation et configuration ### PrĂ©requis systĂšme - **Flutter SDK** : 3.32 ou supĂ©rieur - **Dart SDK** : 3.0 ou supĂ©rieur - **IDE** : Android Studio, VS Code, ou IntelliJ - **Environnement** : - Android : SDK 21+ (Android 5.0+) - iOS : iOS 12.0+ - Web : Navigateurs modernes ### 🔧 Installation pas Ă  pas #### 1. Clonage et dĂ©pendances ```bash # Cloner le repository git clone https://github.com/your-org/geosector.git cd geosector/app # Installer les dĂ©pendances Flutter flutter pub get # VĂ©rifier l'installation flutter doctor ``` #### 2. Configuration de l'environnement ```bash # CrĂ©er le fichier d'environnement cp .env.example .env ``` **Fichier `.env`** : ```env # API Configuration API_BASE_URL=https://api.geosector.com API_TIMEOUT=30000 # Mapbox Configuration MAPBOX_ACCESS_TOKEN=pk.eyJ1IjoieW91ci11c2VybmFtZSIsImEiOiJjbGV5... # MQTT Configuration MQTT_BROKER_URL=mqtt://broker.geosector.com MQTT_PORT=1883 MQTT_USERNAME=geosector_client MQTT_PASSWORD=your_mqtt_password # Environment ENVIRONMENT=development DEBUG_MODE=true ENABLE_LOGGING=true ``` #### 3. GĂ©nĂ©ration du code Hive ```bash # GĂ©nĂ©rer les adaptateurs Hive flutter packages pub run build_runner build --delete-conflicting-outputs # En mode watch pour le dĂ©veloppement flutter packages pub run build_runner watch ``` #### 4. Configuration des assets **`pubspec.yaml`** : ```yaml flutter: assets: - assets/images/ - assets/icons/ - .env fonts: - family: Roboto fonts: - asset: assets/fonts/Roboto-Regular.ttf - asset: assets/fonts/Roboto-Bold.ttf weight: 700 ``` #### 5. Lancement de l'application ```bash # Debug mode flutter run # Release mode flutter run --release # Web flutter run -d web-server --web-port 8080 # Specific device flutter run -d ``` ### 🔐 Configuration des clĂ©s API #### Mapbox (Cartographie) 1. CrĂ©er un compte sur [Mapbox](https://www.mapbox.com/) 2. GĂ©nĂ©rer un token d'accĂšs 3. Ajouter le token dans `.env` #### Configuration MQTT (Chat) 1. Configurer votre broker MQTT 2. CrĂ©er les credentials 3. Tester la connexion --- ## đŸ—„ïž ModĂšles de donnĂ©es ### đŸ‘€ UserModel ```dart @HiveType(typeId: 3) class UserModel extends HiveObject { @HiveField(0) final int id; @HiveField(1) final String username; @HiveField(2) final String email; @HiveField(3) final int role; // 1: user, 2: admin amicale, 3+: super admin @HiveField(4) final int? fkEntite; // ID de l'amicale associĂ©e @HiveField(5) final String firstName; @HiveField(6) final String name; @HiveField(7) final DateTime? lastLogin; @HiveField(8) final bool isActive; UserModel({ required this.id, required this.username, required this.email, required this.role, this.fkEntite, required this.firstName, required this.name, this.lastLogin, this.isActive = true, }); // Factory constructors factory UserModel.fromJson(Map json) { return UserModel( id: json['id'] is String ? int.parse(json['id']) : json['id'], username: json['username'] ?? '', email: json['email'] ?? '', role: json['role'] is String ? int.parse(json['role']) : json['role'], fkEntite: json['fk_entite'] != null ? (json['fk_entite'] is String ? int.parse(json['fk_entite']) : json['fk_entite']) : null, firstName: json['first_name'] ?? '', name: json['name'] ?? '', lastLogin: json['last_login'] != null ? DateTime.parse(json['last_login']) : null, isActive: json['is_active'] ?? true, ); } Map toJson() { return { 'id': id, 'username': username, 'email': email, 'role': role, 'fk_entite': fkEntite, 'first_name': firstName, 'name': name, 'last_login': lastLogin?.toIso8601String(), 'is_active': isActive, }; } // Helpers bool get isUser => role == 1; bool get isAdminAmicale => role == 2; bool get isSuperAdmin => role >= 3; String get fullName => '$firstName $name'; String get roleLabel { switch (role) { case 1: return 'Utilisateur'; case 2: return 'Admin Amicale'; case 3: return 'Super Admin'; default: return 'Inconnu'; } } } ``` ### 🏱 AmicaleModel ```dart @HiveType(typeId: 4) class AmicaleModel extends HiveObject { @HiveField(0) final int id; @HiveField(1) final String name; @HiveField(2) final String? adresse1; @HiveField(3) final String? adresse2; @HiveField(4) final String codePostal; @HiveField(5) final String ville; @HiveField(6) final int? fkRegion; @HiveField(7) final String? libRegion; @HiveField(8) final String? phone; @HiveField(9) final String? mobile; @HiveField(10) final String? email; @HiveField(11) final String? gpsLat; @HiveField(12) final String? gpsLng; @HiveField(13) final bool chkDemo; @HiveField(14) final bool chkCopieMailRecu; @HiveField(15) final bool chkAcceptSms; @HiveField(16) final bool chkActive; @HiveField(17) final bool chkStripe; @HiveField(18) final String? stripeId; @HiveField(19) final DateTime? dateCreation; @HiveField(20) final DateTime? dateModification; AmicaleModel({ required this.id, required this.name, this.adresse1, this.adresse2, required this.codePostal, required this.ville, this.fkRegion, this.libRegion, this.phone, this.mobile, this.email, this.gpsLat, this.gpsLng, this.chkDemo = false, this.chkCopieMailRecu = false, this.chkAcceptSms = false, this.chkActive = true, this.chkStripe = false, this.stripeId, this.dateCreation, this.dateModification, }); // Factory et mĂ©thodes utilitaires factory AmicaleModel.fromJson(Map json) { return AmicaleModel( id: json['id'] is String ? int.parse(json['id']) : json['id'], name: json['name'] ?? '', adresse1: json['adresse1'], adresse2: json['adresse2'], codePostal: json['code_postal'] ?? '', ville: json['ville'] ?? '', fkRegion: json['fk_region'] != null ? (json['fk_region'] is String ? int.parse(json['fk_region']) : json['fk_region']) : null, libRegion: json['lib_region'], phone: json['phone'], mobile: json['mobile'], email: json['email'], gpsLat: json['gps_lat'], gpsLng: json['gps_lng'], chkDemo: json['chk_demo'] == 1 || json['chk_demo'] == true, chkCopieMailRecu: json['chk_copie_mail_recu'] == 1 || json['chk_copie_mail_recu'] == true, chkAcceptSms: json['chk_accept_sms'] == 1 || json['chk_accept_sms'] == true, chkActive: json['chk_active'] == 1 || json['chk_active'] == true, chkStripe: json['chk_stripe'] == 1 || json['chk_stripe'] == true, stripeId: json['stripe_id'], dateCreation: json['date_creation'] != null ? DateTime.parse(json['date_creation']) : null, dateModification: json['date_modification'] != null ? DateTime.parse(json['date_modification']) : null, ); } // Getters utilitaires String get adresseComplete { final parts = []; if (adresse1?.isNotEmpty == true) parts.add(adresse1!); if (adresse2?.isNotEmpty == true) parts.add(adresse2!); parts.add('$codePostal $ville'); return parts.join(', '); } bool get hasGpsCoordinates => gpsLat?.isNotEmpty == true && gpsLng?.isNotEmpty == true; double? get latitude => hasGpsCoordinates ? double.tryParse(gpsLat!) : null; double? get longitude => hasGpsCoordinates ? double.tryParse(gpsLng!) : null; String get statusLabel => chkActive ? 'Actif' : 'Inactif'; } ``` ### đŸ‘„ MembreModel ```dart @HiveType(typeId: 5) class MembreModel extends HiveObject { @HiveField(0) final int id; @HiveField(1) final int fkRole; @HiveField(2) final int fkTitre; @HiveField(3) final String firstName; @HiveField(4) final String? sectName; @HiveField(5) final DateTime? dateNaissance; @HiveField(6) final DateTime? dateEmbauche; @HiveField(7) final int chkActive; @HiveField(8) final String name; @HiveField(9) final String username; @HiveField(10) final String email; @HiveField(11) final int fkEntite; // Association Ă  l'amicale @HiveField(12) final String? phone; @HiveField(13) final String? mobile; @HiveField(14) final DateTime? dateCreation; @HiveField(15) final DateTime? dateModification; MembreModel({ required this.id, required this.fkRole, required this.fkTitre, required this.firstName, this.sectName, this.dateNaissance, this.dateEmbauche, required this.chkActive, required this.name, required this.username, required this.email, required this.fkEntite, this.phone, this.mobile, this.dateCreation, this.dateModification, }); // Getters utilitaires String get fullName => '$firstName $name'; bool get isActive => chkActive == 1; String get statusLabel => isActive ? 'Actif' : 'Inactif'; String get roleLabel { switch (fkRole) { case 1: return 'Distributeur'; case 2: return 'Admin Amicale'; case 3: return 'Super Admin'; default: return 'RĂŽle $fkRole'; } } int? get age { if (dateNaissance == null) return null; final now = DateTime.now(); final age = now.year - dateNaissance!.year; if (now.month < dateNaissance!.month || (now.month == dateNaissance!.month && now.day < dateNaissance!.day)) { return age - 1; } return age; } Duration? get anciennete { if (dateEmbauche == null) return null; return DateTime.now().difference(dateEmbauche!); } } ``` --- ## đŸ—ïž Architecture des composants ### 📊 Repository Pattern #### UserRepository ```dart class UserRepository extends ChangeNotifier { final ApiService _apiService; UserRepository(this._apiService); // AccĂšs Ă  la Box Hive Box getUsersBox() { if (!Hive.isBoxOpen(AppKeys.usersBoxName)) { throw Exception('La boĂźte utilisateurs n\'est pas ouverte'); } return Hive.box(AppKeys.usersBoxName); } // Gestion de l'utilisateur connectĂ© UserModel? getCurrentUser() { try { return getUsersBox().get('current_user'); } catch (e) { debugPrint('Erreur rĂ©cupĂ©ration utilisateur courant: $e'); return null; } } Future setCurrentUser(UserModel user) async { await getUsersBox().put('current_user', user); notifyListeners(); } // Gestion des rĂŽles int getUserRole() { final user = getCurrentUser(); return user?.role ?? 0; } bool isAuthenticated() => getCurrentUser() != null; bool isUser() => getUserRole() == 1; bool isAdminAmicale() => getUserRole() == 2; bool isSuperAdmin() => getUserRole() >= 3; // CRUD Operations List getAllUsers() { return getUsersBox().values.toList(); } UserModel? getUserById(int id) { return getUsersBox().values.firstWhere( (user) => user.id == id, orElse: () => throw StateError('Utilisateur non trouvĂ©'), ); } List getUsersByAmicale(int amicaleId) { return getUsersBox().values .where((user) => user.fkEntite == amicaleId) .toList(); } Future saveUser(UserModel user) async { await getUsersBox().put(user.id, user); notifyListeners(); return user; } Future deleteUser(int id) async { await getUsersBox().delete(id); notifyListeners(); } // API Integration Future authenticateUser(String username, String password) async { try { final response = await _apiService.post('/auth/login', data: { 'username': username, 'password': password, }); if (response.statusCode == 200) { final userData = response.data['user']; final user = UserModel.fromJson(userData); await setCurrentUser(user); return user; } } catch (e) { debugPrint('Erreur authentification: $e'); } return null; } Future logout() async { await getUsersBox().delete('current_user'); notifyListeners(); } Future> syncUsersFromApi() async { try { final response = await _apiService.get('/users'); if (response.statusCode == 200) { final usersData = response.data['users'] as List; final users = usersData.map((userData) => UserModel.fromJson(userData)).toList(); // Clear and update local storage await getUsersBox().clear(); for (final user in users) { await getUsersBox().put(user.id, user); } notifyListeners(); return users; } } catch (e) { debugPrint('Erreur sync utilisateurs: $e'); } return getAllUsers(); // Fallback to local data } } ``` #### AmicaleRepository ```dart class AmicaleRepository extends ChangeNotifier { final ApiService _apiService; AmicaleRepository(this._apiService); // Box access Box getAmicalesBox() { if (!Hive.isBoxOpen(AppKeys.amicaleBoxName)) { throw Exception('La boĂźte amicales n\'est pas ouverte'); } return Hive.box(AppKeys.amicaleBoxName); } // CRUD Operations List getAllAmicales() { return getAmicalesBox().values.toList(); } AmicaleModel? getAmicaleById(int id) { return getAmicalesBox().get(id); } List getActiveAmicales() { return getAmicalesBox().values .where((amicale) => amicale.chkActive) .toList(); } List searchAmicalesByName(String query) { if (query.isEmpty) return getAllAmicales(); final lowercaseQuery = query.toLowerCase(); return getAmicalesBox().values .where((amicale) => amicale.name.toLowerCase().contains(lowercaseQuery)) .toList(); } List getAmicalesByRegion(int regionId) { return getAmicalesBox().values .where((amicale) => amicale.fkRegion == regionId) .toList(); } Future saveAmicale(AmicaleModel amicale) async { await getAmicalesBox().put(amicale.id, amicale); notifyListeners(); return amicale; } Future deleteAmicale(int id) async { await getAmicalesBox().delete(id); notifyListeners(); } // API Integration Future> fetchAmicalesFromApi() async { try { final response = await _apiService.get('/amicales'); if (response.statusCode == 200) { await processAmicalesData(response.data); return getAllAmicales(); } } catch (e) { debugPrint('Erreur rĂ©cupĂ©ration amicales API: $e'); } return getAllAmicales(); // Fallback to local data } Future updateAmicaleViaApi(AmicaleModel amicale) async { try { final response = await _apiService.put( '/amicales/${amicale.id}', data: amicale.toJson(), ); if (response.statusCode == 200) { final updatedAmicaleData = response.data; final updatedAmicale = AmicaleModel.fromJson(updatedAmicaleData); await saveAmicale(updatedAmicale); return updatedAmicale; } } catch (e) { debugPrint('Erreur mise Ă  jour amicale: $e'); } return null; } Future processAmicalesData(dynamic amicalesData) async { try { if (amicalesData == null) return; await getAmicalesBox().clear(); int count = 0; if (amicalesData is List) { for (final amicaleData in amicalesData) { try { final amicale = AmicaleModel.fromJson(amicaleData); await getAmicalesBox().put(amicale.id, amicale); count++; } catch (e) { debugPrint('Erreur traitement amicale: $e'); } } } else if (amicalesData is Map && amicalesData.containsKey('data')) { final amicalesList = amicalesData['data'] as List; for (final amicaleData in amicalesList) { try { final amicale = AmicaleModel.fromJson(amicaleData); await getAmicalesBox().put(amicale.id, amicale); count++; } catch (e) { debugPrint('Erreur traitement amicale: $e'); } } } else if (amicalesData is Map) { try { final Map amicaleMap = {}; amicalesData.forEach((key, value) { if (key is String) { amicaleMap[key] = value; } }); final amicale = AmicaleModel.fromJson(amicaleMap); await getAmicalesBox().put(amicale.id, amicale); count++; } catch (e) { debugPrint('Erreur traitement amicale unique: $e'); } } debugPrint('$count amicales traitĂ©es et stockĂ©es'); notifyListeners(); } catch (e) { debugPrint('Erreur processAmicalesData: $e'); } } } ``` ### 🔄 Injection de dĂ©pendances #### Configuration dans main.dart ```dart void main() async { WidgetsFlutterBinding.ensureInitialized(); // Initialiser Hive await Hive.initFlutter(); // Enregistrer les adaptateurs Hive Hive.registerAdapter(UserModelAdapter()); Hive.registerAdapter(AmicaleModelAdapter()); Hive.registerAdapter(MembreModelAdapter()); // Ouvrir les boxes await Hive.openBox(AppKeys.usersBoxName); await Hive.openBox(AppKeys.amicaleBoxName); await Hive.openBox(AppKeys.membresBoxName); // Initialiser ApiService ApiService.initialize( baseUrl: 'https://api.geosector.com', defaultHeaders: { 'Content-Type': 'application/json', 'Accept': 'application/json', }, ); // CrĂ©er les repositories final apiService = ApiService.instance; final userRepository = UserRepository(apiService); final amicaleRepository = AmicaleRepository(apiService); final membreRepository = MembreRepository(apiService); runApp(MyApp( userRepository: userRepository, amicaleRepository: amicaleRepository, membreRepository: membreRepository, )); } class MyApp extends StatelessWidget { final UserRepository userRepository; final AmicaleRepository amicaleRepository; final MembreRepository membreRepository; const MyApp({ super.key, required this.userRepository, required this.amicaleRepository, required this.membreRepository, }); @override Widget build(BuildContext context) { return MaterialApp.router( title: 'GEOSECTOR', theme: AppTheme.lightTheme, routerConfig: AppRouter.createRouter( userRepository: userRepository, amicaleRepository: amicaleRepository, membreRepository: membreRepository, ), ); } } ``` ### 🔌 ValueListenableBuilder Pattern ```dart class AdminAmicalePage extends StatefulWidget { final UserRepository userRepository; final AmicaleRepository amicaleRepository; final MembreRepository membreRepository; const AdminAmicalePage({ super.key, required this.userRepository, required this.amicaleRepository, required this.membreRepository, }); @override State createState() => _AdminAmicalePageState(); } class _AdminAmicalePageState extends State { UserModel? _currentUser; String? _errorMessage; @override void initState() { super.initState(); _loadCurrentUser(); } void _loadCurrentUser() { final currentUser = widget.userRepository.getCurrentUser(); if (currentUser == null) { setState(() { _errorMessage = 'Utilisateur non connectĂ©'; }); return; } if (currentUser.fkEntite == null) { setState(() { _errorMessage = 'Utilisateur non associĂ© Ă  une amicale'; }); return; } setState(() { _currentUser = currentUser; _errorMessage = null; }); } @override Widget build(BuildContext context) { if (_errorMessage != null) { return _buildErrorWidget(); } if (_currentUser?.fkEntite == null) { return _buildLoadingWidget(); } return Scaffold( body: ValueListenableBuilder>( valueListenable: widget.amicaleRepository.getAmicalesBox().listenable(), builder: (context, amicalesBox, child) { final amicale = amicalesBox.get(_currentUser!.fkEntite!); if (amicale == null) { return _buildAmicaleNotFoundWidget(); } return ValueListenableBuilder>( valueListenable: widget.membreRepository.getMembresBox().listenable(), builder: (context, membresBox, child) { final membres = membresBox.values .where((membre) => membre.fkEntite == _currentUser!.fkEntite) .toList(); return _buildContent(amicale, membres); }, ); }, ), ); } Widget _buildContent(AmicaleModel amicale, List membres) { return Column( children: [ // Section Amicale Expanded( flex: 1, child: AmicaleTableWidget( amicales: [amicale], amicaleRepository: widget.amicaleRepository, userRepository: widget.userRepository, showActionsColumn: false, // Admin amicale ne voit pas les actions ), ), const SizedBox(height: 24), // Section Membres Expanded( flex: 2, child: MembreTableWidget( membres: membres, membreRepository: widget.membreRepository, onEdit: _handleEditMembre, onDelete: null, // Pas de suppression pour admin amicale ), ), ], ); } Widget _buildErrorWidget() { return Scaffold( body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.error_outline, size: 64, color: Theme.of(context).colorScheme.error, ), const SizedBox(height: 16), Text( _errorMessage!, style: Theme.of(context).textTheme.titleLarge, textAlign: TextAlign.center, ), ], ), ), ); } Widget _buildLoadingWidget() { return const Scaffold( body: Center( child: CircularProgressIndicator(), ), ); } Widget _buildAmicaleNotFoundWidget() { return Scaffold( body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.business_outlined, size: 64, color: Theme.of(context).colorScheme.primary.withOpacity(0.7), ), const SizedBox(height: 16), Text( 'Amicale non trouvĂ©e', style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 8), Text( 'L\'amicale associĂ©e Ă  votre compte n\'existe plus.', textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodyLarge, ), ], ), ), ); } void _handleEditMembre(MembreModel membre) { // Navigation vers page d'Ă©dition avec injection des repositories Navigator.of(context).push( MaterialPageRoute( builder: (context) => EditMembrePage( membre: membre, membreRepository: widget.membreRepository, userRepository: widget.userRepository, ), ), ); } } ``` --- ## 🔐 Gestion des rĂŽles ### 📋 SystĂšme de permissions | RĂŽle | Niveau | Permissions | Interface | |------|--------|-------------|-----------| | **Distributeur** | 1 | ‱ Voir ses secteurs
‱ Enregistrer passages
‱ Chat Ă©quipe | Dashboard simplifiĂ© | | **Admin Amicale** | 2 | ‱ GĂ©rer son amicale
‱ GĂ©rer ses membres
‱ Statistiques amicale
‱ Attribution secteurs | Interface admin limitĂ©e | | **Super Admin** | 3+ | ‱ Gestion globale
‱ Multi-amicales
‱ Configuration systùme
‱ Analytics avancĂ©es | Interface admin complĂšte | ### đŸ›Ąïž Middleware de permissions ```dart class PermissionMiddleware { static bool canAccessRoute(String route, UserModel? user) { if (user == null) return false; switch (route) { case '/admin': return user.role >= 2; case '/super-admin': return user.role >= 3; case '/user': return user.role >= 1; default: return true; } } static bool canEditAmicale(UserModel? user, AmicaleModel? amicale) { if (user == null || amicale == null) return false; // Super admin peut tout modifier if (user.isSuperAdmin) return true; // Admin amicale peut modifier seulement son amicale if (user.isAdminAmicale) { return user.fkEntite == amicale.id; } return false; } static bool canDeleteMembre(UserModel? user, MembreModel? membre) { if (user == null || membre == null) return false; // Seul super admin peut supprimer return user.isSuperAdmin; } } ``` ### 🎹 Interface adaptative ```dart class AdaptiveUI { static Widget buildAmicaleActions(UserModel user, AmicaleModel amicale) { if (user.isSuperAdmin) { return Row( children: [ IconButton( icon: const Icon(Icons.edit), onPressed: () => _editAmicale(amicale), ), IconButton( icon: const Icon(Icons.delete), onPressed: () => _deleteAmicale(amicale), ), ], ); } else if (user.isAdminAmicale && user.fkEntite == amicale.id) { return IconButton( icon: const Icon(Icons.edit), onPressed: () => _editAmicale(amicale), ); } return const SizedBox.shrink(); } static List buildNavigationItems(UserModel user) { final baseItems = [ const NavigationDestination( icon: Icon(Icons.dashboard), label: 'Tableau de bord', ), ]; if (user.isAdminAmicale) { baseItems.addAll([ const NavigationDestination( icon: Icon(Icons.business), label: 'Mon Amicale', ), const NavigationDestination( icon: Icon(Icons.people), label: 'Membres', ), ]); } if (user.isSuperAdmin) { baseItems.addAll([ const NavigationDestination( icon: Icon(Icons.admin_panel_settings), label: 'Administration', ), const NavigationDestination( icon: Icon(Icons.analytics), label: 'Analytics', ), ]); } return baseItems; } } ``` --- ## 🌐 API et synchronisation ### 🔧 ApiService Singleton ```dart class ApiService { static ApiService? _instance; late Dio _dio; static ApiService get instance { if (_instance == null) { throw Exception('ApiService not initialized. Call initialize() first.'); } return _instance!; } static void initialize({ required String baseUrl, String? authToken, Map? defaultHeaders, Duration? connectTimeout, Duration? receiveTimeout, }) { _instance = ApiService._( baseUrl: baseUrl, authToken: authToken, defaultHeaders: defaultHeaders, connectTimeout: connectTimeout ?? const Duration(seconds: 30), receiveTimeout: receiveTimeout ?? const Duration(seconds: 30), ); } ApiService._({ required String baseUrl, String? authToken, Map? defaultHeaders, required Duration connectTimeout, required Duration receiveTimeout, }) { _dio = Dio(BaseOptions( baseUrl: baseUrl, connectTimeout: connectTimeout, receiveTimeout: receiveTimeout, headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', ...?defaultHeaders, }, )); _setupInterceptors(); if (authToken != null) { setAuthToken(authToken); } } void _setupInterceptors() { // Logging interceptor _dio.interceptors.add(LogInterceptor( requestBody: true, responseBody: true, logPrint: (object) => debugPrint(object.toString()), )); // Auth interceptor _dio.interceptors.add(InterceptorsWrapper( onRequest: (options, handler) { // Add timestamp options.headers['X-Timestamp'] = DateTime.now().millisecondsSinceEpoch; handler.next(options); }, onError: (error, handler) { if (error.response?.statusCode == 401) { // Token expired, redirect to login _handleAuthError(); } handler.next(error); }, )); // Retry interceptor _dio.interceptors.add(RetryInterceptor( dio: _dio, logPrint: debugPrint, retries: 3, retryDelays: const [ Duration(seconds: 1), Duration(seconds: 2), Duration(seconds: 3), ], )); } void setAuthToken(String token) { _dio.options.headers['Authorization'] = 'Bearer $token'; } void clearAuthToken() { _dio.options.headers.remove('Authorization'); } // HTTP Methods Future> get( String path, { Map? queryParameters, Options? options, }) async { try { return await _dio.get( path, queryParameters: queryParameters, options: options, ); } on DioException catch (e) { throw _handleDioError(e); } } Future> post( String path, { dynamic data, Map? queryParameters, Options? options, }) async { try { return await _dio.post( path, data: data, queryParameters: queryParameters, options: options, ); } on DioException catch (e) { throw _handleDioError(e); } } Future> put( String path, { dynamic data, Map? queryParameters, Options? options, }) async { try { return await _dio.put( path, data: data, queryParameters: queryParameters, options: options, ); } on DioException catch (e) { throw _handleDioError(e); } } Future> delete( String path, { dynamic data, Map? queryParameters, Options? options, }) async { try { return await _dio.delete( path, data: data, queryParameters: queryParameters, options: options, ); } on DioException catch (e) { throw _handleDioError(e); } } // File upload Future uploadFile( String path, String filePath, { String fileName = 'file', Map? data, }) async { final formData = FormData.fromMap({ fileName: await MultipartFile.fromFile(filePath), ...?data, }); return await post(path, data: formData); } // Connection check Future hasInternetConnection() async { try { final response = await _dio.get('/health'); return response.statusCode == 200; } catch (e) { return false; } } // Error handling Exception _handleDioError(DioException error) { switch (error.type) { case DioExceptionType.connectionTimeout: return TimeoutException('Connection timeout'); case DioExceptionType.receiveTimeout: return TimeoutException('Receive timeout'); case DioExceptionType.connectionError: return NetworkException('Network error'); case DioExceptionType.badResponse: return HttpException( 'HTTP ${error.response?.statusCode}: ${error.response?.statusMessage}', ); default: return Exception('Unknown error: ${error.message}'); } } void _handleAuthError() { // Clear stored auth token clearAuthToken(); // Navigate to login (implementation depends on your navigation setup) debugPrint('Authentication error - redirecting to login'); } } // Custom exceptions class NetworkException implements Exception { final String message; NetworkException(this.message); } class TimeoutException implements Exception { final String message; TimeoutException(this.message); } class HttpException implements Exception { final String message; HttpException(this.message); } ``` ### 🔄 StratĂ©gies de synchronisation ```dart class SyncStrategy { static Future syncData({ required UserRepository userRepository, required AmicaleRepository amicaleRepository, required MembreRepository membreRepository, bool forceSync = false, }) async { final results = {}; try { // Check internet connection final hasConnection = await ApiService.instance.hasInternetConnection(); if (!hasConnection && !forceSync) { return SyncResult.offline(); } // Sync users try { await userRepository.syncUsersFromApi(); results['users'] = true; } catch (e) { results['users'] = false; debugPrint('Erreur sync users: $e'); } // Sync amicales try { await amicaleRepository.fetchAmicalesFromApi(); results['amicales'] = true; } catch (e) { results['amicales'] = false; debugPrint('Erreur sync amicales: $e'); } // Sync membres try { await membreRepository.fetchMembresFromApi(); results['membres'] = true; } catch (e) { results['membres'] = false; debugPrint('Erreur sync membres: $e'); } return SyncResult.success(results); } catch (e) { debugPrint('Erreur sync globale: $e'); return SyncResult.error(e.toString()); } } static Future scheduledSync({ required UserRepository userRepository, required AmicaleRepository amicaleRepository, required MembreRepository membreRepository, Duration interval = const Duration(minutes: 15), }) async { Timer.periodic(interval, (timer) async { await syncData( userRepository: userRepository, amicaleRepository: amicaleRepository, membreRepository: membreRepository, ); }); } } class SyncResult { final bool success; final Map? results; final String? error; final bool isOffline; SyncResult.success(this.results) : success = true, error = null, isOffline = false; SyncResult.error(this.error) : success = false, results = null, isOffline = false; SyncResult.offline() : success = false, results = null, error = null, isOffline = true; } ``` --- ## đŸ—ș Cartes et gĂ©olocalisation ### 📍 Configuration Flutter Map ```dart class MapConfiguration { static const String mapboxStyleUrl = 'https://api.mapbox.com/styles/v1/mapbox/streets-v11/tiles/{z}/{x}/{y}?access_token={accessToken}'; static const LatLng franceCenterPoint = LatLng(46.2276, 2.2137); static const double defaultZoom = 6.0; static const double sectorZoom = 15.0; static TileLayer get mapboxTileLayer => TileLayer( urlTemplate: mapboxStyleUrl, additionalOptions: const { 'accessToken': String.fromEnvironment('MAPBOX_ACCESS_TOKEN'), 'id': 'mapbox/streets-v11', }, userAgentPackageName: 'com.geosector.app', ); static MarkerLayer buildMarkersLayer(List amicales) { return MarkerLayer( markers: amicales .where((amicale) => amicale.hasGpsCoordinates) .map((amicale) => Marker( point: LatLng(amicale.latitude!, amicale.longitude!), width: 40, height: 40, child: GestureDetector( onTap: () => _showAmicaleInfo(amicale), child: Container( decoration: BoxDecoration( color: amicale.chkActive ? Colors.red : Colors.grey, shape: BoxShape.circle, border: Border.all(color: Colors.white, width: 2), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.3), blurRadius: 4, offset: const Offset(0, 2), ), ], ), child: const Icon( Icons.fireplace_rounded, color: Colors.white, size: 24, ), ), ), )) .toList(), ); } static void _showAmicaleInfo(AmicaleModel amicale) { // Implementation for showing amicale information } } ``` ### 🎯 Service de gĂ©olocalisation ```dart class LocationService { static const LocationSettings _locationSettings = LocationSettings( accuracy: LocationAccuracy.high, distanceFilter: 10, // meters ); static Future checkPermissions() async { bool serviceEnabled; LocationPermission permission; // Check if location services are enabled serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (!serviceEnabled) { return false; } permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { permission = await Geolocator.requestPermission(); if (permission == LocationPermission.denied) { return false; } } if (permission == LocationPermission.deniedForever) { return false; } return true; } static Future getCurrentPosition() async { try { final hasPermission = await checkPermissions(); if (!hasPermission) return null; return await Geolocator.getCurrentPosition( desiredAccuracy: LocationAccuracy.high, ); } catch (e) { debugPrint('Erreur gĂ©olocalisation: $e'); return null; } } static Stream watchPosition() { return Geolocator.getPositionStream( locationSettings: _locationSettings, ); } static Future distanceBetween( double startLatitude, double startLongitude, double endLatitude, double endLongitude, ) async { return Geolocator.distanceBetween( startLatitude, startLongitude, endLatitude, endLongitude, ); } static Future> getAddressFromCoordinates( double latitude, double longitude, ) async { try { return await placemarkFromCoordinates(latitude, longitude); } catch (e) { debugPrint('Erreur geocoding: $e'); return []; } } } ``` ### đŸ—ș Widget de carte interactive ```dart class InteractiveMapWidget extends StatefulWidget { final List amicales; final LatLng? initialCenter; final double initialZoom; final bool showUserLocation; final Function(AmicaleModel)? onAmicaleSelected; const InteractiveMapWidget({ super.key, required this.amicales, this.initialCenter, this.initialZoom = 6.0, this.showUserLocation = true, this.onAmicaleSelected, }); @override State createState() => _InteractiveMapWidgetState(); } class _InteractiveMapWidgetState extends State { final MapController _mapController = MapController(); Position? _userPosition; StreamSubscription? _positionSubscription; @override void initState() { super.initState(); if (widget.showUserLocation) { _initializeLocation(); } } @override void dispose() { _positionSubscription?.cancel(); super.dispose(); } Future _initializeLocation() async { final position = await LocationService.getCurrentPosition(); if (position != null && mounted) { setState(() { _userPosition = position; }); // Start watching position _positionSubscription = LocationService.watchPosition().listen( (position) { if (mounted) { setState(() { _userPosition = position; }); } }, ); } } @override Widget build(BuildContext context) { return FlutterMap( mapController: _mapController, options: MapOptions( center: widget.initialCenter ?? (_userPosition != null ? LatLng(_userPosition!.latitude, _userPosition!.longitude) : MapConfiguration.franceCenterPoint), zoom: widget.initialZoom, maxZoom: 18.0, minZoom: 3.0, interactiveFlags: InteractiveFlag.all, ), children: [ // Base map layer MapConfiguration.mapboxTileLayer, // Amicales markers MarkerLayer( markers: _buildAmicaleMarkers(), ), // User location marker if (_userPosition != null) MarkerLayer( markers: [ Marker( point: LatLng(_userPosition!.latitude, _userPosition!.longitude), width: 30, height: 30, child: Container( decoration: BoxDecoration( color: Colors.blue, shape: BoxShape.circle, border: Border.all(color: Colors.white, width: 2), ), child: const Icon( Icons.my_location, color: Colors.white, size: 16, ), ), ), ], ), // Map controls Positioned( top: 16, right: 16, child: Column( children: [ FloatingActionButton.small( heroTag: 'zoom_in', onPressed: () => _mapController.move( _mapController.center, _mapController.zoom + 1, ), child: const Icon(Icons.add), ), const SizedBox(height: 8), FloatingActionButton.small( heroTag: 'zoom_out', onPressed: () => _mapController.move( _mapController.center, _mapController.zoom - 1, ), child: const Icon(Icons.remove), ), const SizedBox(height: 8), if (_userPosition != null) FloatingActionButton.small( heroTag: 'my_location', onPressed: () => _mapController.move( LatLng(_userPosition!.latitude, _userPosition!.longitude), 15.0, ), child: const Icon(Icons.my_location), ), ], ), ), ], ); } List _buildAmicaleMarkers() { return widget.amicales .where((amicale) => amicale.hasGpsCoordinates) .map((amicale) => Marker( point: LatLng(amicale.latitude!, amicale.longitude!), width: 40, height: 40, child: GestureDetector( onTap: () { if (widget.onAmicaleSelected != null) { widget.onAmicaleSelected!(amicale); } else { _showAmicaleBottomSheet(amicale); } }, child: Container( decoration: BoxDecoration( color: amicale.chkActive ? Colors.red : Colors.grey, shape: BoxShape.circle, border: Border.all(color: Colors.white, width: 2), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.3), blurRadius: 4, offset: const Offset(0, 2), ), ], ), child: const Icon( Icons.fireplace_rounded, color: Colors.white, size: 24, ), ), ), )) .toList(); } void _showAmicaleBottomSheet(AmicaleModel amicale) { showModalBottomSheet( context: context, builder: (context) => AmicaleInfoBottomSheet(amicale: amicale), ); } } class AmicaleInfoBottomSheet extends StatelessWidget { final AmicaleModel amicale; const AmicaleInfoBottomSheet({ super.key, required this.amicale, }); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(16), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( amicale.name, style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 8), Text( amicale.adresseComplete, style: Theme.of(context).textTheme.bodyMedium, ), if (amicale.phone?.isNotEmpty == true) ...[ const SizedBox(height: 4), Text( 'TĂ©l: ${amicale.phone}', style: Theme.of(context).textTheme.bodyMedium, ), ], if (amicale.email?.isNotEmpty == true) ...[ const SizedBox(height: 4), Text( 'Email: ${amicale.email}', style: Theme.of(context).textTheme.bodyMedium, ), ], const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ ElevatedButton.icon( onPressed: () { // Navigation vers l'amicale Navigator.pop(context); }, icon: const Icon(Icons.directions), label: const Text('ItinĂ©raire'), ), OutlinedButton.icon( onPressed: () { // Appeler l'amicale Navigator.pop(context); }, icon: const Icon(Icons.phone), label: const Text('Appeler'), ), ], ), ], ), ); } } ``` --- ## đŸ§Ș Tests et qualitĂ© ### 🔬 Tests unitaires ```dart // test/repositories/user_repository_test.dart import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:hive_test/hive_test.dart'; import 'package:geosector_app/core/repositories/user_repository.dart'; import 'package:geosector_app/core/data/models/user_model.dart'; import '../mocks/mock_api_service.dart'; void main() { group('UserRepository Tests', () { late UserRepository userRepository; late MockApiService mockApiService; setUpAll(() async { await setUpTestHive(); }); setUp(() async { mockApiService = MockApiService(); userRepository = UserRepository(mockApiService); // Open test box await Hive.openBox('test_users'); }); tearDown(() async { await Hive.box('test_users').clear(); await Hive.box('test_users').close(); }); tearDownAll(() async { await tearDownTestHive(); }); test('should save and retrieve user', () async { // Arrange final user = UserModel( id: 1, username: 'test_user', email: 'test@example.com', role: 1, firstName: 'Test', name: 'User', ); // Act await userRepository.saveUser(user); final retrievedUser = userRepository.getUserById(1); // Assert expect(retrievedUser, isNotNull); expect(retrievedUser!.username, equals('test_user')); expect(retrievedUser.email, equals('test@example.com')); }); test('should check user roles correctly', () { // Arrange final adminUser = UserModel( id: 1, username: 'admin', email: 'admin@example.com', role: 2, firstName: 'Admin', name: 'User', ); userRepository.setCurrentUser(adminUser); // Act & Assert expect(userRepository.isAdminAmicale(), isTrue); expect(userRepository.isSuperAdmin(), isFalse); expect(userRepository.getUserRole(), equals(2)); }); test('should authenticate user via API', () async { // Arrange final userJson = { 'id': 1, 'username': 'test_user', 'email': 'test@example.com', 'role': 1, 'first_name': 'Test', 'name': 'User', }; when(mockApiService.post('/auth/login', data: anyNamed('data'))) .thenAnswer((_) async => MockResponse( statusCode: 200, data: {'user': userJson}, )); // Act final user = await userRepository.authenticateUser('test_user', 'password'); // Assert expect(user, isNotNull); expect(user!.username, equals('test_user')); verify(mockApiService.post('/auth/login', data: { 'username': 'test_user', 'password': 'password', })).called(1); }); }); } ``` ### 🎭 Tests d'intĂ©gration ```dart // integration_test/app_test.dart import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:geosector_app/main.dart' as app; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('App Integration Tests', () { testWidgets('should complete login flow', (WidgetTester tester) async { // Start app app.main(); await tester.pumpAndSettle(); // Verify login page is shown expect(find.text('Connexion'), findsOneWidget); expect(find.byType(TextField), findsNWidgets(2)); // Enter credentials await tester.enterText(find.byKey(const Key('username_field')), 'admin'); await tester.enterText(find.byKey(const Key('password_field')), 'password'); // Tap login button await tester.tap(find.text('Se connecter')); await tester.pumpAndSettle(); // Verify navigation to dashboard expect(find.text('Tableau de bord'), findsOneWidget); }); testWidgets('should navigate between admin sections', (WidgetTester tester) async { // Assume user is logged in as admin app.main(); await tester.pumpAndSettle(); // Navigate to login and login as admin // ... login flow ... // Verify admin dashboard expect(find.text('Tableau de bord Administration'), findsOneWidget); // Navigate to amicale section await tester.tap(find.text('Amicale & membres')); await tester.pumpAndSettle(); // Verify amicale page expect(find.text('Mon amicale et ses membres'), findsOneWidget); // Navigate to statistics await tester.tap(find.text('Statistiques')); await tester.pumpAndSettle(); // Verify statistics page expect(find.text('Statistiques'), findsOneWidget); }); }); } ``` ### 📊 Couverture de tests ```bash # GĂ©nĂ©rer rapport de couverture flutter test --coverage # Visualiser avec lcov (Linux/Mac) genhtml coverage/lcov.info -o coverage/html # Ouvrir le rapport open coverage/html/index.html ``` ### 🔍 Analyse statique ```yaml # analysis_options.yaml include: package:flutter_lints/flutter.yaml linter: rules: # Enabled rules prefer_const_constructors: true prefer_const_literals_to_create_immutables: true prefer_final_fields: true prefer_final_locals: true require_trailing_commas: true use_super_parameters: true # Performance avoid_function_literals_in_foreach_calls: true avoid_unnecessary_containers: true sized_box_for_whitespace: true # Style always_declare_return_types: true always_put_required_named_parameters_first: true avoid_escaping_inner_quotes: true prefer_single_quotes: true analyzer: exclude: - "**/*.g.dart" - "**/*.freezed.dart" - "**/generated_plugin_registrant.dart" strong-mode: implicit-casts: false implicit-dynamic: false ``` --- ## 🚀 DĂ©ploiement ### đŸ“± Android ```bash # Build APK debug flutter build apk --debug # Build APK release flutter build apk --release # Build App Bundle (Google Play) flutter build appbundle --release # Install on device flutter install ``` **Configuration `android/app/build.gradle`** : ```gradle android { compileSdkVersion 34 ndkVersion flutter.ndkVersion compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } defaultConfig { applicationId "com.geosector.app" minSdkVersion 21 targetSdkVersion 34 versionCode flutterVersionCode.toInteger() versionName flutterVersionName multiDexEnabled true } signingConfigs { release { keyAlias keystoreProperties['keyAlias'] keyPassword keystoreProperties['keyPassword'] storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null storePassword keystoreProperties['storePassword'] } } buildTypes { release { signingConfig signingConfigs.release minifyEnabled true shrinkResources true } } } ``` ### 🍎 iOS ```bash # Build iOS flutter build ios --release # Open Xcode for signing and deployment open ios/Runner.xcworkspace ``` **Configuration `ios/Runner/Info.plist`** : ```xml NSLocationWhenInUseUsageDescription Cette app utilise la localisation pour le suivi des tournĂ©es NSLocationAlwaysAndWhenInUseUsageDescription Cette app utilise la localisation pour le suivi des tournĂ©es NSCameraUsageDescription Cette app utilise l'appareil photo pour prendre des photos ``` ### 🌐 Web ```bash # Build web flutter build web --release # Serve locally flutter run -d web-server --web-port 8080 # Deploy to Firebase Hosting firebase deploy --only hosting ``` **Configuration Firebase `firebase.json`** : ```json { "hosting": { "public": "build/web", "ignore": [ "firebase.json", "**/.*", "**/node_modules/**" ], "rewrites": [ { "source": "**", "destination": "/index.html" } ] } } ``` ### 🐳 Docker ```dockerfile # Dockerfile FROM nginx:alpine # Copy web build COPY build/web /usr/share/nginx/html # Copy nginx config COPY nginx.conf /etc/nginx/nginx.conf EXPOSE 80 CMD ["nginx", "-g", "daemon off;"] ``` **nginx.conf** : ```nginx events {} http { include /etc/nginx/mime.types; default_type application/octet-stream; server { listen 80; server_name localhost; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ /index.html; } } } ``` ### 🚀 CI/CD avec GitHub Actions ```yaml # .github/workflows/ci.yml name: CI/CD on: push: branches: [ main, develop ] pull_request: branches: [ main ] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Setup Flutter uses: subosito/flutter-action@v2 with: flutter-version: '3.32.0' - name: Get dependencies run: flutter pub get working-directory: ./app - name: Generate code run: flutter packages pub run build_runner build --delete-conflicting-outputs working-directory: ./app - name: Run tests run: flutter test --coverage working-directory: ./app - name: Upload coverage uses: codecov/codecov-action@v3 with: file: ./app/coverage/lcov.info build_android: needs: test runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Setup Flutter uses: subosito/flutter-action@v2 with: flutter-version: '3.32.0' - name: Build APK run: flutter build apk --release working-directory: ./app - name: Upload APK uses: actions/upload-artifact@v3 with: name: app-release.apk path: app/build/app/outputs/flutter-apk/app-release.apk build_web: needs: test runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Setup Flutter uses: subosito/flutter-action@v2 with: flutter-version: '3.32.0' - name: Build Web run: flutter build web --release working-directory: ./app - name: Deploy to Firebase uses: FirebaseExtended/action-hosting-deploy@v0 with: repoToken: '${{ secrets.GITHUB_TOKEN }}' firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT }}' projectId: geosector-app ``` --- ## 🔄 Migration v1 → v2 ### 🎯 Objectifs de la migration La migration de la v1 vers la v2 vise Ă  : - ✅ **Supprimer Provider** : Éliminer la complexitĂ© et l'overhead - ✅ **Introduire ValueListenableBuilder** : RĂ©activitĂ© native avec Hive - ✅ **ImplĂ©menter l'injection de dĂ©pendances** : Meilleure testabilitĂ© - ✅ **Centraliser l'API** : Singleton ApiService - ✅ **Optimiser les performances** : RĂ©duction des rebuilds ### 📋 Checklist de migration #### Phase 1 : PrĂ©paration - [ ] Sauvegarder les donnĂ©es utilisateur - [ ] Mettre Ă  jour les dĂ©pendances - [ ] Configurer les nouveaux adapters Hive - [ ] PrĂ©parer l'environnement de test #### Phase 2 : Architecture - [ ] CrĂ©er les nouveaux repositories avec injection - [ ] ImplĂ©menter ApiService singleton - [ ] Migrer les modĂšles vers Hive - [ ] Configurer ValueListenableBuilder #### Phase 3 : Interface - [ ] Refactoriser les pages admin - [ ] Mettre Ă  jour les widgets de tableau - [ ] ImplĂ©menter la gestion des rĂŽles - [ ] Tester l'interface adaptative #### Phase 4 : Tests - [ ] Tests unitaires des repositories - [ ] Tests d'intĂ©gration des workflows - [ ] Tests de performance - [ ] Validation utilisateur #### Phase 5 : DĂ©ploiement - [ ] DĂ©ployment en staging - [ ] Tests de charge - [ ] Formation utilisateurs - [ ] DĂ©ployment production ### 🔧 Script de migration ```dart // migration/migration_script.dart class MigrationScript { static Future migrateV1ToV2() async { try { print('🚀 DĂ©but de la migration v1 → v2'); // Step 1: Backup existing data await _backupExistingData(); // Step 2: Initialize new Hive boxes await _initializeNewHiveBoxes(); // Step 3: Migrate user data await _migrateUserData(); // Step 4: Migrate amicale data await _migrateAmicaleData(); // Step 5: Migrate membre data await _migrateMembreData(); // Step 6: Update app version await _updateAppVersion(); print('✅ Migration v1 → v2 terminĂ©e avec succĂšs'); } catch (e) { print('❌ Erreur lors de la migration: $e'); await _rollbackMigration(); } } static Future _backupExistingData() async { print('📩 Sauvegarde des donnĂ©es existantes...'); // Implementation } static Future _initializeNewHiveBoxes() async { print('đŸ—„ïž Initialisation des nouvelles Box Hive...'); // Register adapters Hive.registerAdapter(UserModelAdapter()); Hive.registerAdapter(AmicaleModelAdapter()); Hive.registerAdapter(MembreModelAdapter()); // Open boxes await Hive.openBox('users_v2'); await Hive.openBox('amicales_v2'); await Hive.openBox('membres_v2'); } static Future _migrateUserData() async { print('đŸ‘€ Migration des donnĂ©es utilisateurs...'); // Implementation } static Future _migrateAmicaleData() async { print('🏱 Migration des donnĂ©es amicales...'); // Implementation } static Future _migrateMembreData() async { print('đŸ‘„ Migration des donnĂ©es membres...'); // Implementation } static Future _updateAppVersion() async { print('🔄 Mise Ă  jour de la version...'); final prefs = await SharedPreferences.getInstance(); await prefs.setString('app_version', '2.0.0'); } static Future _rollbackMigration() async { print('🔙 Rollback de la migration...'); // Implementation } } ``` ### 📚 Guide de migration pour dĂ©veloppeurs #### Avant (v1 avec Provider) ```dart // v1 - Avec Provider class MyPage extends StatelessWidget { @override Widget build(BuildContext context) { return Consumer( builder: (context, provider, child) { return ListView.builder( itemCount: provider.amicales.length, itemBuilder: (context, index) { return ListTile( title: Text(provider.amicales[index].name), ); }, ); }, ); } } ``` #### AprĂšs (v2 avec injection + ValueListenableBuilder) ```dart // v2 - Avec injection de dĂ©pendances + ValueListenableBuilder class MyPage extends StatelessWidget { final AmicaleRepository amicaleRepository; const MyPage({ super.key, required this.amicaleRepository, }); @override Widget build(BuildContext context) { return ValueListenableBuilder>( valueListenable: amicaleRepository.getAmicalesBox().listenable(), builder: (context, box, child) { final amicales = box.values.toList(); return ListView.builder( itemCount: amicales.length, itemBuilder: (context, index) { return ListTile( title: Text(amicales[index].name), ); }, ); }, ); } } ``` ### 🎓 Formation utilisateurs #### NouveautĂ©s v2 pour les utilisateurs - **Interface plus fluide** : RĂ©activitĂ© amĂ©liorĂ©e - **Performance accrue** : Chargement plus rapide - **Meilleure stabilitĂ©** : Moins de bugs et crashs - **Nouvelles fonctionnalitĂ©s** : Interface adaptative selon le rĂŽle #### Guide de transition 1. **Mise Ă  jour automatique** : L'app se met Ă  jour automatiquement 2. **DonnĂ©es prĂ©servĂ©es** : Toutes les donnĂ©es sont conservĂ©es 3. **Nouvelle interface** : LĂ©gĂšres modifications visuelles 4. **AmĂ©lioration des performances** : ExpĂ©rience plus fluide --- ## 📈 Roadmap et Ă©volutions ### 🎯 v2.1 (Q2 2024) - [ ] Mode hors-ligne avancĂ© - [ ] Synchronisation diffĂ©rentielle - [ ] Notifications push - [ ] Export PDF des rapports ### 🎯 v2.2 (Q3 2024) - [ ] Module de planification - [ ] Analytics avancĂ©es - [ ] API GraphQL - [ ] Support tablettes ### 🎯 v3.0 (Q4 2024) - [ ] Intelligence artificielle - [ ] Optimisation des tournĂ©es - [ ] RĂ©alitĂ© augmentĂ©e - [ ] IoT integration --- ## đŸ€ Contribution ### 🔧 Setup dĂ©veloppement ```bash # Clone du projet git clone https://github.com/your-org/geosector.git cd geosector/app # Installation des dĂ©pendances flutter pub get # GĂ©nĂ©ration du code flutter packages pub run build_runner watch # Tests flutter test # Lancement en debug flutter run ``` ### 📝 Standards de code - **Langue** : Code en anglais, commentaires en français - **Formatting** : `flutter format .` - **Linting** : Respect des rĂšgles `analysis_options.yaml` - **Tests** : Couverture minimum 80% - **Documentation** : Commentaires pour les mĂ©thodes publiques ### 🚀 Workflow de contribution 1. **Fork** le repository 2. **Branch** feature depuis `develop` 3. **Commits** descriptifs avec convention 4. **Tests** unitaires et d'intĂ©gration 5. **Pull Request** avec description dĂ©taillĂ©e 6. **Code Review** par l'Ă©quipe 7. **Merge** aprĂšs validation ### 📋 Convention de commits ``` feat: ajout fonctionnalitĂ© de gĂ©olocalisation fix: correction bug authentification docs: mise Ă  jour README style: formatage du code refactor: restructuration des repositories test: ajout tests unitaires chore: mise Ă  jour dĂ©pendances ``` --- ## 📞 Support et contacts ### đŸ› ïž Support technique - **Email** : support@geosector.com - **Discord** : [Serveur dĂ©veloppeurs](https://discord.gg/geosector) - **Documentation** : [Wiki GitHub](https://github.com/your-org/geosector/wiki) ### 🐛 Signalement de bugs - **GitHub Issues** : [Reporter un bug](https://github.com/your-org/geosector/issues) - **Template** : Utiliser le template de bug report - **Informations** : Version app, OS, logs d'erreur ### 💡 Demandes de fonctionnalitĂ©s - **GitHub Discussions** : [Proposer une idĂ©e](https://github.com/your-org/geosector/discussions) - **Roadmap** : Consulter la feuille de route - **Vote** : Voter pour les fonctionnalitĂ©s prioritaires ### đŸ‘„ Équipe - **Lead Developer** : [@lead-dev](https://github.com/lead-dev) - **Frontend Team** : [@frontend-team](https://github.com/frontend-team) - **Backend Team** : [@backend-team](https://github.com/backend-team) - **QA Team** : [@qa-team](https://github.com/qa-team) --- ## 📄 Licence et crĂ©dits ### 📜 Licence ``` MIT License Copyright (c) 2024 GEOSECTOR Team Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` ### 🙏 Remerciements - **Flutter Team** : Framework exceptionnel - **Hive Team** : Base de donnĂ©es locale performante - **Mapbox** : Solutions cartographiques - **CommunautĂ© Flutter** : Support et contributions - **Pompiers de France** : Retours utilisateurs prĂ©cieux ---
**GEOSECTOR v2.0** 🚒 *RĂ©volutionnant la gestion des distributions de calendriers* [![Flutter](https://img.shields.io/badge/Flutter-3.32+-blue.svg)](https://flutter.dev) [![Dart](https://img.shields.io/badge/Dart-3.0+-blue.svg)](https://dart.dev) [![License](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) [![Build](https://img.shields.io/github/workflow/status/your-org/geosector/CI/main)](https://github.com/your-org/geosector/actions) [🚀 DĂ©mo](https://demo.geosector.com) ‱ [📚 Documentation](https://docs.geosector.com) ‱ [💬 Discord](https://discord.gg/geosector) ‱ [🐛 Issues](https://github.com/your-org/geosector/issues)