feat: Version 3.5.2 - Configuration Stripe et gestion des immeubles

- Configuration complète Stripe pour les 3 environnements (DEV/REC/PROD)
  * DEV: Clés TEST Pierre (mode test)
  * REC: Clés TEST Client (mode test)
  * PROD: Clés LIVE Client (mode live)
- Ajout de la gestion des bases de données immeubles/bâtiments
  * Configuration buildings_database pour DEV/REC/PROD
  * Service BuildingService pour enrichissement des adresses
- Optimisations pages et améliorations ergonomie
- Mises à jour des dépendances Composer
- Nettoyage des fichiers obsolètes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
pierre
2025-11-09 18:26:27 +01:00
parent 21657a3820
commit 2f5946a184
812 changed files with 142105 additions and 25992 deletions

View File

@@ -101,10 +101,10 @@ class PassageRepository extends ChangeNotifier {
return _passageBox.values.where((passage) => passage.fkOperation == operationId).toList();
}
// Récupérer les passages par utilisateur
List<PassageModel> getPassagesByUser(int userId) {
// Récupérer les passages par utilisateur (ope_users.id)
List<PassageModel> getPassagesByUser(int opeUserId) {
try {
return _passageBox.values.where((passage) => passage.fkUser == userId).toList();
return _passageBox.values.where((passage) => passage.fkUser == opeUserId).toList();
} catch (e) {
debugPrint('Erreur lors de la récupération des passages par utilisateur: $e');
return [];
@@ -380,12 +380,12 @@ class PassageRepository extends ChangeNotifier {
// Vérifier si la requête a été mise en file d'attente
if (response.data['queued'] == true) {
// Récupérer l'utilisateur actuel
final currentUserId = CurrentUserService.instance.userId;
// Récupérer l'utilisateur actuel (ope_users.id)
final currentOpeUserId = CurrentUserService.instance.opeUserId;
// Mode offline : mettre à jour localement et marquer comme non synchronisé
final offlinePassage = passage.copyWith(
fkUser: currentUserId, // Le passage appartient maintenant à l'utilisateur qui l'a modifié
fkUser: currentOpeUserId, // Le passage appartient à l'utilisateur qui l'a modifié (ope_users.id)
lastSyncedAt: null,
isSynced: false,
);
@@ -418,12 +418,12 @@ class PassageRepository extends ChangeNotifier {
// Mode online : traitement normal
if (response.statusCode == 200) {
// Récupérer l'utilisateur actuel
final currentUserId = CurrentUserService.instance.userId;
// Récupérer l'utilisateur actuel (ope_users.id)
final currentOpeUserId = CurrentUserService.instance.opeUserId;
// Mettre à jour le passage localement avec le user actuel
final updatedPassage = passage.copyWith(
fkUser: currentUserId, // Le passage appartient maintenant à l'utilisateur qui l'a modifié
fkUser: currentOpeUserId, // Le passage appartient à l'utilisateur qui l'a modifié (ope_users.id)
lastSyncedAt: DateTime.now(),
isSynced: true,
);
@@ -574,10 +574,10 @@ class PassageRepository extends ChangeNotifier {
// Calculer les statistiques pour chaque utilisateur
for (final entry in passagesByUser.entries) {
final userId = entry.key;
final opeUserId = entry.key; // ID de l'utilisateur dans ope_users
final userPassages = entry.value;
statsByUser[userId] = {
statsByUser[opeUserId] = {
'total': userPassages.length,
'effectues': userPassages.where((p) => p.fkType == 1).length,
'a_finaliser': userPassages.where((p) => p.fkType == 2).length,

View File

@@ -301,17 +301,20 @@ class SectorRepository extends ChangeNotifier {
}
// Mettre à jour un secteur via l'API
Future<Map<String, dynamic>> updateSector(SectorModel sector, {List<int>? users}) async {
Future<Map<String, dynamic>> updateSector(SectorModel sector, {List<int>? users, int? chkAdressesChange}) async {
try {
// Préparer les données à envoyer
final Map<String, dynamic> requestData = {
...sector.toJson(),
};
// Ajouter les utilisateurs si fournis
if (users != null) {
requestData['users'] = users;
}
// Ajouter le paramètre chk_adresses_change (par défaut 1 si non spécifié)
requestData['chk_adresses_change'] = chkAdressesChange ?? 1;
final response = await ApiService.instance.put(
'${AppKeys.sectorsEndpoint}/${sector.id}',
@@ -339,19 +342,19 @@ class SectorRepository extends ChangeNotifier {
final SectorModel updatedSector = SectorModel.fromJson(responseData['sector']);
await saveSector(updatedSector);
}
// Traiter les passages retournés s'ils existent
if (responseData['passages_sector'] != null) {
// Traiter les passages retournés UNIQUEMENT si chk_adresses_change = 1
if ((chkAdressesChange ?? 1) == 1 && responseData['passages_sector'] != null) {
try {
final passagesData = responseData['passages_sector'] as List<dynamic>;
debugPrint('Traitement de ${passagesData.length} passages après UPDATE');
debugPrint('Traitement de ${passagesData.length} passages après UPDATE (chk_adresses_change=1)');
// Utiliser PassageRepository pour traiter les passages
final passageRepository = PassageRepository();
// Vider d'abord tous les passages du secteur
await _deleteAllPassagesOfSector(sector.id);
// Puis sauvegarder tous les passages retournés
final List<PassageModel> passagesToSave = [];
for (final passageData in passagesData) {
@@ -363,7 +366,7 @@ class SectorRepository extends ChangeNotifier {
debugPrint('Erreur lors du traitement d\'un passage: $e');
}
}
if (passagesToSave.isNotEmpty) {
await passageRepository.savePassages(passagesToSave);
debugPrint('${passagesToSave.length} passages sauvegardés après UPDATE');
@@ -371,6 +374,8 @@ class SectorRepository extends ChangeNotifier {
} catch (e) {
debugPrint('Erreur lors du traitement des passages: $e');
}
} else if ((chkAdressesChange ?? 1) == 0) {
debugPrint('⏭️ Passages ignorés (chk_adresses_change=0) - les passages existants sont conservés');
}
// Traiter les users_sectors retournés s'ils existent

View File

@@ -646,8 +646,8 @@ class UserRepository extends ChangeNotifier {
// === SYNCHRONISATION ET REFRESH ===
/// Rafraîchir la session (soft login)
/// Utilise un refresh partiel si la dernière sync date de moins de 24h
/// Sinon fait un refresh complet
/// NOTE: Les endpoints /session/refresh/all et /session/refresh/partial ont été retirés
/// Cette méthode maintient la session locale sans faire d'appel API
Future<bool> refreshSession() async {
try {
debugPrint('🔄 Début du refresh de session...');
@@ -658,7 +658,7 @@ class UserRepository extends ChangeNotifier {
return false;
}
// NOUVEAU : Vérifier la connexion internet avant de faire des appels API
// Vérifier la connexion internet avant de faire des appels API
final hasConnection = await ApiService.instance.hasInternetConnection();
if (!hasConnection) {
debugPrint('📵 Pas de connexion internet - refresh annulé');
@@ -671,147 +671,17 @@ class UserRepository extends ChangeNotifier {
_startAutoRefreshTimer();
}
// Récupérer la dernière date de sync depuis settings
DateTime? lastSync;
try {
if (Hive.isBoxOpen(AppKeys.settingsBoxName)) {
final settingsBox = Hive.box(AppKeys.settingsBoxName);
final lastSyncString = settingsBox.get('last_sync') as String?;
if (lastSyncString != null) {
lastSync = DateTime.parse(lastSyncString);
debugPrint('📅 Dernière sync: ${lastSync.toIso8601String()}');
}
}
} catch (e) {
debugPrint('⚠️ Erreur lecture last_sync: $e');
}
// NOTE: Les endpoints de refresh ont été retirés
// La session locale est maintenue mais aucune synchronisation avec le serveur n'est effectuée
debugPrint(' Refresh de session désactivé (endpoints retirés)');
// Déterminer si on fait un refresh partiel ou complet
// Refresh partiel si:
// - On a une date de dernière sync
// - Cette date est de moins de 24h
final now = DateTime.now();
final shouldPartialRefresh = lastSync != null &&
now.difference(lastSync).inHours < 24;
if (shouldPartialRefresh) {
debugPrint('⚡ Refresh partiel (dernière sync < 24h)');
try {
// Appel API pour refresh partiel
final response = await ApiService.instance.refreshSessionPartial(lastSync);
if (response.data != null && response.data['status'] == 'success') {
// Traiter uniquement les données modifiées
await _processPartialRefreshData(response.data);
// Mettre à jour last_sync
await _saveLastSyncTimestamp(now);
debugPrint('✅ Refresh partiel réussi');
return true;
}
} catch (e) {
debugPrint('⚠️ Erreur refresh partiel: $e');
// Vérifier si c'est une erreur d'authentification
if (_isAuthenticationError(e)) {
debugPrint('🔒 Erreur d\'authentification détectée - nettoyage de la session locale');
await _clearInvalidSession();
return false;
}
// Sinon, on tente un refresh complet
debugPrint('Tentative de refresh complet...');
}
}
// Refresh complet
debugPrint('🔄 Refresh complet des données...');
try {
final response = await ApiService.instance.refreshSessionAll();
if (response.data != null && response.data['status'] == 'success') {
// Traiter toutes les données comme un login
await DataLoadingService.instance.processLoginData(response.data);
// Mettre à jour last_sync
await _saveLastSyncTimestamp(now);
debugPrint('✅ Refresh complet réussi');
return true;
}
} catch (e) {
debugPrint('❌ Erreur refresh complet: $e');
// Vérifier si c'est une erreur d'authentification
if (_isAuthenticationError(e)) {
debugPrint('🔒 Session invalide côté serveur - nettoyage de la session locale');
await _clearInvalidSession();
}
return false;
}
return false;
return true;
} catch (e) {
debugPrint('❌ Erreur générale refresh session: $e');
return false;
}
}
/// Traiter les données d'un refresh partiel
Future<void> _processPartialRefreshData(Map<String, dynamic> data) async {
try {
debugPrint('📦 Traitement des données partielles...');
// Traiter les secteurs modifiés
if (data['sectors'] != null && data['sectors'] is List) {
final sectorsBox = Hive.box<SectorModel>(AppKeys.sectorsBoxName);
for (final sectorData in data['sectors']) {
final sector = SectorModel.fromJson(sectorData);
await sectorsBox.put(sector.id, sector);
}
debugPrint('${data['sectors'].length} secteurs mis à jour');
}
// Traiter les passages modifiés
if (data['passages'] != null && data['passages'] is List) {
final passagesBox = Hive.box<PassageModel>(AppKeys.passagesBoxName);
for (final passageData in data['passages']) {
final passage = PassageModel.fromJson(passageData);
await passagesBox.put(passage.id, passage);
}
debugPrint('${data['passages'].length} passages mis à jour');
}
// Traiter les opérations modifiées
if (data['operations'] != null && data['operations'] is List) {
final operationsBox = Hive.box<OperationModel>(AppKeys.operationsBoxName);
for (final operationData in data['operations']) {
final operation = OperationModel.fromJson(operationData);
await operationsBox.put(operation.id, operation);
}
debugPrint('${data['operations'].length} opérations mises à jour');
}
// Traiter les membres modifiés
if (data['membres'] != null && data['membres'] is List) {
final membresBox = Hive.box<MembreModel>(AppKeys.membresBoxName);
for (final membreData in data['membres']) {
final membre = MembreModel.fromJson(membreData);
await membresBox.put(membre.id, membre);
}
debugPrint('${data['membres'].length} membres mis à jour');
}
} catch (e) {
debugPrint('❌ Erreur traitement données partielles: $e');
rethrow;
}
}
/// Sauvegarder le timestamp de la dernière sync
Future<void> _saveLastSyncTimestamp(DateTime timestamp) async {
try {
@@ -825,55 +695,6 @@ class UserRepository extends ChangeNotifier {
}
}
/// Vérifie si l'erreur est une erreur d'authentification (401, 403)
/// Retourne false pour les erreurs 404 (route non trouvée)
bool _isAuthenticationError(dynamic error) {
final errorMessage = error.toString().toLowerCase();
// Si c'est une erreur 404, ce n'est pas une erreur d'authentification
// C'est juste que la route n'existe pas encore côté API
if (errorMessage.contains('404') || errorMessage.contains('not found')) {
debugPrint('⚠️ Route API non trouvée (404) - en attente de l\'implémentation côté serveur');
return false;
}
// Vérifier les vraies erreurs d'authentification
return errorMessage.contains('401') ||
errorMessage.contains('403') ||
errorMessage.contains('unauthorized') ||
errorMessage.contains('forbidden') ||
errorMessage.contains('session expired') ||
errorMessage.contains('authentication failed');
}
/// Nettoie la session locale invalide
Future<void> _clearInvalidSession() async {
try {
debugPrint('🗑️ Nettoyage de la session invalide...');
// Arrêter le timer de refresh
_stopAutoRefreshTimer();
// Nettoyer les données de session
await CurrentUserService.instance.clearUser();
await CurrentAmicaleService.instance.clearAmicale();
// Nettoyer les IDs dans settings
if (Hive.isBoxOpen(AppKeys.settingsBoxName)) {
final settingsBox = Hive.box(AppKeys.settingsBoxName);
await settingsBox.delete('current_user_id');
await settingsBox.delete('current_amicale_id');
await settingsBox.delete('last_sync');
}
// Supprimer le sessionId de l'API
ApiService.instance.setSessionId(null);
debugPrint('✅ Session locale nettoyée suite à erreur d\'authentification');
} catch (e) {
debugPrint('❌ Erreur lors du nettoyage de session: $e');
}
}
// === TIMER DE REFRESH AUTOMATIQUE ===
@@ -1078,8 +899,14 @@ class UserRepository extends ChangeNotifier {
}
}
// Convertir ope_user_id en int si présent
final dynamic rawOpeUserId = userData['ope_user_id'];
final int? opeUserId = rawOpeUserId != null
? (rawOpeUserId is String ? int.parse(rawOpeUserId) : rawOpeUserId as int)
: null;
debugPrint(
'✅ Données traitées - id: $id, role: $role, fkEntite: $fkEntite');
'✅ Données traitées - id: $id, role: $role, fkEntite: $fkEntite, opeUserId: $opeUserId');
// Créer un utilisateur avec toutes les données disponibles
return UserModel(
@@ -1103,6 +930,7 @@ class UserRepository extends ChangeNotifier {
mobile: userData['mobile'],
dateNaissance: dateNaissance,
dateEmbauche: dateEmbauche,
opeUserId: opeUserId,
);
}
}