feat: synchronisation mode deconnecte fin chat et stats

This commit is contained in:
2025-08-31 18:21:20 +02:00
parent 41a4505b4b
commit 604294af96
149 changed files with 285769 additions and 250633 deletions

View File

@@ -1,5 +1,6 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:geosector_app/core/data/models/amicale_model.dart';
import 'package:geosector_app/core/services/api_service.dart';
@@ -93,7 +94,7 @@ class AmicaleRepository extends ChangeNotifier {
}
// Créer une amicale via l'API
Future<bool> createAmicale(AmicaleModel amicale) async {
Future<bool> createAmicale(AmicaleModel amicale, {BuildContext? context}) async {
_isLoading = true;
notifyListeners();
@@ -104,6 +105,39 @@ class AmicaleRepository extends ChangeNotifier {
// Appeler l'API pour créer l'amicale
final response = await ApiService.instance.post('/amicales', data: data);
// Vérifier si la requête a été mise en queue (mode offline)
if (response.data != null && response.data['queued'] == true) {
// Mode offline : NE PAS créer l'amicale dans Hive
debugPrint('⏳ Création de l\'amicale mise en attente (mode hors ligne)');
// Informer l'utilisateur si un contexte est fourni
if (context != null && context.mounted) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Row(
children: [
Icon(Icons.cloud_off, color: Colors.orange),
SizedBox(width: 8),
Text('Création en attente'),
],
),
content: const Text(
'L\'amicale sera créée dès que la connexion sera rétablie.\n\n'
'La création a été ajoutée à la file d\'attente.',
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Compris'),
),
],
),
);
}
return true; // Indiquer que l'opération a été acceptée (mise en queue)
}
if (response.statusCode == 201 || response.statusCode == 200) {
// Récupérer l'ID de la nouvelle amicale
final amicaleId = response.data['id'] is String ? int.parse(response.data['id']) : response.data['id'] as int;
@@ -163,6 +197,17 @@ class AmicaleRepository extends ChangeNotifier {
// Appeler l'API pour mettre à jour l'amicale
final response = await ApiService.instance.put('/amicales/${amicale.id}', data: data);
// Vérifier si la requête a été mise en queue (mode offline)
if (response.data != null && response.data['queued'] == true) {
// Mode offline : Sauvegarder localement avec indicateur
final updatedAmicale = amicale.copyWith(
updatedAt: DateTime.now(),
);
await saveAmicale(updatedAmicale);
debugPrint('⏳ Modification de l\'amicale ${amicale.id} mise en attente (mode hors ligne)');
return true;
}
if (response.statusCode == 200) {
// Mettre à jour l'amicale localement avec updatedAt
final updatedAmicale = amicale.copyWith(
@@ -194,6 +239,14 @@ class AmicaleRepository extends ChangeNotifier {
data: amicale.toJson(),
);
// Vérifier si la requête a été mise en queue (mode offline)
if (response.data != null && response.data['queued'] == true) {
// Mode offline : Sauvegarder localement
await saveAmicale(amicale);
debugPrint('⏳ Modification de l\'amicale ${amicale.id} mise en attente (mode hors ligne)');
return amicale; // Retourner l'amicale locale
}
if (response.statusCode == 200) {
final updatedAmicaleData = response.data;
final updatedAmicale = AmicaleModel.fromJson(updatedAmicaleData);
@@ -221,6 +274,14 @@ class AmicaleRepository extends ChangeNotifier {
// Appeler l'API pour supprimer l'amicale
final response = await ApiService.instance.delete('/amicales/$id');
// Vérifier si la requête a été mise en queue (mode offline)
if (response.data != null && response.data['queued'] == true) {
// Mode offline : Supprimer localement immédiatement
await deleteAmicale(id);
debugPrint('⏳ Suppression de l\'amicale $id mise en attente (mode hors ligne)');
return true;
}
if (response.statusCode == 200 || response.statusCode == 204) {
// Supprimer l'amicale localement
await deleteAmicale(id);

View File

@@ -1,5 +1,6 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:geosector_app/core/data/models/client_model.dart';
import 'package:geosector_app/core/services/api_service.dart';
@@ -54,7 +55,7 @@ class ClientRepository extends ChangeNotifier {
}
// Créer un client via l'API
Future<bool> createClient(ClientModel client) async {
Future<bool> createClient(ClientModel client, {BuildContext? context}) async {
_isLoading = true;
notifyListeners();
@@ -68,6 +69,39 @@ class ClientRepository extends ChangeNotifier {
// Appeler l'API pour créer le client
final response = await ApiService.instance.post('/clients', data: data);
// Vérifier si la requête a été mise en queue (mode offline)
if (response.data != null && response.data['queued'] == true) {
// Mode offline : NE PAS créer le client dans Hive
debugPrint('⏳ Création du client mise en attente (mode hors ligne)');
// Informer l'utilisateur si un contexte est fourni
if (context != null && context.mounted) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Row(
children: [
Icon(Icons.cloud_off, color: Colors.orange),
SizedBox(width: 8),
Text('Création en attente'),
],
),
content: const Text(
'Le client sera créé dès que la connexion sera rétablie.\n\n'
'La création a été ajoutée à la file d\'attente.',
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Compris'),
),
],
),
);
}
return true; // Indiquer que l'opération a été acceptée (mise en queue)
}
if (response.statusCode == 201 || response.statusCode == 200) {
// Récupérer l'ID du nouveau client
final clientId = response.data['id'] is String ? int.parse(response.data['id']) : response.data['id'] as int;
@@ -104,6 +138,18 @@ class ClientRepository extends ChangeNotifier {
// Appeler l'API pour mettre à jour le client
final response = await ApiService.instance.put('/clients/${client.id}', data: data);
// Vérifier si la requête a été mise en queue (mode offline)
if (response.data != null && response.data['queued'] == true) {
// Mode offline : Sauvegarder localement avec un indicateur
final updatedClient = client.copyWith(
updatedAt: DateTime.now(),
// Note: isSynced n'existe pas dans ClientModel, on utilise updatedAt comme indicateur
);
await saveClient(updatedClient);
debugPrint('⏳ Modification du client ${client.id} mise en attente (mode hors ligne)');
return true;
}
if (response.statusCode == 200) {
// Mettre à jour le client localement avec updatedAt
final updatedClient = client.copyWith(
@@ -132,6 +178,14 @@ class ClientRepository extends ChangeNotifier {
// Appeler l'API pour supprimer le client
final response = await ApiService.instance.delete('/clients/$id');
// Vérifier si la requête a été mise en queue (mode offline)
if (response.data != null && response.data['queued'] == true) {
// Mode offline : Supprimer localement immédiatement
await deleteClient(id);
debugPrint('⏳ Suppression du client $id mise en attente (mode hors ligne)');
return true;
}
if (response.statusCode == 200 || response.statusCode == 204) {
// Supprimer le client localement
await deleteClient(id);

View File

@@ -1,5 +1,6 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:geosector_app/core/data/models/membre_model.dart';
import 'package:geosector_app/core/services/api_service.dart';
@@ -122,7 +123,7 @@ class MembreRepository extends ChangeNotifier {
// === MÉTHODES API ===
// Créer un membre via l'API
Future<MembreModel?> createMembre(MembreModel membre, {String? password}) async {
Future<MembreModel?> createMembre(MembreModel membre, {String? password, BuildContext? context}) async {
_isLoading = true;
notifyListeners();
@@ -171,6 +172,39 @@ class MembreRepository extends ChangeNotifier {
// Appeler l'API users
final response = await ApiService.instance.post('/users', data: data);
// Vérifier si la requête a été mise en queue (mode offline)
if (response.data != null && response.data['queued'] == true) {
// Mode offline : NE PAS créer le membre dans Hive
debugPrint('⏳ Création du membre mise en attente (mode hors ligne)');
// Informer l'utilisateur si un contexte est fourni
if (context != null && context.mounted) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Row(
children: [
Icon(Icons.cloud_off, color: Colors.orange),
SizedBox(width: 8),
Text('Création en attente'),
],
),
content: const Text(
'Le membre sera créé dès que la connexion sera rétablie.\n\n'
'La création a été ajoutée à la file d\'attente.',
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Compris'),
),
],
),
);
}
return null; // Retourner null car pas de membre créé localement
}
// Vérifier d'abord si on a une réponse avec un statut d'erreur
if (response.data != null && response.data is Map<String, dynamic>) {
final responseData = response.data as Map<String, dynamic>;
@@ -276,6 +310,14 @@ class MembreRepository extends ChangeNotifier {
// Appeler l'API users au lieu de membres
final response = await ApiService.instance.put('/users/${membre.id}', data: data);
// Vérifier si la requête a été mise en queue (mode offline)
if (response.data != null && response.data['queued'] == true) {
// Mode offline : Sauvegarder localement avec indicateur
await saveMembreBox(membre);
debugPrint('⏳ Modification du membre ${membre.id} mise en attente (mode hors ligne)');
return true;
}
// Si on arrive ici, c'est que la requête a réussi (200)
// Sauvegarder le membre mis à jour localement
await saveMembreBox(membre);
@@ -362,6 +404,14 @@ class MembreRepository extends ChangeNotifier {
final response = await ApiService.instance.delete(endpoint);
// Vérifier si la requête a été mise en queue (mode offline)
if (response.data != null && response.data['queued'] == true) {
// Mode offline : Supprimer localement immédiatement
await deleteMembreBox(membreId);
debugPrint('⏳ Suppression du membre $membreId mise en attente (mode hors ligne)');
return true;
}
// Vérifier si on a une réponse avec un statut d'erreur
if (response.data != null && response.data is Map<String, dynamic>) {
final responseData = response.data as Map<String, dynamic>;

View File

@@ -163,7 +163,7 @@ class OperationRepository extends ChangeNotifier {
// Créer une opération
Future<bool> createOperation(
String name, DateTime dateDebut, DateTime dateFin) async {
String name, DateTime dateDebut, DateTime dateFin, {BuildContext? context}) async {
_isLoading = true;
notifyListeners();
@@ -183,6 +183,39 @@ class OperationRepository extends ChangeNotifier {
final response =
await ApiService.instance.post('/operations', data: data);
// Vérifier si la requête a été mise en queue (mode offline)
if (response.data != null && response.data['queued'] == true) {
// Mode offline : NE PAS créer l'opération dans Hive
debugPrint('⏳ Création de l\'opération mise en attente (mode hors ligne)');
// Informer l'utilisateur si un contexte est fourni
if (context != null && context.mounted) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Row(
children: [
Icon(Icons.cloud_off, color: Colors.orange),
SizedBox(width: 8),
Text('Création en attente'),
],
),
content: const Text(
'L\'opération sera créée dès que la connexion sera rétablie.\n\n'
'La création a été ajoutée à la file d\'attente.',
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Compris'),
),
],
),
);
}
return true; // Indiquer que l'opération a été acceptée (mise en queue)
}
if (response.statusCode == 201 || response.statusCode == 200) {
debugPrint('✅ Opération créée avec succès');
@@ -256,6 +289,7 @@ class OperationRepository extends ChangeNotifier {
operation.name,
operation.dateDebut,
operation.dateFin,
context: null, // Pas de contexte disponible ici
);
} else {
// Opération existante - mettre à jour
@@ -329,6 +363,23 @@ class OperationRepository extends ChangeNotifier {
final response =
await ApiService.instance.put('/operations/$id', data: data);
// Vérifier si la requête a été mise en queue (mode offline)
if (response.data != null && response.data['queued'] == true) {
// Mode offline : Sauvegarder localement avec indicateur
final updatedOperation = existingOperation.copyWith(
name: name,
dateDebut: dateDebut,
dateFin: dateFin,
isActive: isActive,
fkEntite: fkEntite,
lastSyncedAt: null,
isSynced: false,
);
await saveOperation(updatedOperation);
debugPrint('⏳ Modification de l\'opération $id mise en attente (mode hors ligne)');
return true;
}
if (response.statusCode == 200) {
debugPrint('✅ Opération $id mise à jour avec succès');
// Mettre à jour l'opération localement
@@ -368,6 +419,14 @@ class OperationRepository extends ChangeNotifier {
// Appeler l'API pour supprimer l'opération inactive
final response = await ApiService.instance.delete('/operations/$id');
// Vérifier si la requête a été mise en queue (mode offline)
if (response.data != null && response.data['queued'] == true) {
// Mode offline : Supprimer localement immédiatement
await deleteOperation(id);
debugPrint('⏳ Suppression de l\'opération $id mise en attente (mode hors ligne)');
return true;
}
if (response.statusCode == 200 || response.statusCode == 204) {
debugPrint('✅ Suppression réussie - Traitement de la réponse');
@@ -409,6 +468,15 @@ class OperationRepository extends ChangeNotifier {
// Appeler l'API pour supprimer l'opération active
final response = await ApiService.instance.delete('/operations/$id');
// Vérifier si la requête a été mise en queue (mode offline)
if (response.data != null && response.data['queued'] == true) {
// Mode offline : Supprimer localement et vider les données liées
await _clearAllRelatedBoxes();
await deleteOperation(id);
debugPrint('⏳ Suppression de l\'opération active $id mise en attente (mode hors ligne)');
return true;
}
if (response.statusCode == 200 || response.statusCode == 204) {
debugPrint(
'✅ Suppression opération active réussie - Traitement complet');

View File

@@ -1,5 +1,6 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:geosector_app/core/data/models/passage_model.dart';
import 'package:geosector_app/core/services/api_service.dart';
@@ -164,7 +165,7 @@ class PassageRepository extends ChangeNotifier {
}
// Créer un passage via l'API
Future<bool> createPassage(PassageModel passage) async {
Future<bool> createPassage(PassageModel passage, {BuildContext? context}) async {
_isLoading = true;
notifyListeners();
@@ -175,6 +176,67 @@ class PassageRepository extends ChangeNotifier {
// Appeler l'API pour créer le passage
final response = await ApiService.instance.post('/passages', data: data);
// Vérifier si la requête a été mise en file d'attente
if (response.data['queued'] == true) {
// Mode offline : NE PAS créer le passage dans Hive
// Il sera créé automatiquement après synchronisation
// Afficher un message explicatif
if (context != null && context.mounted) {
showDialog(
context: context,
barrierDismissible: false,
builder: (dialogContext) => AlertDialog(
icon: const Icon(Icons.cloud_queue, color: Colors.orange, size: 48),
title: const Text('Création en attente'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'Votre passage a été enregistré et sera créé dès que la connexion sera rétablie.',
style: TextStyle(fontSize: 14),
),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.orange.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.orange.shade200),
),
child: const Row(
children: [
Icon(Icons.info_outline, size: 16, color: Colors.orange),
SizedBox(width: 8),
Expanded(
child: Text(
'Le passage apparaîtra dans votre liste après synchronisation.',
style: TextStyle(fontSize: 13),
),
),
],
),
),
],
),
actions: [
ElevatedButton(
onPressed: () => Navigator.of(dialogContext).pop(),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange,
),
child: const Text('Compris'),
),
],
),
);
}
// Retourner true car la requête est bien en file d'attente
return true;
}
// Mode online : traitement normal
if (response.statusCode == 201 || response.statusCode == 200) {
// Récupérer l'ID du nouveau passage
final passageId = response.data['id'] is String ? int.parse(response.data['id']) : response.data['id'] as int;
@@ -200,7 +262,7 @@ class PassageRepository extends ChangeNotifier {
}
// Mettre à jour un passage via l'API
Future<bool> updatePassage(PassageModel passage) async {
Future<bool> updatePassage(PassageModel passage, {BuildContext? context}) async {
_isLoading = true;
notifyListeners();
@@ -210,6 +272,42 @@ class PassageRepository extends ChangeNotifier {
// Appeler l'API pour mettre à jour le passage
final response = await ApiService.instance.put('/passages/${passage.id}', data: data);
// Vérifier si la requête a été mise en file d'attente
if (response.data['queued'] == true) {
// Mode offline : mettre à jour localement et marquer comme non synchronisé
final offlinePassage = passage.copyWith(
lastSyncedAt: null,
isSynced: false,
);
await savePassage(offlinePassage);
// Afficher un message si un contexte est fourni
if (context != null && context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Row(
children: [
Icon(Icons.cloud_queue, color: Colors.white),
SizedBox(width: 12),
Expanded(
child: Text(
'Modification en attente de synchronisation',
),
),
],
),
backgroundColor: Colors.orange,
duration: Duration(seconds: 3),
),
);
}
return true;
}
// Mode online : traitement normal
if (response.statusCode == 200) {
// Mettre à jour le passage localement
final updatedPassage = passage.copyWith(
@@ -231,7 +329,7 @@ class PassageRepository extends ChangeNotifier {
}
// Supprimer un passage via l'API
Future<bool> deletePassageViaApi(int id) async {
Future<bool> deletePassageViaApi(int id, {BuildContext? context}) async {
_isLoading = true;
notifyListeners();
@@ -239,9 +337,31 @@ class PassageRepository extends ChangeNotifier {
// Appeler l'API pour supprimer le passage
final response = await ApiService.instance.delete('/passages/$id');
if (response.statusCode == 200 || response.statusCode == 204) {
if (response.statusCode == 200 || response.statusCode == 204 || response.data['queued'] == true) {
// Supprimer le passage localement
await deletePassage(id);
// Si mis en file d'attente, informer l'utilisateur
if (response.data['queued'] == true && context != null && context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Row(
children: [
Icon(Icons.cloud_queue, color: Colors.white),
SizedBox(width: 12),
Expanded(
child: Text(
'Suppression en attente de synchronisation',
),
),
],
),
backgroundColor: Colors.orange,
duration: Duration(seconds: 3),
),
);
}
return true;
}

View File

@@ -1,4 +1,5 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:dio/dio.dart';
import 'package:geosector_app/core/data/models/sector_model.dart';
@@ -138,7 +139,7 @@ class SectorRepository extends ChangeNotifier {
}
// Créer un nouveau secteur via l'API
Future<Map<String, dynamic>> createSector(SectorModel sector, {required List<int> users, required int fkEntite, required int operationId}) async {
Future<Map<String, dynamic>> createSector(SectorModel sector, {required List<int> users, required int fkEntite, required int operationId, BuildContext? context}) async {
try {
// Préparer les données à envoyer
final Map<String, dynamic> requestData = {
@@ -153,8 +154,44 @@ class SectorRepository extends ChangeNotifier {
data: requestData,
);
// Vérifier si la requête a été mise en queue (mode offline)
if (response.data != null && response.data['queued'] == true) {
// Mode offline : NE PAS créer le secteur dans Hive
debugPrint('⏳ Création du secteur mise en attente (mode hors ligne)');
// Informer l'utilisateur si un contexte est fourni
if (context != null && context.mounted) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Row(
children: [
Icon(Icons.cloud_off, color: Colors.orange),
SizedBox(width: 8),
Text('Création en attente'),
],
),
content: const Text(
'Le secteur sera créé dès que la connexion sera rétablie.\n\n'
'La création a été ajoutée à la file d\'attente.',
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Compris'),
),
],
),
);
}
return {
'status': 'queued',
'message': 'Création mise en attente'
};
}
// Gérer la réponse correctement
final dynamic responseRaw = response is Response ? response.data : response;
final dynamic responseRaw = response.data;
final Map<String, dynamic> responseData = Map<String, dynamic>.from(responseRaw as Map);
if (responseData['status'] == 'success') {
@@ -282,8 +319,19 @@ class SectorRepository extends ChangeNotifier {
data: requestData,
);
// Vérifier si la requête a été mise en queue (mode offline)
if (response.data != null && response.data['queued'] == true) {
// Mode offline : Sauvegarder localement avec indicateur
await saveSector(sector);
debugPrint('⏳ Modification du secteur ${sector.id} mise en attente (mode hors ligne)');
return {
'status': 'queued',
'message': 'Modification mise en attente'
};
}
// Gérer la réponse correctement
final dynamic responseRaw = response is Response ? response.data : response;
final dynamic responseRaw = response.data;
final Map<String, dynamic> responseData = Map<String, dynamic>.from(responseRaw as Map);
if (responseData['status'] == 'success') {
@@ -382,6 +430,19 @@ class SectorRepository extends ChangeNotifier {
final response = await ApiService.instance.delete(
'${AppKeys.sectorsEndpoint}/$id',
);
// Vérifier si la requête a été mise en queue (mode offline)
if (response.data != null && response.data['queued'] == true) {
// Mode offline : Supprimer localement immédiatement
await _deleteAllPassagesOfSector(id);
await deleteSector(id);
debugPrint('⏳ Suppression du secteur $id mise en attente (mode hors ligne)');
return {
'status': 'queued',
'message': 'Suppression mise en attente'
};
}
final Map<String, dynamic> responseData = response.data as Map<String, dynamic>;
if (responseData['status'] == 'success') {

View File

@@ -9,6 +9,8 @@ import 'package:geosector_app/core/services/current_amicale_service.dart';
import 'package:geosector_app/core/services/data_loading_service.dart';
import 'package:geosector_app/core/services/hive_service.dart';
import 'package:geosector_app/core/services/hive_reset_state_service.dart';
import 'package:geosector_app/core/services/chat_manager.dart';
import 'package:geosector_app/chat/services/chat_info_service.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/operation_model.dart';
@@ -17,7 +19,6 @@ import 'package:geosector_app/core/data/models/passage_model.dart';
import 'package:geosector_app/core/data/models/membre_model.dart';
import 'package:geosector_app/presentation/widgets/loading_spin_overlay.dart';
import 'package:geosector_app/core/models/loading_state.dart';
import 'package:geosector_app/chat/services/chat_info_service.dart';
class UserRepository extends ChangeNotifier {
bool _isLoading = false;
@@ -296,6 +297,15 @@ class UserRepository extends ChangeNotifier {
debugPrint('⚠️ Connexion réussie mais avec des données partielles');
}
// Initialiser le chat en arrière-plan après connexion réussie
try {
await ChatManager.instance.initializeChat();
debugPrint('✅ Module chat initialisé en arrière-plan');
} catch (chatError) {
// Ne pas bloquer la connexion si le chat échoue
debugPrint('⚠️ Erreur initialisation chat (non bloquant): $chatError');
}
debugPrint('✅ Connexion réussie');
return true;
} catch (e) {
@@ -314,6 +324,60 @@ class UserRepository extends ChangeNotifier {
try {
debugPrint('🚪 Déconnexion en cours...');
// Vérifier les requêtes en attente AVANT de déconnecter
int pendingCount = 0;
try {
if (Hive.isBoxOpen(AppKeys.pendingRequestsBoxName)) {
final pendingBox = Hive.box(AppKeys.pendingRequestsBoxName);
pendingCount = pendingBox.length;
if (pendingCount > 0) {
debugPrint('$pendingCount requêtes en attente trouvées');
// Afficher un avertissement non bloquant
if (context.mounted) {
showDialog(
context: context,
barrierDismissible: true,
builder: (BuildContext dialogContext) => AlertDialog(
title: const Row(
children: [
Icon(Icons.warning_amber, color: Colors.orange, size: 28),
SizedBox(width: 12),
Text('Données en attente'),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
pendingCount == 1
? '1 requête sera synchronisée lors de votre prochaine connexion.'
: '$pendingCount requêtes seront synchronisées lors de votre prochaine connexion.',
style: const TextStyle(fontSize: 16),
),
const SizedBox(height: 12),
const Text(
'Vos données sont conservées et seront envoyées automatiquement.',
style: TextStyle(fontSize: 14, color: Colors.grey),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(),
child: const Text('Compris'),
),
],
),
);
}
}
}
} catch (e) {
debugPrint('⚠️ Impossible de vérifier les requêtes en attente: $e');
}
try {
await logoutAPI();
@@ -328,6 +392,9 @@ class UserRepository extends ChangeNotifier {
await CurrentUserService.instance.clearUser();
await CurrentAmicaleService.instance.clearAmicale();
// Arrêter le chat (stoppe les syncs)
ChatManager.instance.dispose();
// Réinitialiser les infos chat
ChatInfoService.instance.reset();
@@ -431,31 +498,47 @@ class UserRepository extends ChangeNotifier {
// D'ABORD essayer de synchroniser avec l'API
try {
final hasConnection = await ApiService.instance.hasInternetConnection();
if (hasConnection) {
// Tentative de mise à jour sur l'API
await ApiService.instance.updateUser(updatedUser);
debugPrint('✅ Utilisateur mis à jour sur l\'API');
// Si succès API, sauvegarder localement avec sync = true
final syncedUser = updatedUser.copyWith(
isSynced: true,
lastSyncedAt: DateTime.now(),
// Tentative de mise à jour sur l'API (gère automatiquement le mode offline)
final response = await ApiService.instance.put('/users/${updatedUser.id}', data: updatedUser.toJson());
// Vérifier si la requête a été mise en queue (mode offline)
if (response.data != null && response.data['queued'] == true) {
// Mode offline : Sauvegarder localement avec isSynced = false
final unsyncedUser = updatedUser.copyWith(
isSynced: false,
lastSyncedAt: null,
);
await _userBox.put(syncedUser.id, syncedUser);
await _userBox.put(unsyncedUser.id, unsyncedUser);
// Si c'est l'utilisateur connecté, mettre à jour le service
if (currentUser?.id == syncedUser.id) {
await CurrentUserService.instance.setUser(syncedUser);
if (currentUser?.id == unsyncedUser.id) {
await CurrentUserService.instance.setUser(unsyncedUser);
}
debugPrint('⏳ Modification utilisateur ${updatedUser.id} mise en attente (mode hors ligne)');
notifyListeners();
return syncedUser;
} else {
debugPrint('⚠️ Pas de connexion internet');
throw Exception('Pas de connexion internet');
return unsyncedUser;
}
// Mode online : succès de la mise à jour
debugPrint('✅ Utilisateur mis à jour sur l\'API');
// Si succès API, sauvegarder localement avec sync = true
final syncedUser = updatedUser.copyWith(
isSynced: true,
lastSyncedAt: DateTime.now(),
);
await _userBox.put(syncedUser.id, syncedUser);
// Si c'est l'utilisateur connecté, mettre à jour le service
if (currentUser?.id == syncedUser.id) {
await CurrentUserService.instance.setUser(syncedUser);
}
notifyListeners();
return syncedUser;
} catch (apiError) {
debugPrint('❌ Erreur API lors de la mise à jour: $apiError');
// Relancer l'erreur pour qu'elle soit gérée par l'appelant