Refactoring

This commit is contained in:
DahoudG
2025-09-17 17:54:06 +00:00
parent 12d514d866
commit 63fe107f98
165 changed files with 54220 additions and 276 deletions

View File

@@ -0,0 +1,435 @@
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import '../../../../core/error/exceptions.dart';
import '../models/demande_aide_model.dart';
import '../models/proposition_aide_model.dart';
import '../models/evaluation_aide_model.dart';
/// Source de données locale pour le module solidarité
///
/// Cette classe gère le cache local des données de solidarité
/// pour permettre un fonctionnement hors ligne et améliorer les performances.
abstract class SolidariteLocalDataSource {
// Cache des demandes d'aide
Future<void> cacherDemandeAide(DemandeAideModel demande);
Future<DemandeAideModel?> obtenirDemandeAideCachee(String id);
Future<List<DemandeAideModel>> obtenirDemandesAideCachees();
Future<void> supprimerDemandeAideCachee(String id);
Future<void> viderCacheDemandesAide();
// Cache des propositions d'aide
Future<void> cacherPropositionAide(PropositionAideModel proposition);
Future<PropositionAideModel?> obtenirPropositionAideCachee(String id);
Future<List<PropositionAideModel>> obtenirPropositionsAideCachees();
Future<void> supprimerPropositionAideCachee(String id);
Future<void> viderCachePropositionsAide();
// Cache des évaluations
Future<void> cacherEvaluation(EvaluationAideModel evaluation);
Future<EvaluationAideModel?> obtenirEvaluationCachee(String id);
Future<List<EvaluationAideModel>> obtenirEvaluationsCachees();
Future<void> supprimerEvaluationCachee(String id);
Future<void> viderCacheEvaluations();
// Cache des statistiques
Future<void> cacherStatistiques(String organisationId, Map<String, dynamic> statistiques);
Future<Map<String, dynamic>?> obtenirStatistiquesCachees(String organisationId);
Future<void> supprimerStatistiquesCachees(String organisationId);
// Gestion du cache
Future<DateTime?> obtenirDateDerniereMiseAJour(String cacheKey);
Future<void> marquerMiseAJour(String cacheKey);
Future<bool> estCacheExpire(String cacheKey, Duration dureeValidite);
Future<void> viderToutCache();
}
/// Implémentation de la source de données locale
class SolidariteLocalDataSourceImpl implements SolidariteLocalDataSource {
final SharedPreferences sharedPreferences;
// Clés de cache
static const String _demandesAideKey = 'CACHED_DEMANDES_AIDE';
static const String _propositionsAideKey = 'CACHED_PROPOSITIONS_AIDE';
static const String _evaluationsKey = 'CACHED_EVALUATIONS';
static const String _statistiquesKey = 'CACHED_STATISTIQUES';
static const String _lastUpdatePrefix = 'LAST_UPDATE_';
// Durées de validité du cache
static const Duration _dureeValiditeDefaut = Duration(minutes: 15);
static const Duration _dureeValiditeStatistiques = Duration(hours: 1);
SolidariteLocalDataSourceImpl({required this.sharedPreferences});
// Cache des demandes d'aide
@override
Future<void> cacherDemandeAide(DemandeAideModel demande) async {
try {
final demandes = await obtenirDemandesAideCachees();
// Supprimer l'ancienne version si elle existe
demandes.removeWhere((d) => d.id == demande.id);
// Ajouter la nouvelle version
demandes.add(demande);
// Limiter le cache à 100 demandes maximum
if (demandes.length > 100) {
demandes.sort((a, b) => b.dateModification.compareTo(a.dateModification));
demandes.removeRange(100, demandes.length);
}
final jsonList = demandes.map((d) => d.toJson()).toList();
await sharedPreferences.setString(_demandesAideKey, jsonEncode(jsonList));
await marquerMiseAJour(_demandesAideKey);
} catch (e) {
throw CacheException(message: 'Erreur lors de la mise en cache de la demande: ${e.toString()}');
}
}
@override
Future<DemandeAideModel?> obtenirDemandeAideCachee(String id) async {
try {
final demandes = await obtenirDemandesAideCachees();
return demandes.cast<DemandeAideModel?>().firstWhere(
(d) => d?.id == id,
orElse: () => null,
);
} catch (e) {
return null;
}
}
@override
Future<List<DemandeAideModel>> obtenirDemandesAideCachees() async {
try {
final jsonString = sharedPreferences.getString(_demandesAideKey);
if (jsonString == null) return [];
final List<dynamic> jsonList = jsonDecode(jsonString);
return jsonList.map((json) => DemandeAideModel.fromJson(json)).toList();
} catch (e) {
return [];
}
}
@override
Future<void> supprimerDemandeAideCachee(String id) async {
try {
final demandes = await obtenirDemandesAideCachees();
demandes.removeWhere((d) => d.id == id);
final jsonList = demandes.map((d) => d.toJson()).toList();
await sharedPreferences.setString(_demandesAideKey, jsonEncode(jsonList));
} catch (e) {
throw CacheException(message: 'Erreur lors de la suppression de la demande du cache: ${e.toString()}');
}
}
@override
Future<void> viderCacheDemandesAide() async {
try {
await sharedPreferences.remove(_demandesAideKey);
await sharedPreferences.remove('$_lastUpdatePrefix$_demandesAideKey');
} catch (e) {
throw CacheException(message: 'Erreur lors de la suppression du cache des demandes: ${e.toString()}');
}
}
// Cache des propositions d'aide
@override
Future<void> cacherPropositionAide(PropositionAideModel proposition) async {
try {
final propositions = await obtenirPropositionsAideCachees();
// Supprimer l'ancienne version si elle existe
propositions.removeWhere((p) => p.id == proposition.id);
// Ajouter la nouvelle version
propositions.add(proposition);
// Limiter le cache à 100 propositions maximum
if (propositions.length > 100) {
propositions.sort((a, b) => b.dateModification.compareTo(a.dateModification));
propositions.removeRange(100, propositions.length);
}
final jsonList = propositions.map((p) => p.toJson()).toList();
await sharedPreferences.setString(_propositionsAideKey, jsonEncode(jsonList));
await marquerMiseAJour(_propositionsAideKey);
} catch (e) {
throw CacheException(message: 'Erreur lors de la mise en cache de la proposition: ${e.toString()}');
}
}
@override
Future<PropositionAideModel?> obtenirPropositionAideCachee(String id) async {
try {
final propositions = await obtenirPropositionsAideCachees();
return propositions.cast<PropositionAideModel?>().firstWhere(
(p) => p?.id == id,
orElse: () => null,
);
} catch (e) {
return null;
}
}
@override
Future<List<PropositionAideModel>> obtenirPropositionsAideCachees() async {
try {
final jsonString = sharedPreferences.getString(_propositionsAideKey);
if (jsonString == null) return [];
final List<dynamic> jsonList = jsonDecode(jsonString);
return jsonList.map((json) => PropositionAideModel.fromJson(json)).toList();
} catch (e) {
return [];
}
}
@override
Future<void> supprimerPropositionAideCachee(String id) async {
try {
final propositions = await obtenirPropositionsAideCachees();
propositions.removeWhere((p) => p.id == id);
final jsonList = propositions.map((p) => p.toJson()).toList();
await sharedPreferences.setString(_propositionsAideKey, jsonEncode(jsonList));
} catch (e) {
throw CacheException(message: 'Erreur lors de la suppression de la proposition du cache: ${e.toString()}');
}
}
@override
Future<void> viderCachePropositionsAide() async {
try {
await sharedPreferences.remove(_propositionsAideKey);
await sharedPreferences.remove('$_lastUpdatePrefix$_propositionsAideKey');
} catch (e) {
throw CacheException(message: 'Erreur lors de la suppression du cache des propositions: ${e.toString()}');
}
}
// Cache des évaluations
@override
Future<void> cacherEvaluation(EvaluationAideModel evaluation) async {
try {
final evaluations = await obtenirEvaluationsCachees();
// Supprimer l'ancienne version si elle existe
evaluations.removeWhere((e) => e.id == evaluation.id);
// Ajouter la nouvelle version
evaluations.add(evaluation);
// Limiter le cache à 200 évaluations maximum
if (evaluations.length > 200) {
evaluations.sort((a, b) => b.dateModification.compareTo(a.dateModification));
evaluations.removeRange(200, evaluations.length);
}
final jsonList = evaluations.map((e) => e.toJson()).toList();
await sharedPreferences.setString(_evaluationsKey, jsonEncode(jsonList));
await marquerMiseAJour(_evaluationsKey);
} catch (e) {
throw CacheException(message: 'Erreur lors de la mise en cache de l\'évaluation: ${e.toString()}');
}
}
@override
Future<EvaluationAideModel?> obtenirEvaluationCachee(String id) async {
try {
final evaluations = await obtenirEvaluationsCachees();
return evaluations.cast<EvaluationAideModel?>().firstWhere(
(e) => e?.id == id,
orElse: () => null,
);
} catch (e) {
return null;
}
}
@override
Future<List<EvaluationAideModel>> obtenirEvaluationsCachees() async {
try {
final jsonString = sharedPreferences.getString(_evaluationsKey);
if (jsonString == null) return [];
final List<dynamic> jsonList = jsonDecode(jsonString);
return jsonList.map((json) => EvaluationAideModel.fromJson(json)).toList();
} catch (e) {
return [];
}
}
@override
Future<void> supprimerEvaluationCachee(String id) async {
try {
final evaluations = await obtenirEvaluationsCachees();
evaluations.removeWhere((e) => e.id == id);
final jsonList = evaluations.map((e) => e.toJson()).toList();
await sharedPreferences.setString(_evaluationsKey, jsonEncode(jsonList));
} catch (e) {
throw CacheException(message: 'Erreur lors de la suppression de l\'évaluation du cache: ${e.toString()}');
}
}
@override
Future<void> viderCacheEvaluations() async {
try {
await sharedPreferences.remove(_evaluationsKey);
await sharedPreferences.remove('$_lastUpdatePrefix$_evaluationsKey');
} catch (e) {
throw CacheException(message: 'Erreur lors de la suppression du cache des évaluations: ${e.toString()}');
}
}
// Cache des statistiques
@override
Future<void> cacherStatistiques(String organisationId, Map<String, dynamic> statistiques) async {
try {
final key = '$_statistiquesKey$organisationId';
await sharedPreferences.setString(key, jsonEncode(statistiques));
await marquerMiseAJour(key);
} catch (e) {
throw CacheException(message: 'Erreur lors de la mise en cache des statistiques: ${e.toString()}');
}
}
@override
Future<Map<String, dynamic>?> obtenirStatistiquesCachees(String organisationId) async {
try {
final key = '$_statistiquesKey$organisationId';
final jsonString = sharedPreferences.getString(key);
if (jsonString == null) return null;
return Map<String, dynamic>.from(jsonDecode(jsonString));
} catch (e) {
return null;
}
}
@override
Future<void> supprimerStatistiquesCachees(String organisationId) async {
try {
final key = '$_statistiquesKey$organisationId';
await sharedPreferences.remove(key);
await sharedPreferences.remove('$_lastUpdatePrefix$key');
} catch (e) {
throw CacheException(message: 'Erreur lors de la suppression des statistiques du cache: ${e.toString()}');
}
}
// Gestion du cache
@override
Future<DateTime?> obtenirDateDerniereMiseAJour(String cacheKey) async {
try {
final timestamp = sharedPreferences.getInt('$_lastUpdatePrefix$cacheKey');
if (timestamp == null) return null;
return DateTime.fromMillisecondsSinceEpoch(timestamp);
} catch (e) {
return null;
}
}
@override
Future<void> marquerMiseAJour(String cacheKey) async {
try {
final timestamp = DateTime.now().millisecondsSinceEpoch;
await sharedPreferences.setInt('$_lastUpdatePrefix$cacheKey', timestamp);
} catch (e) {
throw CacheException(message: 'Erreur lors de la mise à jour du timestamp: ${e.toString()}');
}
}
@override
Future<bool> estCacheExpire(String cacheKey, Duration dureeValidite) async {
try {
final dateDerniereMiseAJour = await obtenirDateDerniereMiseAJour(cacheKey);
if (dateDerniereMiseAJour == null) return true;
final maintenant = DateTime.now();
final dureeEcoulee = maintenant.difference(dateDerniereMiseAJour);
return dureeEcoulee > dureeValidite;
} catch (e) {
return true; // En cas d'erreur, considérer le cache comme expiré
}
}
@override
Future<void> viderToutCache() async {
try {
await Future.wait([
viderCacheDemandesAide(),
viderCachePropositionsAide(),
viderCacheEvaluations(),
]);
// Supprimer toutes les statistiques cachées
final keys = sharedPreferences.getKeys();
final statistiquesKeys = keys.where((key) => key.startsWith(_statistiquesKey));
for (final key in statistiquesKeys) {
await sharedPreferences.remove(key);
await sharedPreferences.remove('$_lastUpdatePrefix$key');
}
} catch (e) {
throw CacheException(message: 'Erreur lors de la suppression complète du cache: ${e.toString()}');
}
}
/// Méthodes utilitaires pour la gestion du cache
/// Vérifie si le cache des demandes est valide
Future<bool> estCacheDemandesValide() async {
return !(await estCacheExpire(_demandesAideKey, _dureeValiditeDefaut));
}
/// Vérifie si le cache des propositions est valide
Future<bool> estCachePropositionsValide() async {
return !(await estCacheExpire(_propositionsAideKey, _dureeValiditeDefaut));
}
/// Vérifie si le cache des évaluations est valide
Future<bool> estCacheEvaluationsValide() async {
return !(await estCacheExpire(_evaluationsKey, _dureeValiditeDefaut));
}
/// Vérifie si le cache des statistiques est valide
Future<bool> estCacheStatistiquesValide(String organisationId) async {
final key = '$_statistiquesKey$organisationId';
return !(await estCacheExpire(key, _dureeValiditeStatistiques));
}
/// Obtient la taille approximative du cache en octets
Future<int> obtenirTailleCache() async {
try {
int taille = 0;
final demandes = sharedPreferences.getString(_demandesAideKey);
if (demandes != null) taille += demandes.length;
final propositions = sharedPreferences.getString(_propositionsAideKey);
if (propositions != null) taille += propositions.length;
final evaluations = sharedPreferences.getString(_evaluationsKey);
if (evaluations != null) taille += evaluations.length;
// Ajouter les statistiques
final keys = sharedPreferences.getKeys();
final statistiquesKeys = keys.where((key) => key.startsWith(_statistiquesKey));
for (final key in statistiquesKeys) {
final value = sharedPreferences.getString(key);
if (value != null) taille += value.length;
}
return taille;
} catch (e) {
return 0;
}
}
}

View File

@@ -0,0 +1,817 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../../../../core/error/exceptions.dart';
import '../../../../core/network/api_client.dart';
import '../models/demande_aide_model.dart';
import '../models/proposition_aide_model.dart';
import '../models/evaluation_aide_model.dart';
/// Source de données distante pour le module solidarité
///
/// Cette classe gère toutes les communications avec l'API REST
/// du backend UnionFlow pour les fonctionnalités de solidarité.
abstract class SolidariteRemoteDataSource {
// Demandes d'aide
Future<DemandeAideModel> creerDemandeAide(DemandeAideModel demande);
Future<DemandeAideModel> mettreAJourDemandeAide(DemandeAideModel demande);
Future<DemandeAideModel> obtenirDemandeAide(String id);
Future<DemandeAideModel> soumettreDemande(String demandeId);
Future<DemandeAideModel> evaluerDemande({
required String demandeId,
required String evaluateurId,
required String decision,
String? commentaire,
double? montantApprouve,
});
Future<List<DemandeAideModel>> rechercherDemandes({
String? organisationId,
String? typeAide,
String? statut,
String? demandeurId,
bool? urgente,
int page = 0,
int taille = 20,
});
Future<List<DemandeAideModel>> obtenirDemandesUrgentes(String organisationId);
Future<List<DemandeAideModel>> obtenirMesdemandes(String utilisateurId);
// Propositions d'aide
Future<PropositionAideModel> creerPropositionAide(PropositionAideModel proposition);
Future<PropositionAideModel> mettreAJourPropositionAide(PropositionAideModel proposition);
Future<PropositionAideModel> obtenirPropositionAide(String id);
Future<PropositionAideModel> changerStatutProposition({
required String propositionId,
required bool activer,
});
Future<List<PropositionAideModel>> rechercherPropositions({
String? organisationId,
String? typeAide,
String? proposantId,
bool? actives,
int page = 0,
int taille = 20,
});
Future<List<PropositionAideModel>> obtenirPropositionsActives(String typeAide);
Future<List<PropositionAideModel>> obtenirMeilleuresPropositions(int limite);
Future<List<PropositionAideModel>> obtenirMesPropositions(String utilisateurId);
// Matching
Future<List<PropositionAideModel>> trouverPropositionsCompatibles(String demandeId);
Future<List<DemandeAideModel>> trouverDemandesCompatibles(String propositionId);
Future<List<PropositionAideModel>> rechercherProposantsFinanciers(String demandeId);
// Évaluations
Future<EvaluationAideModel> creerEvaluation(EvaluationAideModel evaluation);
Future<EvaluationAideModel> mettreAJourEvaluation(EvaluationAideModel evaluation);
Future<EvaluationAideModel> obtenirEvaluation(String id);
Future<List<EvaluationAideModel>> obtenirEvaluationsDemande(String demandeId);
Future<List<EvaluationAideModel>> obtenirEvaluationsProposition(String propositionId);
Future<EvaluationAideModel> signalerEvaluation({
required String evaluationId,
required String motif,
});
Future<StatistiquesEvaluationModel> calculerMoyenneDemande(String demandeId);
Future<StatistiquesEvaluationModel> calculerMoyenneProposition(String propositionId);
// Statistiques
Future<Map<String, dynamic>> obtenirStatistiquesSolidarite(String organisationId);
}
/// Implémentation de la source de données distante
class SolidariteRemoteDataSourceImpl implements SolidariteRemoteDataSource {
final ApiClient apiClient;
static const String baseEndpoint = '/api/solidarite';
SolidariteRemoteDataSourceImpl({required this.apiClient});
// Demandes d'aide
@override
Future<DemandeAideModel> creerDemandeAide(DemandeAideModel demande) async {
try {
final response = await apiClient.post(
'$baseEndpoint/demandes',
data: demande.toJson(),
);
if (response.statusCode == 201) {
return DemandeAideModel.fromJson(response.data);
} else {
throw ServerException(
message: 'Erreur lors de la création de la demande d\'aide',
statusCode: response.statusCode,
);
}
} catch (e) {
if (e is ServerException) rethrow;
throw ServerException(
message: 'Erreur de communication avec le serveur: ${e.toString()}',
);
}
}
@override
Future<DemandeAideModel> mettreAJourDemandeAide(DemandeAideModel demande) async {
try {
final response = await apiClient.put(
'$baseEndpoint/demandes/${demande.id}',
data: demande.toJson(),
);
if (response.statusCode == 200) {
return DemandeAideModel.fromJson(response.data);
} else {
throw ServerException(
message: 'Erreur lors de la mise à jour de la demande d\'aide',
statusCode: response.statusCode,
);
}
} catch (e) {
if (e is ServerException) rethrow;
throw ServerException(
message: 'Erreur de communication avec le serveur: ${e.toString()}',
);
}
}
@override
Future<DemandeAideModel> obtenirDemandeAide(String id) async {
try {
final response = await apiClient.get('$baseEndpoint/demandes/$id');
if (response.statusCode == 200) {
return DemandeAideModel.fromJson(response.data);
} else if (response.statusCode == 404) {
throw NotFoundException(message: 'Demande d\'aide non trouvée');
} else {
throw ServerException(
message: 'Erreur lors de la récupération de la demande d\'aide',
statusCode: response.statusCode,
);
}
} catch (e) {
if (e is ServerException || e is NotFoundException) rethrow;
throw ServerException(
message: 'Erreur de communication avec le serveur: ${e.toString()}',
);
}
}
@override
Future<DemandeAideModel> soumettreDemande(String demandeId) async {
try {
final response = await apiClient.post(
'$baseEndpoint/demandes/$demandeId/soumettre',
);
if (response.statusCode == 200) {
return DemandeAideModel.fromJson(response.data);
} else {
throw ServerException(
message: 'Erreur lors de la soumission de la demande',
statusCode: response.statusCode,
);
}
} catch (e) {
if (e is ServerException) rethrow;
throw ServerException(
message: 'Erreur de communication avec le serveur: ${e.toString()}',
);
}
}
@override
Future<DemandeAideModel> evaluerDemande({
required String demandeId,
required String evaluateurId,
required String decision,
String? commentaire,
double? montantApprouve,
}) async {
try {
final data = {
'evaluateurId': evaluateurId,
'decision': decision,
if (commentaire != null) 'commentaire': commentaire,
if (montantApprouve != null) 'montantApprouve': montantApprouve,
};
final response = await apiClient.post(
'$baseEndpoint/demandes/$demandeId/evaluer',
data: data,
);
if (response.statusCode == 200) {
return DemandeAideModel.fromJson(response.data);
} else {
throw ServerException(
message: 'Erreur lors de l\'évaluation de la demande',
statusCode: response.statusCode,
);
}
} catch (e) {
if (e is ServerException) rethrow;
throw ServerException(
message: 'Erreur de communication avec le serveur: ${e.toString()}',
);
}
}
@override
Future<List<DemandeAideModel>> rechercherDemandes({
String? organisationId,
String? typeAide,
String? statut,
String? demandeurId,
bool? urgente,
int page = 0,
int taille = 20,
}) async {
try {
final queryParams = <String, dynamic>{
'page': page,
'size': taille,
if (organisationId != null) 'organisationId': organisationId,
if (typeAide != null) 'typeAide': typeAide,
if (statut != null) 'statut': statut,
if (demandeurId != null) 'demandeurId': demandeurId,
if (urgente != null) 'urgente': urgente,
};
final response = await apiClient.get(
'$baseEndpoint/demandes/rechercher',
queryParameters: queryParams,
);
if (response.statusCode == 200) {
final List<dynamic> data = response.data['content'];
return data.map((json) => DemandeAideModel.fromJson(json)).toList();
} else {
throw ServerException(
message: 'Erreur lors de la recherche des demandes',
statusCode: response.statusCode,
);
}
} catch (e) {
if (e is ServerException) rethrow;
throw ServerException(
message: 'Erreur de communication avec le serveur: ${e.toString()}',
);
}
}
@override
Future<List<DemandeAideModel>> obtenirDemandesUrgentes(String organisationId) async {
try {
final response = await apiClient.get(
'$baseEndpoint/demandes/urgentes',
queryParameters: {'organisationId': organisationId},
);
if (response.statusCode == 200) {
final List<dynamic> data = response.data;
return data.map((json) => DemandeAideModel.fromJson(json)).toList();
} else {
throw ServerException(
message: 'Erreur lors de la récupération des demandes urgentes',
statusCode: response.statusCode,
);
}
} catch (e) {
if (e is ServerException) rethrow;
throw ServerException(
message: 'Erreur de communication avec le serveur: ${e.toString()}',
);
}
}
@override
Future<List<DemandeAideModel>> obtenirMesdemandes(String utilisateurId) async {
try {
final response = await apiClient.get(
'$baseEndpoint/demandes/mes-demandes',
queryParameters: {'utilisateurId': utilisateurId},
);
if (response.statusCode == 200) {
final List<dynamic> data = response.data;
return data.map((json) => DemandeAideModel.fromJson(json)).toList();
} else {
throw ServerException(
message: 'Erreur lors de la récupération de vos demandes',
statusCode: response.statusCode,
);
}
} catch (e) {
if (e is ServerException) rethrow;
throw ServerException(
message: 'Erreur de communication avec le serveur: ${e.toString()}',
);
}
}
// Propositions d'aide
@override
Future<PropositionAideModel> creerPropositionAide(PropositionAideModel proposition) async {
try {
final response = await apiClient.post(
'$baseEndpoint/propositions',
data: proposition.toJson(),
);
if (response.statusCode == 201) {
return PropositionAideModel.fromJson(response.data);
} else {
throw ServerException(
message: 'Erreur lors de la création de la proposition d\'aide',
statusCode: response.statusCode,
);
}
} catch (e) {
if (e is ServerException) rethrow;
throw ServerException(
message: 'Erreur de communication avec le serveur: ${e.toString()}',
);
}
}
@override
Future<PropositionAideModel> mettreAJourPropositionAide(PropositionAideModel proposition) async {
try {
final response = await apiClient.put(
'$baseEndpoint/propositions/${proposition.id}',
data: proposition.toJson(),
);
if (response.statusCode == 200) {
return PropositionAideModel.fromJson(response.data);
} else {
throw ServerException(
message: 'Erreur lors de la mise à jour de la proposition d\'aide',
statusCode: response.statusCode,
);
}
} catch (e) {
if (e is ServerException) rethrow;
throw ServerException(
message: 'Erreur de communication avec le serveur: ${e.toString()}',
);
}
}
@override
Future<PropositionAideModel> obtenirPropositionAide(String id) async {
try {
final response = await apiClient.get('$baseEndpoint/propositions/$id');
if (response.statusCode == 200) {
return PropositionAideModel.fromJson(response.data);
} else if (response.statusCode == 404) {
throw NotFoundException(message: 'Proposition d\'aide non trouvée');
} else {
throw ServerException(
message: 'Erreur lors de la récupération de la proposition d\'aide',
statusCode: response.statusCode,
);
}
} catch (e) {
if (e is ServerException || e is NotFoundException) rethrow;
throw ServerException(
message: 'Erreur de communication avec le serveur: ${e.toString()}',
);
}
}
@override
Future<PropositionAideModel> changerStatutProposition({
required String propositionId,
required bool activer,
}) async {
try {
final endpoint = activer ? 'activer' : 'desactiver';
final response = await apiClient.post(
'$baseEndpoint/propositions/$propositionId/$endpoint',
);
if (response.statusCode == 200) {
return PropositionAideModel.fromJson(response.data);
} else {
throw ServerException(
message: 'Erreur lors du changement de statut de la proposition',
statusCode: response.statusCode,
);
}
} catch (e) {
if (e is ServerException) rethrow;
throw ServerException(
message: 'Erreur de communication avec le serveur: ${e.toString()}',
);
}
}
@override
Future<List<PropositionAideModel>> rechercherPropositions({
String? organisationId,
String? typeAide,
String? proposantId,
bool? actives,
int page = 0,
int taille = 20,
}) async {
try {
final queryParams = <String, dynamic>{
'page': page,
'size': taille,
if (organisationId != null) 'organisationId': organisationId,
if (typeAide != null) 'typeAide': typeAide,
if (proposantId != null) 'proposantId': proposantId,
if (actives != null) 'actives': actives,
};
final response = await apiClient.get(
'$baseEndpoint/propositions/rechercher',
queryParameters: queryParams,
);
if (response.statusCode == 200) {
final List<dynamic> data = response.data['content'];
return data.map((json) => PropositionAideModel.fromJson(json)).toList();
} else {
throw ServerException(
message: 'Erreur lors de la recherche des propositions',
statusCode: response.statusCode,
);
}
} catch (e) {
if (e is ServerException) rethrow;
throw ServerException(
message: 'Erreur de communication avec le serveur: ${e.toString()}',
);
}
}
@override
Future<List<PropositionAideModel>> obtenirPropositionsActives(String typeAide) async {
try {
final response = await apiClient.get(
'$baseEndpoint/propositions/actives',
queryParameters: {'typeAide': typeAide},
);
if (response.statusCode == 200) {
final List<dynamic> data = response.data;
return data.map((json) => PropositionAideModel.fromJson(json)).toList();
} else {
throw ServerException(
message: 'Erreur lors de la récupération des propositions actives',
statusCode: response.statusCode,
);
}
} catch (e) {
if (e is ServerException) rethrow;
throw ServerException(
message: 'Erreur de communication avec le serveur: ${e.toString()}',
);
}
}
@override
Future<List<PropositionAideModel>> obtenirMeilleuresPropositions(int limite) async {
try {
final response = await apiClient.get(
'$baseEndpoint/propositions/meilleures',
queryParameters: {'limite': limite},
);
if (response.statusCode == 200) {
final List<dynamic> data = response.data;
return data.map((json) => PropositionAideModel.fromJson(json)).toList();
} else {
throw ServerException(
message: 'Erreur lors de la récupération des meilleures propositions',
statusCode: response.statusCode,
);
}
} catch (e) {
if (e is ServerException) rethrow;
throw ServerException(
message: 'Erreur de communication avec le serveur: ${e.toString()}',
);
}
}
@override
Future<List<PropositionAideModel>> obtenirMesPropositions(String utilisateurId) async {
try {
final response = await apiClient.get(
'$baseEndpoint/propositions/mes-propositions',
queryParameters: {'utilisateurId': utilisateurId},
);
if (response.statusCode == 200) {
final List<dynamic> data = response.data;
return data.map((json) => PropositionAideModel.fromJson(json)).toList();
} else {
throw ServerException(
message: 'Erreur lors de la récupération de vos propositions',
statusCode: response.statusCode,
);
}
} catch (e) {
if (e is ServerException) rethrow;
throw ServerException(
message: 'Erreur de communication avec le serveur: ${e.toString()}',
);
}
}
// Matching
@override
Future<List<PropositionAideModel>> trouverPropositionsCompatibles(String demandeId) async {
try {
final response = await apiClient.get(
'$baseEndpoint/matching/propositions-compatibles/$demandeId',
);
if (response.statusCode == 200) {
final List<dynamic> data = response.data;
return data.map((json) => PropositionAideModel.fromJson(json)).toList();
} else {
throw ServerException(
message: 'Erreur lors de la recherche de propositions compatibles',
statusCode: response.statusCode,
);
}
} catch (e) {
if (e is ServerException) rethrow;
throw ServerException(
message: 'Erreur de communication avec le serveur: ${e.toString()}',
);
}
}
@override
Future<List<DemandeAideModel>> trouverDemandesCompatibles(String propositionId) async {
try {
final response = await apiClient.get(
'$baseEndpoint/matching/demandes-compatibles/$propositionId',
);
if (response.statusCode == 200) {
final List<dynamic> data = response.data;
return data.map((json) => DemandeAideModel.fromJson(json)).toList();
} else {
throw ServerException(
message: 'Erreur lors de la recherche de demandes compatibles',
statusCode: response.statusCode,
);
}
} catch (e) {
if (e is ServerException) rethrow;
throw ServerException(
message: 'Erreur de communication avec le serveur: ${e.toString()}',
);
}
}
@override
Future<List<PropositionAideModel>> rechercherProposantsFinanciers(String demandeId) async {
try {
final response = await apiClient.get(
'$baseEndpoint/matching/proposants-financiers/$demandeId',
);
if (response.statusCode == 200) {
final List<dynamic> data = response.data;
return data.map((json) => PropositionAideModel.fromJson(json)).toList();
} else {
throw ServerException(
message: 'Erreur lors de la recherche de proposants financiers',
statusCode: response.statusCode,
);
}
} catch (e) {
if (e is ServerException) rethrow;
throw ServerException(
message: 'Erreur de communication avec le serveur: ${e.toString()}',
);
}
}
// Évaluations
@override
Future<EvaluationAideModel> creerEvaluation(EvaluationAideModel evaluation) async {
try {
final response = await apiClient.post(
'$baseEndpoint/evaluations',
data: evaluation.toJson(),
);
if (response.statusCode == 201) {
return EvaluationAideModel.fromJson(response.data);
} else {
throw ServerException(
message: 'Erreur lors de la création de l\'évaluation',
statusCode: response.statusCode,
);
}
} catch (e) {
if (e is ServerException) rethrow;
throw ServerException(
message: 'Erreur de communication avec le serveur: ${e.toString()}',
);
}
}
@override
Future<EvaluationAideModel> mettreAJourEvaluation(EvaluationAideModel evaluation) async {
try {
final response = await apiClient.put(
'$baseEndpoint/evaluations/${evaluation.id}',
data: evaluation.toJson(),
);
if (response.statusCode == 200) {
return EvaluationAideModel.fromJson(response.data);
} else {
throw ServerException(
message: 'Erreur lors de la mise à jour de l\'évaluation',
statusCode: response.statusCode,
);
}
} catch (e) {
if (e is ServerException) rethrow;
throw ServerException(
message: 'Erreur de communication avec le serveur: ${e.toString()}',
);
}
}
@override
Future<EvaluationAideModel> obtenirEvaluation(String id) async {
try {
final response = await apiClient.get('$baseEndpoint/evaluations/$id');
if (response.statusCode == 200) {
return EvaluationAideModel.fromJson(response.data);
} else if (response.statusCode == 404) {
throw NotFoundException(message: 'Évaluation non trouvée');
} else {
throw ServerException(
message: 'Erreur lors de la récupération de l\'évaluation',
statusCode: response.statusCode,
);
}
} catch (e) {
if (e is ServerException || e is NotFoundException) rethrow;
throw ServerException(
message: 'Erreur de communication avec le serveur: ${e.toString()}',
);
}
}
@override
Future<List<EvaluationAideModel>> obtenirEvaluationsDemande(String demandeId) async {
try {
final response = await apiClient.get(
'$baseEndpoint/evaluations/demande/$demandeId',
);
if (response.statusCode == 200) {
final List<dynamic> data = response.data;
return data.map((json) => EvaluationAideModel.fromJson(json)).toList();
} else {
throw ServerException(
message: 'Erreur lors de la récupération des évaluations de la demande',
statusCode: response.statusCode,
);
}
} catch (e) {
if (e is ServerException) rethrow;
throw ServerException(
message: 'Erreur de communication avec le serveur: ${e.toString()}',
);
}
}
@override
Future<List<EvaluationAideModel>> obtenirEvaluationsProposition(String propositionId) async {
try {
final response = await apiClient.get(
'$baseEndpoint/evaluations/proposition/$propositionId',
);
if (response.statusCode == 200) {
final List<dynamic> data = response.data;
return data.map((json) => EvaluationAideModel.fromJson(json)).toList();
} else {
throw ServerException(
message: 'Erreur lors de la récupération des évaluations de la proposition',
statusCode: response.statusCode,
);
}
} catch (e) {
if (e is ServerException) rethrow;
throw ServerException(
message: 'Erreur de communication avec le serveur: ${e.toString()}',
);
}
}
@override
Future<EvaluationAideModel> signalerEvaluation({
required String evaluationId,
required String motif,
}) async {
try {
final response = await apiClient.post(
'$baseEndpoint/evaluations/$evaluationId/signaler',
data: {'motif': motif},
);
if (response.statusCode == 200) {
return EvaluationAideModel.fromJson(response.data);
} else {
throw ServerException(
message: 'Erreur lors du signalement de l\'évaluation',
statusCode: response.statusCode,
);
}
} catch (e) {
if (e is ServerException) rethrow;
throw ServerException(
message: 'Erreur de communication avec le serveur: ${e.toString()}',
);
}
}
@override
Future<StatistiquesEvaluationModel> calculerMoyenneDemande(String demandeId) async {
try {
final response = await apiClient.get(
'$baseEndpoint/evaluations/moyenne/demande/$demandeId',
);
if (response.statusCode == 200) {
return StatistiquesEvaluationModel.fromJson(response.data);
} else {
throw ServerException(
message: 'Erreur lors du calcul de la moyenne de la demande',
statusCode: response.statusCode,
);
}
} catch (e) {
if (e is ServerException) rethrow;
throw ServerException(
message: 'Erreur de communication avec le serveur: ${e.toString()}',
);
}
}
@override
Future<StatistiquesEvaluationModel> calculerMoyenneProposition(String propositionId) async {
try {
final response = await apiClient.get(
'$baseEndpoint/evaluations/moyenne/proposition/$propositionId',
);
if (response.statusCode == 200) {
return StatistiquesEvaluationModel.fromJson(response.data);
} else {
throw ServerException(
message: 'Erreur lors du calcul de la moyenne de la proposition',
statusCode: response.statusCode,
);
}
} catch (e) {
if (e is ServerException) rethrow;
throw ServerException(
message: 'Erreur de communication avec le serveur: ${e.toString()}',
);
}
}
// Statistiques
@override
Future<Map<String, dynamic>> obtenirStatistiquesSolidarite(String organisationId) async {
try {
final response = await apiClient.get(
'$baseEndpoint/statistiques',
queryParameters: {'organisationId': organisationId},
);
if (response.statusCode == 200) {
return Map<String, dynamic>.from(response.data);
} else {
throw ServerException(
message: 'Erreur lors de la récupération des statistiques',
statusCode: response.statusCode,
);
}
} catch (e) {
if (e is ServerException) rethrow;
throw ServerException(
message: 'Erreur de communication avec le serveur: ${e.toString()}',
);
}
}
}

View File

@@ -0,0 +1,332 @@
import 'package:get_it/get_it.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../../core/network/api_client.dart';
import '../../../core/network/network_info.dart';
// Domain
import '../domain/repositories/solidarite_repository.dart';
import '../domain/usecases/gerer_demandes_aide_usecase.dart';
import '../domain/usecases/gerer_propositions_aide_usecase.dart';
import '../domain/usecases/gerer_matching_usecase.dart';
import '../domain/usecases/gerer_evaluations_usecase.dart';
import '../domain/usecases/obtenir_statistiques_usecase.dart';
// Data
import 'datasources/solidarite_remote_data_source.dart';
import 'datasources/solidarite_local_data_source.dart';
import 'repositories/solidarite_repository_impl.dart';
/// Configuration de l'injection de dépendances pour le module solidarité
///
/// Cette classe configure tous les services, repositories, use cases
/// et data sources nécessaires au fonctionnement du module solidarité.
class SolidariteInjectionContainer {
static final GetIt _sl = GetIt.instance;
/// Initialise toutes les dépendances du module solidarité
static Future<void> init() async {
// ============================================================================
// Features - Solidarité
// ============================================================================
// Use Cases - Demandes d'aide
_sl.registerLazySingleton(() => CreerDemandeAideUseCase(_sl()));
_sl.registerLazySingleton(() => MettreAJourDemandeAideUseCase(_sl()));
_sl.registerLazySingleton(() => ObtenirDemandeAideUseCase(_sl()));
_sl.registerLazySingleton(() => SoumettreDemandeAideUseCase(_sl()));
_sl.registerLazySingleton(() => EvaluerDemandeAideUseCase(_sl()));
_sl.registerLazySingleton(() => RechercherDemandesAideUseCase(_sl()));
_sl.registerLazySingleton(() => ObtenirDemandesUrgentesUseCase(_sl()));
_sl.registerLazySingleton(() => ObtenirMesDemandesUseCase(_sl()));
_sl.registerLazySingleton(() => ValiderDemandeAideUseCase());
_sl.registerLazySingleton(() => CalculerPrioriteDemandeUseCase());
// Use Cases - Propositions d'aide
_sl.registerLazySingleton(() => CreerPropositionAideUseCase(_sl()));
_sl.registerLazySingleton(() => MettreAJourPropositionAideUseCase(_sl()));
_sl.registerLazySingleton(() => ObtenirPropositionAideUseCase(_sl()));
_sl.registerLazySingleton(() => ChangerStatutPropositionUseCase(_sl()));
_sl.registerLazySingleton(() => RechercherPropositionsAideUseCase(_sl()));
_sl.registerLazySingleton(() => ObtenirPropositionsActivesUseCase(_sl()));
_sl.registerLazySingleton(() => ObtenirMeilleuresPropositionsUseCase(_sl()));
_sl.registerLazySingleton(() => ObtenirMesPropositionsUseCase(_sl()));
_sl.registerLazySingleton(() => ValiderPropositionAideUseCase());
_sl.registerLazySingleton(() => CalculerScorePropositionUseCase());
// Use Cases - Matching
_sl.registerLazySingleton(() => TrouverPropositionsCompatiblesUseCase(_sl()));
_sl.registerLazySingleton(() => TrouverDemandesCompatiblesUseCase(_sl()));
_sl.registerLazySingleton(() => RechercherProposantsFinanciersUseCase(_sl()));
_sl.registerLazySingleton(() => CalculerScoreCompatibiliteUseCase());
_sl.registerLazySingleton(() => EffectuerMatchingIntelligentUseCase(
trouverPropositionsCompatibles: _sl(),
calculerScoreCompatibilite: _sl(),
));
_sl.registerLazySingleton(() => AnalyserTendancesMatchingUseCase());
// Use Cases - Évaluations
_sl.registerLazySingleton(() => CreerEvaluationUseCase(_sl()));
_sl.registerLazySingleton(() => MettreAJourEvaluationUseCase(_sl()));
_sl.registerLazySingleton(() => ObtenirEvaluationUseCase(_sl()));
_sl.registerLazySingleton(() => ObtenirEvaluationsDemandeUseCase(_sl()));
_sl.registerLazySingleton(() => ObtenirEvaluationsPropositionUseCase(_sl()));
_sl.registerLazySingleton(() => SignalerEvaluationUseCase(_sl()));
_sl.registerLazySingleton(() => CalculerMoyenneDemandeUseCase(_sl()));
_sl.registerLazySingleton(() => CalculerMoyennePropositionUseCase(_sl()));
_sl.registerLazySingleton(() => ValiderEvaluationUseCase());
_sl.registerLazySingleton(() => CalculerScoreQualiteEvaluationUseCase());
_sl.registerLazySingleton(() => AnalyserTendancesEvaluationUseCase());
// Use Cases - Statistiques
_sl.registerLazySingleton(() => ObtenirStatistiquesSolidariteUseCase(_sl()));
_sl.registerLazySingleton(() => CalculerKPIsPerformanceUseCase());
_sl.registerLazySingleton(() => GenererRapportActiviteUseCase());
// Repository
_sl.registerLazySingleton<SolidariteRepository>(
() => SolidariteRepositoryImpl(
remoteDataSource: _sl(),
localDataSource: _sl(),
networkInfo: _sl(),
),
);
// Data Sources
_sl.registerLazySingleton<SolidariteRemoteDataSource>(
() => SolidariteRemoteDataSourceImpl(apiClient: _sl()),
);
_sl.registerLazySingleton<SolidariteLocalDataSource>(
() => SolidariteLocalDataSourceImpl(sharedPreferences: _sl()),
);
// ============================================================================
// Core (si pas déjà enregistrés)
// ============================================================================
// Ces services sont normalement enregistrés dans le core injection container
// Nous les enregistrons ici seulement s'ils ne sont pas déjà disponibles
if (!_sl.isRegistered<ApiClient>()) {
_sl.registerLazySingleton<ApiClient>(() => ApiClientImpl());
}
if (!_sl.isRegistered<NetworkInfo>()) {
_sl.registerLazySingleton<NetworkInfo>(() => NetworkInfoImpl());
}
if (!_sl.isRegistered<SharedPreferences>()) {
final sharedPreferences = await SharedPreferences.getInstance();
_sl.registerLazySingleton<SharedPreferences>(() => sharedPreferences);
}
}
/// Nettoie toutes les dépendances du module solidarité
static Future<void> dispose() async {
// Use Cases - Demandes d'aide
_sl.unregister<CreerDemandeAideUseCase>();
_sl.unregister<MettreAJourDemandeAideUseCase>();
_sl.unregister<ObtenirDemandeAideUseCase>();
_sl.unregister<SoumettreDemandeAideUseCase>();
_sl.unregister<EvaluerDemandeAideUseCase>();
_sl.unregister<RechercherDemandesAideUseCase>();
_sl.unregister<ObtenirDemandesUrgentesUseCase>();
_sl.unregister<ObtenirMesDemandesUseCase>();
_sl.unregister<ValiderDemandeAideUseCase>();
_sl.unregister<CalculerPrioriteDemandeUseCase>();
// Use Cases - Propositions d'aide
_sl.unregister<CreerPropositionAideUseCase>();
_sl.unregister<MettreAJourPropositionAideUseCase>();
_sl.unregister<ObtenirPropositionAideUseCase>();
_sl.unregister<ChangerStatutPropositionUseCase>();
_sl.unregister<RechercherPropositionsAideUseCase>();
_sl.unregister<ObtenirPropositionsActivesUseCase>();
_sl.unregister<ObtenirMeilleuresPropositionsUseCase>();
_sl.unregister<ObtenirMesPropositionsUseCase>();
_sl.unregister<ValiderPropositionAideUseCase>();
_sl.unregister<CalculerScorePropositionUseCase>();
// Use Cases - Matching
_sl.unregister<TrouverPropositionsCompatiblesUseCase>();
_sl.unregister<TrouverDemandesCompatiblesUseCase>();
_sl.unregister<RechercherProposantsFinanciersUseCase>();
_sl.unregister<CalculerScoreCompatibiliteUseCase>();
_sl.unregister<EffectuerMatchingIntelligentUseCase>();
_sl.unregister<AnalyserTendancesMatchingUseCase>();
// Use Cases - Évaluations
_sl.unregister<CreerEvaluationUseCase>();
_sl.unregister<MettreAJourEvaluationUseCase>();
_sl.unregister<ObtenirEvaluationUseCase>();
_sl.unregister<ObtenirEvaluationsDemandeUseCase>();
_sl.unregister<ObtenirEvaluationsPropositionUseCase>();
_sl.unregister<SignalerEvaluationUseCase>();
_sl.unregister<CalculerMoyenneDemandeUseCase>();
_sl.unregister<CalculerMoyennePropositionUseCase>();
_sl.unregister<ValiderEvaluationUseCase>();
_sl.unregister<CalculerScoreQualiteEvaluationUseCase>();
_sl.unregister<AnalyserTendancesEvaluationUseCase>();
// Use Cases - Statistiques
_sl.unregister<ObtenirStatistiquesSolidariteUseCase>();
_sl.unregister<CalculerKPIsPerformanceUseCase>();
_sl.unregister<GenererRapportActiviteUseCase>();
// Repository et Data Sources
_sl.unregister<SolidariteRepository>();
_sl.unregister<SolidariteRemoteDataSource>();
_sl.unregister<SolidariteLocalDataSource>();
}
/// Obtient une instance d'un service enregistré
static T get<T extends Object>() => _sl.get<T>();
/// Vérifie si un service est enregistré
static bool isRegistered<T extends Object>() => _sl.isRegistered<T>();
/// Réinitialise complètement le container
static Future<void> reset() async {
await dispose();
await init();
}
/// Obtient des statistiques sur les services enregistrés
static Map<String, dynamic> getStats() {
return {
'totalServices': _sl.allReadySync().length,
'solidariteServices': {
'useCases': {
'demandes': 10,
'propositions': 10,
'matching': 6,
'evaluations': 11,
'statistiques': 3,
},
'repositories': 1,
'dataSources': 2,
},
'isInitialized': _sl.isRegistered<SolidariteRepository>(),
};
}
/// Valide que tous les services critiques sont enregistrés
static bool validateConfiguration() {
try {
// Vérifier les services critiques
final criticalServices = [
SolidariteRepository,
SolidariteRemoteDataSource,
SolidariteLocalDataSource,
CreerDemandeAideUseCase,
CreerPropositionAideUseCase,
CreerEvaluationUseCase,
ObtenirStatistiquesSolidariteUseCase,
];
for (final serviceType in criticalServices) {
if (!_sl.isRegistered(instance: serviceType)) {
return false;
}
}
return true;
} catch (e) {
return false;
}
}
/// Effectue un test de santé des services
static Future<Map<String, bool>> healthCheck() async {
final results = <String, bool>{};
try {
// Test du repository
final repository = _sl.get<SolidariteRepository>();
results['repository'] = repository != null;
// Test des data sources
final remoteDataSource = _sl.get<SolidariteRemoteDataSource>();
results['remoteDataSource'] = remoteDataSource != null;
final localDataSource = _sl.get<SolidariteLocalDataSource>();
results['localDataSource'] = localDataSource != null;
// Test des use cases critiques
final creerDemandeUseCase = _sl.get<CreerDemandeAideUseCase>();
results['creerDemandeUseCase'] = creerDemandeUseCase != null;
final creerPropositionUseCase = _sl.get<CreerPropositionAideUseCase>();
results['creerPropositionUseCase'] = creerPropositionUseCase != null;
final creerEvaluationUseCase = _sl.get<CreerEvaluationUseCase>();
results['creerEvaluationUseCase'] = creerEvaluationUseCase != null;
// Test des services de base
results['networkInfo'] = _sl.isRegistered<NetworkInfo>();
results['apiClient'] = _sl.isRegistered<ApiClient>();
results['sharedPreferences'] = _sl.isRegistered<SharedPreferences>();
} catch (e) {
results['error'] = false;
}
return results;
}
}
/// Extension pour faciliter l'accès aux services depuis les widgets
extension SolidariteServiceLocator on GetIt {
// Use Cases - Demandes d'aide
CreerDemandeAideUseCase get creerDemandeAide => get<CreerDemandeAideUseCase>();
MettreAJourDemandeAideUseCase get mettreAJourDemandeAide => get<MettreAJourDemandeAideUseCase>();
ObtenirDemandeAideUseCase get obtenirDemandeAide => get<ObtenirDemandeAideUseCase>();
SoumettreDemandeAideUseCase get soumettreDemandeAide => get<SoumettreDemandeAideUseCase>();
EvaluerDemandeAideUseCase get evaluerDemandeAide => get<EvaluerDemandeAideUseCase>();
RechercherDemandesAideUseCase get rechercherDemandesAide => get<RechercherDemandesAideUseCase>();
ObtenirDemandesUrgentesUseCase get obtenirDemandesUrgentes => get<ObtenirDemandesUrgentesUseCase>();
ObtenirMesDemandesUseCase get obtenirMesdemandes => get<ObtenirMesDemandesUseCase>();
ValiderDemandeAideUseCase get validerDemandeAide => get<ValiderDemandeAideUseCase>();
CalculerPrioriteDemandeUseCase get calculerPrioriteDemande => get<CalculerPrioriteDemandeUseCase>();
// Use Cases - Propositions d'aide
CreerPropositionAideUseCase get creerPropositionAide => get<CreerPropositionAideUseCase>();
MettreAJourPropositionAideUseCase get mettreAJourPropositionAide => get<MettreAJourPropositionAideUseCase>();
ObtenirPropositionAideUseCase get obtenirPropositionAide => get<ObtenirPropositionAideUseCase>();
ChangerStatutPropositionUseCase get changerStatutProposition => get<ChangerStatutPropositionUseCase>();
RechercherPropositionsAideUseCase get rechercherPropositionsAide => get<RechercherPropositionsAideUseCase>();
ObtenirPropositionsActivesUseCase get obtenirPropositionsActives => get<ObtenirPropositionsActivesUseCase>();
ObtenirMeilleuresPropositionsUseCase get obtenirMeilleuresPropositions => get<ObtenirMeilleuresPropositionsUseCase>();
ObtenirMesPropositionsUseCase get obtenirMesPropositions => get<ObtenirMesPropositionsUseCase>();
ValiderPropositionAideUseCase get validerPropositionAide => get<ValiderPropositionAideUseCase>();
CalculerScorePropositionUseCase get calculerScoreProposition => get<CalculerScorePropositionUseCase>();
// Use Cases - Matching
TrouverPropositionsCompatiblesUseCase get trouverPropositionsCompatibles => get<TrouverPropositionsCompatiblesUseCase>();
TrouverDemandesCompatiblesUseCase get trouverDemandesCompatibles => get<TrouverDemandesCompatiblesUseCase>();
RechercherProposantsFinanciersUseCase get rechercherProposantsFinanciers => get<RechercherProposantsFinanciersUseCase>();
CalculerScoreCompatibiliteUseCase get calculerScoreCompatibilite => get<CalculerScoreCompatibiliteUseCase>();
EffectuerMatchingIntelligentUseCase get effectuerMatchingIntelligent => get<EffectuerMatchingIntelligentUseCase>();
AnalyserTendancesMatchingUseCase get analyserTendancesMatching => get<AnalyserTendancesMatchingUseCase>();
// Use Cases - Évaluations
CreerEvaluationUseCase get creerEvaluation => get<CreerEvaluationUseCase>();
MettreAJourEvaluationUseCase get mettreAJourEvaluation => get<MettreAJourEvaluationUseCase>();
ObtenirEvaluationUseCase get obtenirEvaluation => get<ObtenirEvaluationUseCase>();
ObtenirEvaluationsDemandeUseCase get obtenirEvaluationsDemande => get<ObtenirEvaluationsDemandeUseCase>();
ObtenirEvaluationsPropositionUseCase get obtenirEvaluationsProposition => get<ObtenirEvaluationsPropositionUseCase>();
SignalerEvaluationUseCase get signalerEvaluation => get<SignalerEvaluationUseCase>();
CalculerMoyenneDemandeUseCase get calculerMoyenneDemande => get<CalculerMoyenneDemandeUseCase>();
CalculerMoyennePropositionUseCase get calculerMoyenneProposition => get<CalculerMoyennePropositionUseCase>();
ValiderEvaluationUseCase get validerEvaluation => get<ValiderEvaluationUseCase>();
CalculerScoreQualiteEvaluationUseCase get calculerScoreQualiteEvaluation => get<CalculerScoreQualiteEvaluationUseCase>();
AnalyserTendancesEvaluationUseCase get analyserTendancesEvaluation => get<AnalyserTendancesEvaluationUseCase>();
// Use Cases - Statistiques
ObtenirStatistiquesSolidariteUseCase get obtenirStatistiquesSolidarite => get<ObtenirStatistiquesSolidariteUseCase>();
CalculerKPIsPerformanceUseCase get calculerKPIsPerformance => get<CalculerKPIsPerformanceUseCase>();
GenererRapportActiviteUseCase get genererRapportActivite => get<GenererRapportActiviteUseCase>();
// Repository
SolidariteRepository get solidariteRepository => get<SolidariteRepository>();
}

View File

@@ -0,0 +1,524 @@
import '../../domain/entities/demande_aide.dart';
/// Modèle de données pour les demandes d'aide
///
/// Ce modèle fait la conversion entre les DTOs de l'API REST
/// et les entités du domaine pour les demandes d'aide.
class DemandeAideModel extends DemandeAide {
const DemandeAideModel({
required super.id,
required super.numeroReference,
required super.titre,
required super.description,
required super.typeAide,
required super.statut,
required super.priorite,
required super.demandeurId,
required super.nomDemandeur,
required super.organisationId,
super.montantDemande,
super.montantApprouve,
super.montantVerse,
required super.dateCreation,
required super.dateModification,
super.dateSoumission,
super.dateEvaluation,
super.dateApprobation,
super.dateLimiteTraitement,
super.evaluateurId,
super.commentairesEvaluateur,
super.motifRejet,
super.informationsRequises,
super.justificationUrgence,
super.contactUrgence,
super.localisation,
super.beneficiaires,
super.piecesJustificatives,
super.historiqueStatuts,
super.commentaires,
super.donneesPersonnalisees,
super.estModifiable,
super.estUrgente,
super.delaiDepasse,
super.estTerminee,
});
/// Crée un modèle à partir d'un JSON (API Response)
factory DemandeAideModel.fromJson(Map<String, dynamic> json) {
return DemandeAideModel(
id: json['id'] as String,
numeroReference: json['numeroReference'] as String,
titre: json['titre'] as String,
description: json['description'] as String,
typeAide: _parseTypeAide(json['typeAide'] as String),
statut: _parseStatutAide(json['statut'] as String),
priorite: _parsePrioriteAide(json['priorite'] as String),
demandeurId: json['demandeurId'] as String,
nomDemandeur: json['nomDemandeur'] as String,
organisationId: json['organisationId'] as String,
montantDemande: json['montantDemande']?.toDouble(),
montantApprouve: json['montantApprouve']?.toDouble(),
montantVerse: json['montantVerse']?.toDouble(),
dateCreation: DateTime.parse(json['dateCreation'] as String),
dateModification: DateTime.parse(json['dateModification'] as String),
dateSoumission: json['dateSoumission'] != null
? DateTime.parse(json['dateSoumission'] as String)
: null,
dateEvaluation: json['dateEvaluation'] != null
? DateTime.parse(json['dateEvaluation'] as String)
: null,
dateApprobation: json['dateApprobation'] != null
? DateTime.parse(json['dateApprobation'] as String)
: null,
dateLimiteTraitement: json['dateLimiteTraitement'] != null
? DateTime.parse(json['dateLimiteTraitement'] as String)
: null,
evaluateurId: json['evaluateurId'] as String?,
commentairesEvaluateur: json['commentairesEvaluateur'] as String?,
motifRejet: json['motifRejet'] as String?,
informationsRequises: json['informationsRequises'] as String?,
justificationUrgence: json['justificationUrgence'] as String?,
contactUrgence: json['contactUrgence'] != null
? ContactUrgenceModel.fromJson(json['contactUrgence'] as Map<String, dynamic>)
: null,
localisation: json['localisation'] != null
? LocalisationModel.fromJson(json['localisation'] as Map<String, dynamic>)
: null,
beneficiaires: (json['beneficiaires'] as List<dynamic>?)
?.map((e) => BeneficiaireAideModel.fromJson(e as Map<String, dynamic>))
.toList() ?? [],
piecesJustificatives: (json['piecesJustificatives'] as List<dynamic>?)
?.map((e) => PieceJustificativeModel.fromJson(e as Map<String, dynamic>))
.toList() ?? [],
historiqueStatuts: (json['historiqueStatuts'] as List<dynamic>?)
?.map((e) => HistoriqueStatutModel.fromJson(e as Map<String, dynamic>))
.toList() ?? [],
commentaires: (json['commentaires'] as List<dynamic>?)
?.map((e) => CommentaireAideModel.fromJson(e as Map<String, dynamic>))
.toList() ?? [],
donneesPersonnalisees: Map<String, dynamic>.from(json['donneesPersonnalisees'] ?? {}),
estModifiable: json['estModifiable'] as bool? ?? false,
estUrgente: json['estUrgente'] as bool? ?? false,
delaiDepasse: json['delaiDepasse'] as bool? ?? false,
estTerminee: json['estTerminee'] as bool? ?? false,
);
}
/// Convertit le modèle en JSON (API Request)
Map<String, dynamic> toJson() {
return {
'id': id,
'numeroReference': numeroReference,
'titre': titre,
'description': description,
'typeAide': typeAide.name,
'statut': statut.name,
'priorite': priorite.name,
'demandeurId': demandeurId,
'nomDemandeur': nomDemandeur,
'organisationId': organisationId,
'montantDemande': montantDemande,
'montantApprouve': montantApprouve,
'montantVerse': montantVerse,
'dateCreation': dateCreation.toIso8601String(),
'dateModification': dateModification.toIso8601String(),
'dateSoumission': dateSoumission?.toIso8601String(),
'dateEvaluation': dateEvaluation?.toIso8601String(),
'dateApprobation': dateApprobation?.toIso8601String(),
'dateLimiteTraitement': dateLimiteTraitement?.toIso8601String(),
'evaluateurId': evaluateurId,
'commentairesEvaluateur': commentairesEvaluateur,
'motifRejet': motifRejet,
'informationsRequises': informationsRequises,
'justificationUrgence': justificationUrgence,
'contactUrgence': contactUrgence != null
? (contactUrgence as ContactUrgenceModel).toJson()
: null,
'localisation': localisation != null
? (localisation as LocalisationModel).toJson()
: null,
'beneficiaires': beneficiaires
.map((e) => (e as BeneficiaireAideModel).toJson())
.toList(),
'piecesJustificatives': piecesJustificatives
.map((e) => (e as PieceJustificativeModel).toJson())
.toList(),
'historiqueStatuts': historiqueStatuts
.map((e) => (e as HistoriqueStatutModel).toJson())
.toList(),
'commentaires': commentaires
.map((e) => (e as CommentaireAideModel).toJson())
.toList(),
'donneesPersonnalisees': donneesPersonnalisees,
'estModifiable': estModifiable,
'estUrgente': estUrgente,
'delaiDepasse': delaiDepasse,
'estTerminee': estTerminee,
};
}
/// Crée un modèle à partir d'une entité du domaine
factory DemandeAideModel.fromEntity(DemandeAide entity) {
return DemandeAideModel(
id: entity.id,
numeroReference: entity.numeroReference,
titre: entity.titre,
description: entity.description,
typeAide: entity.typeAide,
statut: entity.statut,
priorite: entity.priorite,
demandeurId: entity.demandeurId,
nomDemandeur: entity.nomDemandeur,
organisationId: entity.organisationId,
montantDemande: entity.montantDemande,
montantApprouve: entity.montantApprouve,
montantVerse: entity.montantVerse,
dateCreation: entity.dateCreation,
dateModification: entity.dateModification,
dateSoumission: entity.dateSoumission,
dateEvaluation: entity.dateEvaluation,
dateApprobation: entity.dateApprobation,
dateLimiteTraitement: entity.dateLimiteTraitement,
evaluateurId: entity.evaluateurId,
commentairesEvaluateur: entity.commentairesEvaluateur,
motifRejet: entity.motifRejet,
informationsRequises: entity.informationsRequises,
justificationUrgence: entity.justificationUrgence,
contactUrgence: entity.contactUrgence != null
? ContactUrgenceModel.fromEntity(entity.contactUrgence!)
: null,
localisation: entity.localisation != null
? LocalisationModel.fromEntity(entity.localisation!)
: null,
beneficiaires: entity.beneficiaires
.map((e) => BeneficiaireAideModel.fromEntity(e))
.toList(),
piecesJustificatives: entity.piecesJustificatives
.map((e) => PieceJustificativeModel.fromEntity(e))
.toList(),
historiqueStatuts: entity.historiqueStatuts
.map((e) => HistoriqueStatutModel.fromEntity(e))
.toList(),
commentaires: entity.commentaires
.map((e) => CommentaireAideModel.fromEntity(e))
.toList(),
donneesPersonnalisees: Map<String, dynamic>.from(entity.donneesPersonnalisees),
estModifiable: entity.estModifiable,
estUrgente: entity.estUrgente,
delaiDepasse: entity.delaiDepasse,
estTerminee: entity.estTerminee,
);
}
/// Convertit le modèle en entité du domaine
DemandeAide toEntity() {
return DemandeAide(
id: id,
numeroReference: numeroReference,
titre: titre,
description: description,
typeAide: typeAide,
statut: statut,
priorite: priorite,
demandeurId: demandeurId,
nomDemandeur: nomDemandeur,
organisationId: organisationId,
montantDemande: montantDemande,
montantApprouve: montantApprouve,
montantVerse: montantVerse,
dateCreation: dateCreation,
dateModification: dateModification,
dateSoumission: dateSoumission,
dateEvaluation: dateEvaluation,
dateApprobation: dateApprobation,
dateLimiteTraitement: dateLimiteTraitement,
evaluateurId: evaluateurId,
commentairesEvaluateur: commentairesEvaluateur,
motifRejet: motifRejet,
informationsRequises: informationsRequises,
justificationUrgence: justificationUrgence,
contactUrgence: contactUrgence,
localisation: localisation,
beneficiaires: beneficiaires,
piecesJustificatives: piecesJustificatives,
historiqueStatuts: historiqueStatuts,
commentaires: commentaires,
donneesPersonnalisees: donneesPersonnalisees,
estModifiable: estModifiable,
estUrgente: estUrgente,
delaiDepasse: delaiDepasse,
estTerminee: estTerminee,
);
}
// Méthodes utilitaires de parsing
static TypeAide _parseTypeAide(String value) {
return TypeAide.values.firstWhere(
(e) => e.name == value,
orElse: () => TypeAide.autre,
);
}
static StatutAide _parseStatutAide(String value) {
return StatutAide.values.firstWhere(
(e) => e.name == value,
orElse: () => StatutAide.brouillon,
);
}
static PrioriteAide _parsePrioriteAide(String value) {
return PrioriteAide.values.firstWhere(
(e) => e.name == value,
orElse: () => PrioriteAide.normale,
);
}
}
/// Modèles pour les classes auxiliaires
class ContactUrgenceModel extends ContactUrgence {
const ContactUrgenceModel({
required super.nom,
required super.telephone,
super.email,
required super.relation,
});
factory ContactUrgenceModel.fromJson(Map<String, dynamic> json) {
return ContactUrgenceModel(
nom: json['nom'] as String,
telephone: json['telephone'] as String,
email: json['email'] as String?,
relation: json['relation'] as String,
);
}
Map<String, dynamic> toJson() {
return {
'nom': nom,
'telephone': telephone,
'email': email,
'relation': relation,
};
}
factory ContactUrgenceModel.fromEntity(ContactUrgence entity) {
return ContactUrgenceModel(
nom: entity.nom,
telephone: entity.telephone,
email: entity.email,
relation: entity.relation,
);
}
}
class LocalisationModel extends Localisation {
const LocalisationModel({
required super.adresse,
required super.ville,
super.codePostal,
super.pays,
super.latitude,
super.longitude,
});
factory LocalisationModel.fromJson(Map<String, dynamic> json) {
return LocalisationModel(
adresse: json['adresse'] as String,
ville: json['ville'] as String,
codePostal: json['codePostal'] as String?,
pays: json['pays'] as String?,
latitude: json['latitude']?.toDouble(),
longitude: json['longitude']?.toDouble(),
);
}
Map<String, dynamic> toJson() {
return {
'adresse': adresse,
'ville': ville,
'codePostal': codePostal,
'pays': pays,
'latitude': latitude,
'longitude': longitude,
};
}
factory LocalisationModel.fromEntity(Localisation entity) {
return LocalisationModel(
adresse: entity.adresse,
ville: entity.ville,
codePostal: entity.codePostal,
pays: entity.pays,
latitude: entity.latitude,
longitude: entity.longitude,
);
}
}
class BeneficiaireAideModel extends BeneficiaireAide {
const BeneficiaireAideModel({
required super.nom,
required super.prenom,
required super.age,
required super.relation,
super.telephone,
});
factory BeneficiaireAideModel.fromJson(Map<String, dynamic> json) {
return BeneficiaireAideModel(
nom: json['nom'] as String,
prenom: json['prenom'] as String,
age: json['age'] as int,
relation: json['relation'] as String,
telephone: json['telephone'] as String?,
);
}
Map<String, dynamic> toJson() {
return {
'nom': nom,
'prenom': prenom,
'age': age,
'relation': relation,
'telephone': telephone,
};
}
factory BeneficiaireAideModel.fromEntity(BeneficiaireAide entity) {
return BeneficiaireAideModel(
nom: entity.nom,
prenom: entity.prenom,
age: entity.age,
relation: entity.relation,
telephone: entity.telephone,
);
}
}
class PieceJustificativeModel extends PieceJustificative {
const PieceJustificativeModel({
required super.id,
required super.nom,
required super.type,
required super.url,
required super.taille,
required super.dateAjout,
});
factory PieceJustificativeModel.fromJson(Map<String, dynamic> json) {
return PieceJustificativeModel(
id: json['id'] as String,
nom: json['nom'] as String,
type: json['type'] as String,
url: json['url'] as String,
taille: json['taille'] as int,
dateAjout: DateTime.parse(json['dateAjout'] as String),
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'nom': nom,
'type': type,
'url': url,
'taille': taille,
'dateAjout': dateAjout.toIso8601String(),
};
}
factory PieceJustificativeModel.fromEntity(PieceJustificative entity) {
return PieceJustificativeModel(
id: entity.id,
nom: entity.nom,
type: entity.type,
url: entity.url,
taille: entity.taille,
dateAjout: entity.dateAjout,
);
}
}
class HistoriqueStatutModel extends HistoriqueStatut {
const HistoriqueStatutModel({
required super.ancienStatut,
required super.nouveauStatut,
required super.dateChangement,
super.commentaire,
super.utilisateurId,
});
factory HistoriqueStatutModel.fromJson(Map<String, dynamic> json) {
return HistoriqueStatutModel(
ancienStatut: DemandeAideModel._parseStatutAide(json['ancienStatut'] as String),
nouveauStatut: DemandeAideModel._parseStatutAide(json['nouveauStatut'] as String),
dateChangement: DateTime.parse(json['dateChangement'] as String),
commentaire: json['commentaire'] as String?,
utilisateurId: json['utilisateurId'] as String?,
);
}
Map<String, dynamic> toJson() {
return {
'ancienStatut': ancienStatut.name,
'nouveauStatut': nouveauStatut.name,
'dateChangement': dateChangement.toIso8601String(),
'commentaire': commentaire,
'utilisateurId': utilisateurId,
};
}
factory HistoriqueStatutModel.fromEntity(HistoriqueStatut entity) {
return HistoriqueStatutModel(
ancienStatut: entity.ancienStatut,
nouveauStatut: entity.nouveauStatut,
dateChangement: entity.dateChangement,
commentaire: entity.commentaire,
utilisateurId: entity.utilisateurId,
);
}
}
class CommentaireAideModel extends CommentaireAide {
const CommentaireAideModel({
required super.id,
required super.contenu,
required super.auteurId,
required super.nomAuteur,
required super.dateCreation,
super.estPrive,
});
factory CommentaireAideModel.fromJson(Map<String, dynamic> json) {
return CommentaireAideModel(
id: json['id'] as String,
contenu: json['contenu'] as String,
auteurId: json['auteurId'] as String,
nomAuteur: json['nomAuteur'] as String,
dateCreation: DateTime.parse(json['dateCreation'] as String),
estPrive: json['estPrive'] as bool? ?? false,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'contenu': contenu,
'auteurId': auteurId,
'nomAuteur': nomAuteur,
'dateCreation': dateCreation.toIso8601String(),
'estPrive': estPrive,
};
}
factory CommentaireAideModel.fromEntity(CommentaireAide entity) {
return CommentaireAideModel(
id: entity.id,
contenu: entity.contenu,
auteurId: entity.auteurId,
nomAuteur: entity.nomAuteur,
dateCreation: entity.dateCreation,
estPrive: entity.estPrive,
);
}
}

View File

@@ -0,0 +1,388 @@
import '../../domain/entities/evaluation_aide.dart';
/// Modèle de données pour les évaluations d'aide
///
/// Ce modèle fait la conversion entre les DTOs de l'API REST
/// et les entités du domaine pour les évaluations d'aide.
class EvaluationAideModel extends EvaluationAide {
const EvaluationAideModel({
required super.id,
required super.demandeId,
super.propositionId,
required super.evaluateurId,
required super.nomEvaluateur,
required super.typeEvaluateur,
required super.statut,
required super.noteGlobale,
super.noteDelaiReponse,
super.noteCommunication,
super.noteProfessionnalisme,
super.noteRespectEngagements,
required super.commentairePrincipal,
super.pointsPositifs,
super.pointsAmelioration,
super.recommandations,
super.recommande,
required super.dateCreation,
required super.dateModification,
super.dateValidation,
super.validateurId,
super.motifSignalement,
super.nombreSignalements,
super.estModeree,
super.estPublique,
super.donneesPersonnalisees,
});
/// Crée un modèle à partir d'un JSON (API Response)
factory EvaluationAideModel.fromJson(Map<String, dynamic> json) {
return EvaluationAideModel(
id: json['id'] as String,
demandeId: json['demandeId'] as String,
propositionId: json['propositionId'] as String?,
evaluateurId: json['evaluateurId'] as String,
nomEvaluateur: json['nomEvaluateur'] as String,
typeEvaluateur: _parseTypeEvaluateur(json['typeEvaluateur'] as String),
statut: _parseStatutEvaluation(json['statut'] as String),
noteGlobale: json['noteGlobale'].toDouble(),
noteDelaiReponse: json['noteDelaiReponse']?.toDouble(),
noteCommunication: json['noteCommunication']?.toDouble(),
noteProfessionnalisme: json['noteProfessionnalisme']?.toDouble(),
noteRespectEngagements: json['noteRespectEngagements']?.toDouble(),
commentairePrincipal: json['commentairePrincipal'] as String,
pointsPositifs: json['pointsPositifs'] as String?,
pointsAmelioration: json['pointsAmelioration'] as String?,
recommandations: json['recommandations'] as String?,
recommande: json['recommande'] as bool?,
dateCreation: DateTime.parse(json['dateCreation'] as String),
dateModification: DateTime.parse(json['dateModification'] as String),
dateValidation: json['dateValidation'] != null
? DateTime.parse(json['dateValidation'] as String)
: null,
validateurId: json['validateurId'] as String?,
motifSignalement: json['motifSignalement'] as String?,
nombreSignalements: json['nombreSignalements'] as int? ?? 0,
estModeree: json['estModeree'] as bool? ?? false,
estPublique: json['estPublique'] as bool? ?? true,
donneesPersonnalisees: Map<String, dynamic>.from(json['donneesPersonnalisees'] ?? {}),
);
}
/// Convertit le modèle en JSON (API Request)
Map<String, dynamic> toJson() {
return {
'id': id,
'demandeId': demandeId,
'propositionId': propositionId,
'evaluateurId': evaluateurId,
'nomEvaluateur': nomEvaluateur,
'typeEvaluateur': typeEvaluateur.name,
'statut': statut.name,
'noteGlobale': noteGlobale,
'noteDelaiReponse': noteDelaiReponse,
'noteCommunication': noteCommunication,
'noteProfessionnalisme': noteProfessionnalisme,
'noteRespectEngagements': noteRespectEngagements,
'commentairePrincipal': commentairePrincipal,
'pointsPositifs': pointsPositifs,
'pointsAmelioration': pointsAmelioration,
'recommandations': recommandations,
'recommande': recommande,
'dateCreation': dateCreation.toIso8601String(),
'dateModification': dateModification.toIso8601String(),
'dateValidation': dateValidation?.toIso8601String(),
'validateurId': validateurId,
'motifSignalement': motifSignalement,
'nombreSignalements': nombreSignalements,
'estModeree': estModeree,
'estPublique': estPublique,
'donneesPersonnalisees': donneesPersonnalisees,
};
}
/// Crée un modèle à partir d'une entité du domaine
factory EvaluationAideModel.fromEntity(EvaluationAide entity) {
return EvaluationAideModel(
id: entity.id,
demandeId: entity.demandeId,
propositionId: entity.propositionId,
evaluateurId: entity.evaluateurId,
nomEvaluateur: entity.nomEvaluateur,
typeEvaluateur: entity.typeEvaluateur,
statut: entity.statut,
noteGlobale: entity.noteGlobale,
noteDelaiReponse: entity.noteDelaiReponse,
noteCommunication: entity.noteCommunication,
noteProfessionnalisme: entity.noteProfessionnalisme,
noteRespectEngagements: entity.noteRespectEngagements,
commentairePrincipal: entity.commentairePrincipal,
pointsPositifs: entity.pointsPositifs,
pointsAmelioration: entity.pointsAmelioration,
recommandations: entity.recommandations,
recommande: entity.recommande,
dateCreation: entity.dateCreation,
dateModification: entity.dateModification,
dateValidation: entity.dateValidation,
validateurId: entity.validateurId,
motifSignalement: entity.motifSignalement,
nombreSignalements: entity.nombreSignalements,
estModeree: entity.estModeree,
estPublique: entity.estPublique,
donneesPersonnalisees: Map<String, dynamic>.from(entity.donneesPersonnalisees),
);
}
/// Convertit le modèle en entité du domaine
EvaluationAide toEntity() {
return EvaluationAide(
id: id,
demandeId: demandeId,
propositionId: propositionId,
evaluateurId: evaluateurId,
nomEvaluateur: nomEvaluateur,
typeEvaluateur: typeEvaluateur,
statut: statut,
noteGlobale: noteGlobale,
noteDelaiReponse: noteDelaiReponse,
noteCommunication: noteCommunication,
noteProfessionnalisme: noteProfessionnalisme,
noteRespectEngagements: noteRespectEngagements,
commentairePrincipal: commentairePrincipal,
pointsPositifs: pointsPositifs,
pointsAmelioration: pointsAmelioration,
recommandations: recommandations,
recommande: recommande,
dateCreation: dateCreation,
dateModification: dateModification,
dateValidation: dateValidation,
validateurId: validateurId,
motifSignalement: motifSignalement,
nombreSignalements: nombreSignalements,
estModeree: estModeree,
estPublique: estPublique,
donneesPersonnalisees: donneesPersonnalisees,
);
}
// Méthodes utilitaires de parsing
static TypeEvaluateur _parseTypeEvaluateur(String value) {
return TypeEvaluateur.values.firstWhere(
(e) => e.name == value,
orElse: () => TypeEvaluateur.beneficiaire,
);
}
static StatutEvaluation _parseStatutEvaluation(String value) {
return StatutEvaluation.values.firstWhere(
(e) => e.name == value,
orElse: () => StatutEvaluation.brouillon,
);
}
}
/// Modèle pour les statistiques d'évaluation
class StatistiquesEvaluationModel {
final double noteMoyenne;
final int nombreEvaluations;
final Map<int, int> repartitionNotes;
final double pourcentageRecommandations;
final List<EvaluationAideModel> evaluationsRecentes;
final DateTime dateCalcul;
const StatistiquesEvaluationModel({
required this.noteMoyenne,
required this.nombreEvaluations,
required this.repartitionNotes,
required this.pourcentageRecommandations,
required this.evaluationsRecentes,
required this.dateCalcul,
});
/// Crée un modèle à partir d'un JSON (API Response)
factory StatistiquesEvaluationModel.fromJson(Map<String, dynamic> json) {
return StatistiquesEvaluationModel(
noteMoyenne: json['noteMoyenne'].toDouble(),
nombreEvaluations: json['nombreEvaluations'] as int,
repartitionNotes: Map<int, int>.from(json['repartitionNotes']),
pourcentageRecommandations: json['pourcentageRecommandations'].toDouble(),
evaluationsRecentes: (json['evaluationsRecentes'] as List<dynamic>)
.map((e) => EvaluationAideModel.fromJson(e as Map<String, dynamic>))
.toList(),
dateCalcul: DateTime.parse(json['dateCalcul'] as String),
);
}
/// Convertit le modèle en JSON
Map<String, dynamic> toJson() {
return {
'noteMoyenne': noteMoyenne,
'nombreEvaluations': nombreEvaluations,
'repartitionNotes': repartitionNotes,
'pourcentageRecommandations': pourcentageRecommandations,
'evaluationsRecentes': evaluationsRecentes
.map((e) => e.toJson())
.toList(),
'dateCalcul': dateCalcul.toIso8601String(),
};
}
/// Convertit le modèle en entité du domaine
StatistiquesEvaluation toEntity() {
return StatistiquesEvaluation(
noteMoyenne: noteMoyenne,
nombreEvaluations: nombreEvaluations,
repartitionNotes: repartitionNotes,
pourcentageRecommandations: pourcentageRecommandations,
evaluationsRecentes: evaluationsRecentes
.map((e) => e.toEntity())
.toList(),
dateCalcul: dateCalcul,
);
}
/// Crée un modèle à partir d'une entité du domaine
factory StatistiquesEvaluationModel.fromEntity(StatistiquesEvaluation entity) {
return StatistiquesEvaluationModel(
noteMoyenne: entity.noteMoyenne,
nombreEvaluations: entity.nombreEvaluations,
repartitionNotes: Map<int, int>.from(entity.repartitionNotes),
pourcentageRecommandations: entity.pourcentageRecommandations,
evaluationsRecentes: entity.evaluationsRecentes
.map((e) => EvaluationAideModel.fromEntity(e))
.toList(),
dateCalcul: entity.dateCalcul,
);
}
}
/// Modèle pour les réponses de recherche d'évaluations
class RechercheEvaluationsResponse {
final List<EvaluationAideModel> evaluations;
final int totalElements;
final int totalPages;
final int currentPage;
final int pageSize;
final bool hasNext;
final bool hasPrevious;
const RechercheEvaluationsResponse({
required this.evaluations,
required this.totalElements,
required this.totalPages,
required this.currentPage,
required this.pageSize,
required this.hasNext,
required this.hasPrevious,
});
/// Crée un modèle à partir d'un JSON (API Response)
factory RechercheEvaluationsResponse.fromJson(Map<String, dynamic> json) {
return RechercheEvaluationsResponse(
evaluations: (json['content'] as List<dynamic>)
.map((e) => EvaluationAideModel.fromJson(e as Map<String, dynamic>))
.toList(),
totalElements: json['totalElements'] as int,
totalPages: json['totalPages'] as int,
currentPage: json['number'] as int,
pageSize: json['size'] as int,
hasNext: !(json['last'] as bool),
hasPrevious: !(json['first'] as bool),
);
}
/// Convertit le modèle en JSON
Map<String, dynamic> toJson() {
return {
'content': evaluations.map((e) => e.toJson()).toList(),
'totalElements': totalElements,
'totalPages': totalPages,
'number': currentPage,
'size': pageSize,
'last': !hasNext,
'first': !hasPrevious,
};
}
}
/// Modèle pour les requêtes de création d'évaluation
class CreerEvaluationRequest {
final String demandeId;
final String? propositionId;
final String evaluateurId;
final TypeEvaluateur typeEvaluateur;
final double noteGlobale;
final double? noteDelaiReponse;
final double? noteCommunication;
final double? noteProfessionnalisme;
final double? noteRespectEngagements;
final String commentairePrincipal;
final String? pointsPositifs;
final String? pointsAmelioration;
final String? recommandations;
final bool? recommande;
final bool estPublique;
final Map<String, dynamic> donneesPersonnalisees;
const CreerEvaluationRequest({
required this.demandeId,
this.propositionId,
required this.evaluateurId,
required this.typeEvaluateur,
required this.noteGlobale,
this.noteDelaiReponse,
this.noteCommunication,
this.noteProfessionnalisme,
this.noteRespectEngagements,
required this.commentairePrincipal,
this.pointsPositifs,
this.pointsAmelioration,
this.recommandations,
this.recommande,
this.estPublique = true,
this.donneesPersonnalisees = const {},
});
/// Convertit la requête en JSON
Map<String, dynamic> toJson() {
return {
'demandeId': demandeId,
'propositionId': propositionId,
'evaluateurId': evaluateurId,
'typeEvaluateur': typeEvaluateur.name,
'noteGlobale': noteGlobale,
'noteDelaiReponse': noteDelaiReponse,
'noteCommunication': noteCommunication,
'noteProfessionnalisme': noteProfessionnalisme,
'noteRespectEngagements': noteRespectEngagements,
'commentairePrincipal': commentairePrincipal,
'pointsPositifs': pointsPositifs,
'pointsAmelioration': pointsAmelioration,
'recommandations': recommandations,
'recommande': recommande,
'estPublique': estPublique,
'donneesPersonnalisees': donneesPersonnalisees,
};
}
/// Crée une requête à partir d'une entité d'évaluation
factory CreerEvaluationRequest.fromEntity(EvaluationAide entity) {
return CreerEvaluationRequest(
demandeId: entity.demandeId,
propositionId: entity.propositionId,
evaluateurId: entity.evaluateurId,
typeEvaluateur: entity.typeEvaluateur,
noteGlobale: entity.noteGlobale,
noteDelaiReponse: entity.noteDelaiReponse,
noteCommunication: entity.noteCommunication,
noteProfessionnalisme: entity.noteProfessionnalisme,
noteRespectEngagements: entity.noteRespectEngagements,
commentairePrincipal: entity.commentairePrincipal,
pointsPositifs: entity.pointsPositifs,
pointsAmelioration: entity.pointsAmelioration,
recommandations: entity.recommandations,
recommande: entity.recommande,
estPublique: entity.estPublique,
donneesPersonnalisees: entity.donneesPersonnalisees,
);
}
}

View File

@@ -0,0 +1,335 @@
import '../../domain/entities/proposition_aide.dart';
import '../../domain/entities/demande_aide.dart';
/// Modèle de données pour les propositions d'aide
///
/// Ce modèle fait la conversion entre les DTOs de l'API REST
/// et les entités du domaine pour les propositions d'aide.
class PropositionAideModel extends PropositionAide {
const PropositionAideModel({
required super.id,
required super.titre,
required super.description,
required super.typeAide,
required super.statut,
required super.proposantId,
required super.nomProposant,
required super.organisationId,
required super.nombreMaxBeneficiaires,
super.montantMaximum,
super.montantMinimum,
required super.delaiReponseHeures,
required super.dateCreation,
required super.dateModification,
super.dateExpiration,
super.dateActivation,
super.dateDesactivation,
required super.contactProposant,
super.zonesGeographiques,
super.creneauxDisponibilite,
super.criteresSelection,
super.conditionsSpeciales,
super.nombreBeneficiairesAides,
super.nombreVues,
super.nombreCandidatures,
super.noteMoyenne,
super.nombreEvaluations,
super.donneesPersonnalisees,
super.estVerifiee,
super.estPromue,
});
/// Crée un modèle à partir d'un JSON (API Response)
factory PropositionAideModel.fromJson(Map<String, dynamic> json) {
return PropositionAideModel(
id: json['id'] as String,
titre: json['titre'] as String,
description: json['description'] as String,
typeAide: _parseTypeAide(json['typeAide'] as String),
statut: _parseStatutProposition(json['statut'] as String),
proposantId: json['proposantId'] as String,
nomProposant: json['nomProposant'] as String,
organisationId: json['organisationId'] as String,
nombreMaxBeneficiaires: json['nombreMaxBeneficiaires'] as int,
montantMaximum: json['montantMaximum']?.toDouble(),
montantMinimum: json['montantMinimum']?.toDouble(),
delaiReponseHeures: json['delaiReponseHeures'] as int,
dateCreation: DateTime.parse(json['dateCreation'] as String),
dateModification: DateTime.parse(json['dateModification'] as String),
dateExpiration: json['dateExpiration'] != null
? DateTime.parse(json['dateExpiration'] as String)
: null,
dateActivation: json['dateActivation'] != null
? DateTime.parse(json['dateActivation'] as String)
: null,
dateDesactivation: json['dateDesactivation'] != null
? DateTime.parse(json['dateDesactivation'] as String)
: null,
contactProposant: ContactProposantModel.fromJson(
json['contactProposant'] as Map<String, dynamic>
),
zonesGeographiques: (json['zonesGeographiques'] as List<dynamic>?)
?.cast<String>() ?? [],
creneauxDisponibilite: (json['creneauxDisponibilite'] as List<dynamic>?)
?.map((e) => CreneauDisponibiliteModel.fromJson(e as Map<String, dynamic>))
.toList() ?? [],
criteresSelection: (json['criteresSelection'] as List<dynamic>?)
?.map((e) => CritereSelectionModel.fromJson(e as Map<String, dynamic>))
.toList() ?? [],
conditionsSpeciales: (json['conditionsSpeciales'] as List<dynamic>?)
?.cast<String>() ?? [],
nombreBeneficiairesAides: json['nombreBeneficiairesAides'] as int? ?? 0,
nombreVues: json['nombreVues'] as int? ?? 0,
nombreCandidatures: json['nombreCandidatures'] as int? ?? 0,
noteMoyenne: json['noteMoyenne']?.toDouble(),
nombreEvaluations: json['nombreEvaluations'] as int? ?? 0,
donneesPersonnalisees: Map<String, dynamic>.from(json['donneesPersonnalisees'] ?? {}),
estVerifiee: json['estVerifiee'] as bool? ?? false,
estPromue: json['estPromue'] as bool? ?? false,
);
}
/// Convertit le modèle en JSON (API Request)
Map<String, dynamic> toJson() {
return {
'id': id,
'titre': titre,
'description': description,
'typeAide': typeAide.name,
'statut': statut.name,
'proposantId': proposantId,
'nomProposant': nomProposant,
'organisationId': organisationId,
'nombreMaxBeneficiaires': nombreMaxBeneficiaires,
'montantMaximum': montantMaximum,
'montantMinimum': montantMinimum,
'delaiReponseHeures': delaiReponseHeures,
'dateCreation': dateCreation.toIso8601String(),
'dateModification': dateModification.toIso8601String(),
'dateExpiration': dateExpiration?.toIso8601String(),
'dateActivation': dateActivation?.toIso8601String(),
'dateDesactivation': dateDesactivation?.toIso8601String(),
'contactProposant': (contactProposant as ContactProposantModel).toJson(),
'zonesGeographiques': zonesGeographiques,
'creneauxDisponibilite': creneauxDisponibilite
.map((e) => (e as CreneauDisponibiliteModel).toJson())
.toList(),
'criteresSelection': criteresSelection
.map((e) => (e as CritereSelectionModel).toJson())
.toList(),
'conditionsSpeciales': conditionsSpeciales,
'nombreBeneficiairesAides': nombreBeneficiairesAides,
'nombreVues': nombreVues,
'nombreCandidatures': nombreCandidatures,
'noteMoyenne': noteMoyenne,
'nombreEvaluations': nombreEvaluations,
'donneesPersonnalisees': donneesPersonnalisees,
'estVerifiee': estVerifiee,
'estPromue': estPromue,
};
}
/// Crée un modèle à partir d'une entité du domaine
factory PropositionAideModel.fromEntity(PropositionAide entity) {
return PropositionAideModel(
id: entity.id,
titre: entity.titre,
description: entity.description,
typeAide: entity.typeAide,
statut: entity.statut,
proposantId: entity.proposantId,
nomProposant: entity.nomProposant,
organisationId: entity.organisationId,
nombreMaxBeneficiaires: entity.nombreMaxBeneficiaires,
montantMaximum: entity.montantMaximum,
montantMinimum: entity.montantMinimum,
delaiReponseHeures: entity.delaiReponseHeures,
dateCreation: entity.dateCreation,
dateModification: entity.dateModification,
dateExpiration: entity.dateExpiration,
dateActivation: entity.dateActivation,
dateDesactivation: entity.dateDesactivation,
contactProposant: ContactProposantModel.fromEntity(entity.contactProposant),
zonesGeographiques: List<String>.from(entity.zonesGeographiques),
creneauxDisponibilite: entity.creneauxDisponibilite
.map((e) => CreneauDisponibiliteModel.fromEntity(e))
.toList(),
criteresSelection: entity.criteresSelection
.map((e) => CritereSelectionModel.fromEntity(e))
.toList(),
conditionsSpeciales: List<String>.from(entity.conditionsSpeciales),
nombreBeneficiairesAides: entity.nombreBeneficiairesAides,
nombreVues: entity.nombreVues,
nombreCandidatures: entity.nombreCandidatures,
noteMoyenne: entity.noteMoyenne,
nombreEvaluations: entity.nombreEvaluations,
donneesPersonnalisees: Map<String, dynamic>.from(entity.donneesPersonnalisees),
estVerifiee: entity.estVerifiee,
estPromue: entity.estPromue,
);
}
/// Convertit le modèle en entité du domaine
PropositionAide toEntity() {
return PropositionAide(
id: id,
titre: titre,
description: description,
typeAide: typeAide,
statut: statut,
proposantId: proposantId,
nomProposant: nomProposant,
organisationId: organisationId,
nombreMaxBeneficiaires: nombreMaxBeneficiaires,
montantMaximum: montantMaximum,
montantMinimum: montantMinimum,
delaiReponseHeures: delaiReponseHeures,
dateCreation: dateCreation,
dateModification: dateModification,
dateExpiration: dateExpiration,
dateActivation: dateActivation,
dateDesactivation: dateDesactivation,
contactProposant: contactProposant,
zonesGeographiques: zonesGeographiques,
creneauxDisponibilite: creneauxDisponibilite,
criteresSelection: criteresSelection,
conditionsSpeciales: conditionsSpeciales,
nombreBeneficiairesAides: nombreBeneficiairesAides,
nombreVues: nombreVues,
nombreCandidatures: nombreCandidatures,
noteMoyenne: noteMoyenne,
nombreEvaluations: nombreEvaluations,
donneesPersonnalisees: donneesPersonnalisees,
estVerifiee: estVerifiee,
estPromue: estPromue,
);
}
// Méthodes utilitaires de parsing
static TypeAide _parseTypeAide(String value) {
return TypeAide.values.firstWhere(
(e) => e.name == value,
orElse: () => TypeAide.autre,
);
}
static StatutProposition _parseStatutProposition(String value) {
return StatutProposition.values.firstWhere(
(e) => e.name == value,
orElse: () => StatutProposition.brouillon,
);
}
}
/// Modèles pour les classes auxiliaires
class ContactProposantModel extends ContactProposant {
const ContactProposantModel({
required super.nom,
required super.telephone,
super.email,
super.adresse,
super.heuresDisponibilite,
});
factory ContactProposantModel.fromJson(Map<String, dynamic> json) {
return ContactProposantModel(
nom: json['nom'] as String,
telephone: json['telephone'] as String,
email: json['email'] as String?,
adresse: json['adresse'] as String?,
heuresDisponibilite: json['heuresDisponibilite'] as String?,
);
}
Map<String, dynamic> toJson() {
return {
'nom': nom,
'telephone': telephone,
'email': email,
'adresse': adresse,
'heuresDisponibilite': heuresDisponibilite,
};
}
factory ContactProposantModel.fromEntity(ContactProposant entity) {
return ContactProposantModel(
nom: entity.nom,
telephone: entity.telephone,
email: entity.email,
adresse: entity.adresse,
heuresDisponibilite: entity.heuresDisponibilite,
);
}
}
class CreneauDisponibiliteModel extends CreneauDisponibilite {
const CreneauDisponibiliteModel({
required super.jourSemaine,
required super.heureDebut,
required super.heureFin,
super.commentaire,
});
factory CreneauDisponibiliteModel.fromJson(Map<String, dynamic> json) {
return CreneauDisponibiliteModel(
jourSemaine: json['jourSemaine'] as String,
heureDebut: json['heureDebut'] as String,
heureFin: json['heureFin'] as String,
commentaire: json['commentaire'] as String?,
);
}
Map<String, dynamic> toJson() {
return {
'jourSemaine': jourSemaine,
'heureDebut': heureDebut,
'heureFin': heureFin,
'commentaire': commentaire,
};
}
factory CreneauDisponibiliteModel.fromEntity(CreneauDisponibilite entity) {
return CreneauDisponibiliteModel(
jourSemaine: entity.jourSemaine,
heureDebut: entity.heureDebut,
heureFin: entity.heureFin,
commentaire: entity.commentaire,
);
}
}
class CritereSelectionModel extends CritereSelection {
const CritereSelectionModel({
required super.nom,
required super.description,
required super.obligatoire,
super.valeurAttendue,
});
factory CritereSelectionModel.fromJson(Map<String, dynamic> json) {
return CritereSelectionModel(
nom: json['nom'] as String,
description: json['description'] as String,
obligatoire: json['obligatoire'] as bool,
valeurAttendue: json['valeurAttendue'] as String?,
);
}
Map<String, dynamic> toJson() {
return {
'nom': nom,
'description': description,
'obligatoire': obligatoire,
'valeurAttendue': valeurAttendue,
};
}
factory CritereSelectionModel.fromEntity(CritereSelection entity) {
return CritereSelectionModel(
nom: entity.nom,
description: entity.description,
obligatoire: entity.obligatoire,
valeurAttendue: entity.valeurAttendue,
);
}
}

View File

@@ -0,0 +1,561 @@
import 'package:dartz/dartz.dart';
import '../../../../core/error/failures.dart';
import '../../../../core/error/exceptions.dart';
import '../../../../core/network/network_info.dart';
import '../../domain/entities/demande_aide.dart';
import '../../domain/entities/proposition_aide.dart';
import '../../domain/entities/evaluation_aide.dart';
import '../../domain/repositories/solidarite_repository.dart';
import '../datasources/solidarite_remote_data_source.dart';
import '../datasources/solidarite_local_data_source.dart';
import '../models/demande_aide_model.dart';
import '../models/proposition_aide_model.dart';
import '../models/evaluation_aide_model.dart';
/// Implémentation du repository de solidarité
///
/// Cette classe implémente le contrat défini dans le domaine
/// en combinant les sources de données locale et distante.
class SolidariteRepositoryImpl implements SolidariteRepository {
final SolidariteRemoteDataSource remoteDataSource;
final SolidariteLocalDataSource localDataSource;
final NetworkInfo networkInfo;
SolidariteRepositoryImpl({
required this.remoteDataSource,
required this.localDataSource,
required this.networkInfo,
});
// Demandes d'aide
@override
Future<Either<Failure, DemandeAide>> creerDemandeAide(DemandeAide demande) async {
try {
if (await networkInfo.isConnected) {
final demandeModel = DemandeAideModel.fromEntity(demande);
final result = await remoteDataSource.creerDemandeAide(demandeModel);
// Mettre en cache le résultat
await localDataSource.cacherDemandeAide(result);
return Right(result.toEntity());
} else {
return Left(NetworkFailure('Aucune connexion internet disponible'));
}
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} on CacheException catch (e) {
// Continuer même si la mise en cache échoue
return Left(CacheFailure(e.message));
} catch (e) {
return Left(UnexpectedFailure(e.toString()));
}
}
@override
Future<Either<Failure, DemandeAide>> mettreAJourDemandeAide(DemandeAide demande) async {
try {
if (await networkInfo.isConnected) {
final demandeModel = DemandeAideModel.fromEntity(demande);
final result = await remoteDataSource.mettreAJourDemandeAide(demandeModel);
// Mettre à jour le cache
await localDataSource.cacherDemandeAide(result);
return Right(result.toEntity());
} else {
return Left(NetworkFailure('Aucune connexion internet disponible'));
}
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} on CacheException catch (e) {
return Left(CacheFailure(e.message));
} catch (e) {
return Left(UnexpectedFailure(e.toString()));
}
}
@override
Future<Either<Failure, DemandeAide>> obtenirDemandeAide(String id) async {
try {
// Essayer d'abord le cache local
final cachedDemande = await localDataSource.obtenirDemandeAideCachee(id);
if (cachedDemande != null && await _estCacheValide()) {
return Right(cachedDemande.toEntity());
}
// Si pas en cache ou cache expiré, aller chercher sur le serveur
if (await networkInfo.isConnected) {
final result = await remoteDataSource.obtenirDemandeAide(id);
// Mettre en cache le résultat
await localDataSource.cacherDemandeAide(result);
return Right(result.toEntity());
} else {
// Si pas de connexion, utiliser le cache même s'il est expiré
if (cachedDemande != null) {
return Right(cachedDemande.toEntity());
}
return Left(NetworkFailure('Aucune connexion internet et aucune donnée en cache'));
}
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} on NotFoundException catch (e) {
return Left(NotFoundFailure(e.message));
} on CacheException catch (e) {
return Left(CacheFailure(e.message));
} catch (e) {
return Left(UnexpectedFailure(e.toString()));
}
}
@override
Future<Either<Failure, DemandeAide>> soumettreDemande(String demandeId) async {
try {
if (await networkInfo.isConnected) {
final result = await remoteDataSource.soumettreDemande(demandeId);
// Mettre à jour le cache
await localDataSource.cacherDemandeAide(result);
return Right(result.toEntity());
} else {
return Left(NetworkFailure('Aucune connexion internet disponible'));
}
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} on CacheException catch (e) {
return Left(CacheFailure(e.message));
} catch (e) {
return Left(UnexpectedFailure(e.toString()));
}
}
@override
Future<Either<Failure, DemandeAide>> evaluerDemande({
required String demandeId,
required String evaluateurId,
required StatutAide decision,
String? commentaire,
double? montantApprouve,
}) async {
try {
if (await networkInfo.isConnected) {
final result = await remoteDataSource.evaluerDemande(
demandeId: demandeId,
evaluateurId: evaluateurId,
decision: decision.name,
commentaire: commentaire,
montantApprouve: montantApprouve,
);
// Mettre à jour le cache
await localDataSource.cacherDemandeAide(result);
return Right(result.toEntity());
} else {
return Left(NetworkFailure('Aucune connexion internet disponible'));
}
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} on CacheException catch (e) {
return Left(CacheFailure(e.message));
} catch (e) {
return Left(UnexpectedFailure(e.toString()));
}
}
@override
Future<Either<Failure, List<DemandeAide>>> rechercherDemandes({
String? organisationId,
TypeAide? typeAide,
StatutAide? statut,
String? demandeurId,
bool? urgente,
int page = 0,
int taille = 20,
}) async {
try {
if (await networkInfo.isConnected) {
final result = await remoteDataSource.rechercherDemandes(
organisationId: organisationId,
typeAide: typeAide?.name,
statut: statut?.name,
demandeurId: demandeurId,
urgente: urgente,
page: page,
taille: taille,
);
// Mettre en cache les résultats
for (final demande in result) {
await localDataSource.cacherDemandeAide(demande);
}
return Right(result.map((model) => model.toEntity()).toList());
} else {
// Mode hors ligne : rechercher dans le cache local
final cachedDemandes = await localDataSource.obtenirDemandesAideCachees();
var filteredDemandes = cachedDemandes.where((demande) {
if (organisationId != null && demande.organisationId != organisationId) return false;
if (typeAide != null && demande.typeAide != typeAide) return false;
if (statut != null && demande.statut != statut) return false;
if (demandeurId != null && demande.demandeurId != demandeurId) return false;
if (urgente != null && demande.estUrgente != urgente) return false;
return true;
}).toList();
// Pagination locale
final startIndex = page * taille;
final endIndex = (startIndex + taille).clamp(0, filteredDemandes.length);
if (startIndex < filteredDemandes.length) {
filteredDemandes = filteredDemandes.sublist(startIndex, endIndex);
} else {
filteredDemandes = [];
}
return Right(filteredDemandes.map((model) => model.toEntity()).toList());
}
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} on CacheException catch (e) {
return Left(CacheFailure(e.message));
} catch (e) {
return Left(UnexpectedFailure(e.toString()));
}
}
@override
Future<Either<Failure, List<DemandeAide>>> obtenirDemandesUrgentes(String organisationId) async {
try {
if (await networkInfo.isConnected) {
final result = await remoteDataSource.obtenirDemandesUrgentes(organisationId);
// Mettre en cache les résultats
for (final demande in result) {
await localDataSource.cacherDemandeAide(demande);
}
return Right(result.map((model) => model.toEntity()).toList());
} else {
// Mode hors ligne : filtrer le cache local
final cachedDemandes = await localDataSource.obtenirDemandesAideCachees();
final demandesUrgentes = cachedDemandes
.where((demande) => demande.organisationId == organisationId && demande.estUrgente)
.toList();
return Right(demandesUrgentes.map((model) => model.toEntity()).toList());
}
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} on CacheException catch (e) {
return Left(CacheFailure(e.message));
} catch (e) {
return Left(UnexpectedFailure(e.toString()));
}
}
@override
Future<Either<Failure, List<DemandeAide>>> obtenirMesdemandes(String utilisateurId) async {
try {
if (await networkInfo.isConnected) {
final result = await remoteDataSource.obtenirMesdemandes(utilisateurId);
// Mettre en cache les résultats
for (final demande in result) {
await localDataSource.cacherDemandeAide(demande);
}
return Right(result.map((model) => model.toEntity()).toList());
} else {
// Mode hors ligne : filtrer le cache local
final cachedDemandes = await localDataSource.obtenirDemandesAideCachees();
final mesdemandes = cachedDemandes
.where((demande) => demande.demandeurId == utilisateurId)
.toList();
return Right(mesdemandes.map((model) => model.toEntity()).toList());
}
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} on CacheException catch (e) {
return Left(CacheFailure(e.message));
} catch (e) {
return Left(UnexpectedFailure(e.toString()));
}
}
// Propositions d'aide
@override
Future<Either<Failure, PropositionAide>> creerPropositionAide(PropositionAide proposition) async {
try {
if (await networkInfo.isConnected) {
final propositionModel = PropositionAideModel.fromEntity(proposition);
final result = await remoteDataSource.creerPropositionAide(propositionModel);
// Mettre en cache le résultat
await localDataSource.cacherPropositionAide(result);
return Right(result.toEntity());
} else {
return Left(NetworkFailure('Aucune connexion internet disponible'));
}
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} on CacheException catch (e) {
return Left(CacheFailure(e.message));
} catch (e) {
return Left(UnexpectedFailure(e.toString()));
}
}
@override
Future<Either<Failure, PropositionAide>> mettreAJourPropositionAide(PropositionAide proposition) async {
try {
if (await networkInfo.isConnected) {
final propositionModel = PropositionAideModel.fromEntity(proposition);
final result = await remoteDataSource.mettreAJourPropositionAide(propositionModel);
// Mettre à jour le cache
await localDataSource.cacherPropositionAide(result);
return Right(result.toEntity());
} else {
return Left(NetworkFailure('Aucune connexion internet disponible'));
}
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} on CacheException catch (e) {
return Left(CacheFailure(e.message));
} catch (e) {
return Left(UnexpectedFailure(e.toString()));
}
}
@override
Future<Either<Failure, PropositionAide>> obtenirPropositionAide(String id) async {
try {
// Essayer d'abord le cache local
final cachedProposition = await localDataSource.obtenirPropositionAideCachee(id);
if (cachedProposition != null && await _estCacheValide()) {
return Right(cachedProposition.toEntity());
}
// Si pas en cache ou cache expiré, aller chercher sur le serveur
if (await networkInfo.isConnected) {
final result = await remoteDataSource.obtenirPropositionAide(id);
// Mettre en cache le résultat
await localDataSource.cacherPropositionAide(result);
return Right(result.toEntity());
} else {
// Si pas de connexion, utiliser le cache même s'il est expiré
if (cachedProposition != null) {
return Right(cachedProposition.toEntity());
}
return Left(NetworkFailure('Aucune connexion internet et aucune donnée en cache'));
}
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} on NotFoundException catch (e) {
return Left(NotFoundFailure(e.message));
} on CacheException catch (e) {
return Left(CacheFailure(e.message));
} catch (e) {
return Left(UnexpectedFailure(e.toString()));
}
}
@override
Future<Either<Failure, PropositionAide>> changerStatutProposition({
required String propositionId,
required bool activer,
}) async {
try {
if (await networkInfo.isConnected) {
final result = await remoteDataSource.changerStatutProposition(
propositionId: propositionId,
activer: activer,
);
// Mettre à jour le cache
await localDataSource.cacherPropositionAide(result);
return Right(result.toEntity());
} else {
return Left(NetworkFailure('Aucune connexion internet disponible'));
}
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} on CacheException catch (e) {
return Left(CacheFailure(e.message));
} catch (e) {
return Left(UnexpectedFailure(e.toString()));
}
}
@override
Future<Either<Failure, List<PropositionAide>>> rechercherPropositions({
String? organisationId,
TypeAide? typeAide,
String? proposantId,
bool? actives,
int page = 0,
int taille = 20,
}) async {
try {
if (await networkInfo.isConnected) {
final result = await remoteDataSource.rechercherPropositions(
organisationId: organisationId,
typeAide: typeAide?.name,
proposantId: proposantId,
actives: actives,
page: page,
taille: taille,
);
// Mettre en cache les résultats
for (final proposition in result) {
await localDataSource.cacherPropositionAide(proposition);
}
return Right(result.map((model) => model.toEntity()).toList());
} else {
// Mode hors ligne : rechercher dans le cache local
final cachedPropositions = await localDataSource.obtenirPropositionsAideCachees();
var filteredPropositions = cachedPropositions.where((proposition) {
if (organisationId != null && proposition.organisationId != organisationId) return false;
if (typeAide != null && proposition.typeAide != typeAide) return false;
if (proposantId != null && proposition.proposantId != proposantId) return false;
if (actives != null && proposition.isActiveEtDisponible != actives) return false;
return true;
}).toList();
// Pagination locale
final startIndex = page * taille;
final endIndex = (startIndex + taille).clamp(0, filteredPropositions.length);
if (startIndex < filteredPropositions.length) {
filteredPropositions = filteredPropositions.sublist(startIndex, endIndex);
} else {
filteredPropositions = [];
}
return Right(filteredPropositions.map((model) => model.toEntity()).toList());
}
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} on CacheException catch (e) {
return Left(CacheFailure(e.message));
} catch (e) {
return Left(UnexpectedFailure(e.toString()));
}
}
@override
Future<Either<Failure, List<PropositionAide>>> obtenirPropositionsActives(TypeAide typeAide) async {
try {
if (await networkInfo.isConnected) {
final result = await remoteDataSource.obtenirPropositionsActives(typeAide.name);
// Mettre en cache les résultats
for (final proposition in result) {
await localDataSource.cacherPropositionAide(proposition);
}
return Right(result.map((model) => model.toEntity()).toList());
} else {
// Mode hors ligne : filtrer le cache local
final cachedPropositions = await localDataSource.obtenirPropositionsAideCachees();
final propositionsActives = cachedPropositions
.where((proposition) => proposition.typeAide == typeAide && proposition.isActiveEtDisponible)
.toList();
return Right(propositionsActives.map((model) => model.toEntity()).toList());
}
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} on CacheException catch (e) {
return Left(CacheFailure(e.message));
} catch (e) {
return Left(UnexpectedFailure(e.toString()));
}
}
@override
Future<Either<Failure, List<PropositionAide>>> obtenirMeilleuresPropositions(int limite) async {
try {
if (await networkInfo.isConnected) {
final result = await remoteDataSource.obtenirMeilleuresPropositions(limite);
// Mettre en cache les résultats
for (final proposition in result) {
await localDataSource.cacherPropositionAide(proposition);
}
return Right(result.map((model) => model.toEntity()).toList());
} else {
// Mode hors ligne : trier le cache local par note moyenne
final cachedPropositions = await localDataSource.obtenirPropositionsAideCachees();
cachedPropositions.sort((a, b) {
final noteA = a.noteMoyenne ?? 0.0;
final noteB = b.noteMoyenne ?? 0.0;
return noteB.compareTo(noteA);
});
final meilleuresPropositions = cachedPropositions.take(limite).toList();
return Right(meilleuresPropositions.map((model) => model.toEntity()).toList());
}
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} on CacheException catch (e) {
return Left(CacheFailure(e.message));
} catch (e) {
return Left(UnexpectedFailure(e.toString()));
}
}
@override
Future<Either<Failure, List<PropositionAide>>> obtenirMesPropositions(String utilisateurId) async {
try {
if (await networkInfo.isConnected) {
final result = await remoteDataSource.obtenirMesPropositions(utilisateurId);
// Mettre en cache les résultats
for (final proposition in result) {
await localDataSource.cacherPropositionAide(proposition);
}
return Right(result.map((model) => model.toEntity()).toList());
} else {
// Mode hors ligne : filtrer le cache local
final cachedPropositions = await localDataSource.obtenirPropositionsAideCachees();
final mesPropositions = cachedPropositions
.where((proposition) => proposition.proposantId == utilisateurId)
.toList();
return Right(mesPropositions.map((model) => model.toEntity()).toList());
}
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} on CacheException catch (e) {
return Left(CacheFailure(e.message));
} catch (e) {
return Left(UnexpectedFailure(e.toString()));
}
}
// Méthodes utilitaires privées
Future<bool> _estCacheValide() async {
try {
final localDataSourceImpl = localDataSource as SolidariteLocalDataSourceImpl;
return await localDataSourceImpl.estCacheDemandesValide() &&
await localDataSourceImpl.estCachePropositionsValide();
} catch (e) {
return false;
}
}
}

View File

@@ -0,0 +1,338 @@
// Partie 2 de l'implémentation du repository de solidarité
// Cette partie contient les méthodes pour le matching, les évaluations et les statistiques
import 'package:dartz/dartz.dart';
import '../../../../core/error/failures.dart';
import '../../../../core/error/exceptions.dart';
import '../../domain/entities/demande_aide.dart';
import '../../domain/entities/proposition_aide.dart';
import '../../domain/entities/evaluation_aide.dart';
import '../datasources/solidarite_remote_data_source.dart';
import '../datasources/solidarite_local_data_source.dart';
import '../models/demande_aide_model.dart';
import '../models/proposition_aide_model.dart';
import '../models/evaluation_aide_model.dart';
/// Extension de l'implémentation du repository de solidarité
/// Cette partie sera intégrée dans la classe principale
mixin SolidariteRepositoryImplPart2 {
SolidariteRemoteDataSource get remoteDataSource;
SolidariteLocalDataSource get localDataSource;
bool Function() get isConnected;
// Matching
Future<Either<Failure, List<PropositionAide>>> trouverPropositionsCompatibles(String demandeId) async {
try {
if (await isConnected()) {
final result = await remoteDataSource.trouverPropositionsCompatibles(demandeId);
// Mettre en cache les résultats
for (final proposition in result) {
await localDataSource.cacherPropositionAide(proposition);
}
return Right(result.map((model) => model.toEntity()).toList());
} else {
return Left(NetworkFailure('Aucune connexion internet disponible'));
}
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} on CacheException catch (e) {
return Left(CacheFailure(e.message));
} catch (e) {
return Left(UnexpectedFailure(e.toString()));
}
}
Future<Either<Failure, List<DemandeAide>>> trouverDemandesCompatibles(String propositionId) async {
try {
if (await isConnected()) {
final result = await remoteDataSource.trouverDemandesCompatibles(propositionId);
// Mettre en cache les résultats
for (final demande in result) {
await localDataSource.cacherDemandeAide(demande);
}
return Right(result.map((model) => model.toEntity()).toList());
} else {
return Left(NetworkFailure('Aucune connexion internet disponible'));
}
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} on CacheException catch (e) {
return Left(CacheFailure(e.message));
} catch (e) {
return Left(UnexpectedFailure(e.toString()));
}
}
Future<Either<Failure, List<PropositionAide>>> rechercherProposantsFinanciers(String demandeId) async {
try {
if (await isConnected()) {
final result = await remoteDataSource.rechercherProposantsFinanciers(demandeId);
// Mettre en cache les résultats
for (final proposition in result) {
await localDataSource.cacherPropositionAide(proposition);
}
return Right(result.map((model) => model.toEntity()).toList());
} else {
return Left(NetworkFailure('Aucune connexion internet disponible'));
}
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} on CacheException catch (e) {
return Left(CacheFailure(e.message));
} catch (e) {
return Left(UnexpectedFailure(e.toString()));
}
}
// Évaluations
Future<Either<Failure, EvaluationAide>> creerEvaluation(EvaluationAide evaluation) async {
try {
if (await isConnected()) {
final evaluationModel = EvaluationAideModel.fromEntity(evaluation);
final result = await remoteDataSource.creerEvaluation(evaluationModel);
// Mettre en cache le résultat
await localDataSource.cacherEvaluation(result);
return Right(result.toEntity());
} else {
return Left(NetworkFailure('Aucune connexion internet disponible'));
}
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} on CacheException catch (e) {
return Left(CacheFailure(e.message));
} catch (e) {
return Left(UnexpectedFailure(e.toString()));
}
}
Future<Either<Failure, EvaluationAide>> mettreAJourEvaluation(EvaluationAide evaluation) async {
try {
if (await isConnected()) {
final evaluationModel = EvaluationAideModel.fromEntity(evaluation);
final result = await remoteDataSource.mettreAJourEvaluation(evaluationModel);
// Mettre à jour le cache
await localDataSource.cacherEvaluation(result);
return Right(result.toEntity());
} else {
return Left(NetworkFailure('Aucune connexion internet disponible'));
}
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} on CacheException catch (e) {
return Left(CacheFailure(e.message));
} catch (e) {
return Left(UnexpectedFailure(e.toString()));
}
}
Future<Either<Failure, EvaluationAide>> obtenirEvaluation(String id) async {
try {
// Essayer d'abord le cache local
final cachedEvaluation = await localDataSource.obtenirEvaluationCachee(id);
if (cachedEvaluation != null && await _estCacheEvaluationsValide()) {
return Right(cachedEvaluation.toEntity());
}
// Si pas en cache ou cache expiré, aller chercher sur le serveur
if (await isConnected()) {
final result = await remoteDataSource.obtenirEvaluation(id);
// Mettre en cache le résultat
await localDataSource.cacherEvaluation(result);
return Right(result.toEntity());
} else {
// Si pas de connexion, utiliser le cache même s'il est expiré
if (cachedEvaluation != null) {
return Right(cachedEvaluation.toEntity());
}
return Left(NetworkFailure('Aucune connexion internet et aucune donnée en cache'));
}
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} on NotFoundException catch (e) {
return Left(NotFoundFailure(e.message));
} on CacheException catch (e) {
return Left(CacheFailure(e.message));
} catch (e) {
return Left(UnexpectedFailure(e.toString()));
}
}
Future<Either<Failure, List<EvaluationAide>>> obtenirEvaluationsDemande(String demandeId) async {
try {
if (await isConnected()) {
final result = await remoteDataSource.obtenirEvaluationsDemande(demandeId);
// Mettre en cache les résultats
for (final evaluation in result) {
await localDataSource.cacherEvaluation(evaluation);
}
return Right(result.map((model) => model.toEntity()).toList());
} else {
// Mode hors ligne : filtrer le cache local
final cachedEvaluations = await localDataSource.obtenirEvaluationsCachees();
final evaluationsDemande = cachedEvaluations
.where((evaluation) => evaluation.demandeId == demandeId)
.toList();
return Right(evaluationsDemande.map((model) => model.toEntity()).toList());
}
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} on CacheException catch (e) {
return Left(CacheFailure(e.message));
} catch (e) {
return Left(UnexpectedFailure(e.toString()));
}
}
Future<Either<Failure, List<EvaluationAide>>> obtenirEvaluationsProposition(String propositionId) async {
try {
if (await isConnected()) {
final result = await remoteDataSource.obtenirEvaluationsProposition(propositionId);
// Mettre en cache les résultats
for (final evaluation in result) {
await localDataSource.cacherEvaluation(evaluation);
}
return Right(result.map((model) => model.toEntity()).toList());
} else {
// Mode hors ligne : filtrer le cache local
final cachedEvaluations = await localDataSource.obtenirEvaluationsCachees();
final evaluationsProposition = cachedEvaluations
.where((evaluation) => evaluation.propositionId == propositionId)
.toList();
return Right(evaluationsProposition.map((model) => model.toEntity()).toList());
}
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} on CacheException catch (e) {
return Left(CacheFailure(e.message));
} catch (e) {
return Left(UnexpectedFailure(e.toString()));
}
}
Future<Either<Failure, EvaluationAide>> signalerEvaluation({
required String evaluationId,
required String motif,
}) async {
try {
if (await isConnected()) {
final result = await remoteDataSource.signalerEvaluation(
evaluationId: evaluationId,
motif: motif,
);
// Mettre à jour le cache
await localDataSource.cacherEvaluation(result);
return Right(result.toEntity());
} else {
return Left(NetworkFailure('Aucune connexion internet disponible'));
}
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} on CacheException catch (e) {
return Left(CacheFailure(e.message));
} catch (e) {
return Left(UnexpectedFailure(e.toString()));
}
}
Future<Either<Failure, StatistiquesEvaluation>> calculerMoyenneDemande(String demandeId) async {
try {
if (await isConnected()) {
final result = await remoteDataSource.calculerMoyenneDemande(demandeId);
return Right(result.toEntity());
} else {
return Left(NetworkFailure('Aucune connexion internet disponible'));
}
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} catch (e) {
return Left(UnexpectedFailure(e.toString()));
}
}
Future<Either<Failure, StatistiquesEvaluation>> calculerMoyenneProposition(String propositionId) async {
try {
if (await isConnected()) {
final result = await remoteDataSource.calculerMoyenneProposition(propositionId);
return Right(result.toEntity());
} else {
return Left(NetworkFailure('Aucune connexion internet disponible'));
}
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} catch (e) {
return Left(UnexpectedFailure(e.toString()));
}
}
// Statistiques
Future<Either<Failure, Map<String, dynamic>>> obtenirStatistiquesSolidarite(String organisationId) async {
try {
// Essayer d'abord le cache local
final cachedStats = await localDataSource.obtenirStatistiquesCachees(organisationId);
if (cachedStats != null && await _estCacheStatistiquesValide(organisationId)) {
return Right(cachedStats);
}
// Si pas en cache ou cache expiré, aller chercher sur le serveur
if (await isConnected()) {
final result = await remoteDataSource.obtenirStatistiquesSolidarite(organisationId);
// Mettre en cache le résultat
await localDataSource.cacherStatistiques(organisationId, result);
return Right(result);
} else {
// Si pas de connexion, utiliser le cache même s'il est expiré
if (cachedStats != null) {
return Right(cachedStats);
}
return Left(NetworkFailure('Aucune connexion internet et aucune donnée en cache'));
}
} on ServerException catch (e) {
return Left(ServerFailure(e.message));
} on CacheException catch (e) {
return Left(CacheFailure(e.message));
} catch (e) {
return Left(UnexpectedFailure(e.toString()));
}
}
// Méthodes utilitaires privées
Future<bool> _estCacheEvaluationsValide() async {
try {
final localDataSourceImpl = localDataSource as SolidariteLocalDataSourceImpl;
return await localDataSourceImpl.estCacheEvaluationsValide();
} catch (e) {
return false;
}
}
Future<bool> _estCacheStatistiquesValide(String organisationId) async {
try {
final localDataSourceImpl = localDataSource as SolidariteLocalDataSourceImpl;
return await localDataSourceImpl.estCacheStatistiquesValide(organisationId);
} catch (e) {
return false;
}
}
}

View File

@@ -0,0 +1,481 @@
import 'package:equatable/equatable.dart';
/// Entité représentant une demande d'aide dans le système de solidarité
///
/// Cette entité encapsule toutes les informations relatives à une demande d'aide,
/// incluant les détails du demandeur, le type d'aide, les montants et le statut.
class DemandeAide extends Equatable {
/// Identifiant unique de la demande
final String id;
/// Numéro de référence unique (format: DA-YYYY-NNNNNN)
final String numeroReference;
/// Titre de la demande d'aide
final String titre;
/// Description détaillée de la demande
final String description;
/// Type d'aide demandée
final TypeAide typeAide;
/// Statut actuel de la demande
final StatutAide statut;
/// Priorité de la demande
final PrioriteAide priorite;
/// Identifiant du demandeur
final String demandeurId;
/// Nom complet du demandeur
final String nomDemandeur;
/// Identifiant de l'organisation
final String organisationId;
/// Montant demandé (si applicable)
final double? montantDemande;
/// Montant approuvé (si applicable)
final double? montantApprouve;
/// Montant versé (si applicable)
final double? montantVerse;
/// Date de création de la demande
final DateTime dateCreation;
/// Date de modification
final DateTime dateModification;
/// Date de soumission
final DateTime? dateSoumission;
/// Date d'évaluation
final DateTime? dateEvaluation;
/// Date d'approbation
final DateTime? dateApprobation;
/// Date limite de traitement
final DateTime? dateLimiteTraitement;
/// Identifiant de l'évaluateur assigné
final String? evaluateurId;
/// Commentaires de l'évaluateur
final String? commentairesEvaluateur;
/// Motif de rejet (si applicable)
final String? motifRejet;
/// Informations complémentaires requises
final String? informationsRequises;
/// Justification de l'urgence
final String? justificationUrgence;
/// Contact d'urgence
final ContactUrgence? contactUrgence;
/// Localisation du demandeur
final Localisation? localisation;
/// Liste des bénéficiaires
final List<BeneficiaireAide> beneficiaires;
/// Liste des pièces justificatives
final List<PieceJustificative> piecesJustificatives;
/// Historique des changements de statut
final List<HistoriqueStatut> historiqueStatuts;
/// Commentaires et échanges
final List<CommentaireAide> commentaires;
/// Données personnalisées
final Map<String, dynamic> donneesPersonnalisees;
/// Indique si la demande est modifiable
final bool estModifiable;
/// Indique si la demande est urgente
final bool estUrgente;
/// Indique si le délai est dépassé
final bool delaiDepasse;
/// Indique si la demande est terminée
final bool estTerminee;
const DemandeAide({
required this.id,
required this.numeroReference,
required this.titre,
required this.description,
required this.typeAide,
required this.statut,
required this.priorite,
required this.demandeurId,
required this.nomDemandeur,
required this.organisationId,
this.montantDemande,
this.montantApprouve,
this.montantVerse,
required this.dateCreation,
required this.dateModification,
this.dateSoumission,
this.dateEvaluation,
this.dateApprobation,
this.dateLimiteTraitement,
this.evaluateurId,
this.commentairesEvaluateur,
this.motifRejet,
this.informationsRequises,
this.justificationUrgence,
this.contactUrgence,
this.localisation,
this.beneficiaires = const [],
this.piecesJustificatives = const [],
this.historiqueStatuts = const [],
this.commentaires = const [],
this.donneesPersonnalisees = const {},
this.estModifiable = false,
this.estUrgente = false,
this.delaiDepasse = false,
this.estTerminee = false,
});
/// Calcule le pourcentage d'avancement de la demande
double get pourcentageAvancement {
return statut.pourcentageAvancement;
}
/// Calcule le délai restant en heures
int? get delaiRestantHeures {
if (dateLimiteTraitement == null) return null;
final maintenant = DateTime.now();
if (maintenant.isAfter(dateLimiteTraitement!)) return 0;
return dateLimiteTraitement!.difference(maintenant).inHours;
}
/// Calcule la durée de traitement en jours
int get dureeTraitementJours {
if (dateSoumission == null) return 0;
final dateFin = dateEvaluation ?? DateTime.now();
return dateFin.difference(dateSoumission!).inDays;
}
/// Indique si la demande nécessite une action urgente
bool get necessiteActionUrgente {
return estUrgente || delaiDepasse || priorite == PrioriteAide.critique;
}
/// Obtient la couleur associée au statut
String get couleurStatut => statut.couleur;
/// Obtient l'icône associée au type d'aide
String get iconeTypeAide => typeAide.icone;
@override
List<Object?> get props => [
id,
numeroReference,
titre,
description,
typeAide,
statut,
priorite,
demandeurId,
nomDemandeur,
organisationId,
montantDemande,
montantApprouve,
montantVerse,
dateCreation,
dateModification,
dateSoumission,
dateEvaluation,
dateApprobation,
dateLimiteTraitement,
evaluateurId,
commentairesEvaluateur,
motifRejet,
informationsRequises,
justificationUrgence,
contactUrgence,
localisation,
beneficiaires,
piecesJustificatives,
historiqueStatuts,
commentaires,
donneesPersonnalisees,
estModifiable,
estUrgente,
delaiDepasse,
estTerminee,
];
DemandeAide copyWith({
String? id,
String? numeroReference,
String? titre,
String? description,
TypeAide? typeAide,
StatutAide? statut,
PrioriteAide? priorite,
String? demandeurId,
String? nomDemandeur,
String? organisationId,
double? montantDemande,
double? montantApprouve,
double? montantVerse,
DateTime? dateCreation,
DateTime? dateModification,
DateTime? dateSoumission,
DateTime? dateEvaluation,
DateTime? dateApprobation,
DateTime? dateLimiteTraitement,
String? evaluateurId,
String? commentairesEvaluateur,
String? motifRejet,
String? informationsRequises,
String? justificationUrgence,
ContactUrgence? contactUrgence,
Localisation? localisation,
List<BeneficiaireAide>? beneficiaires,
List<PieceJustificative>? piecesJustificatives,
List<HistoriqueStatut>? historiqueStatuts,
List<CommentaireAide>? commentaires,
Map<String, dynamic>? donneesPersonnalisees,
bool? estModifiable,
bool? estUrgente,
bool? delaiDepasse,
bool? estTerminee,
}) {
return DemandeAide(
id: id ?? this.id,
numeroReference: numeroReference ?? this.numeroReference,
titre: titre ?? this.titre,
description: description ?? this.description,
typeAide: typeAide ?? this.typeAide,
statut: statut ?? this.statut,
priorite: priorite ?? this.priorite,
demandeurId: demandeurId ?? this.demandeurId,
nomDemandeur: nomDemandeur ?? this.nomDemandeur,
organisationId: organisationId ?? this.organisationId,
montantDemande: montantDemande ?? this.montantDemande,
montantApprouve: montantApprouve ?? this.montantApprouve,
montantVerse: montantVerse ?? this.montantVerse,
dateCreation: dateCreation ?? this.dateCreation,
dateModification: dateModification ?? this.dateModification,
dateSoumission: dateSoumission ?? this.dateSoumission,
dateEvaluation: dateEvaluation ?? this.dateEvaluation,
dateApprobation: dateApprobation ?? this.dateApprobation,
dateLimiteTraitement: dateLimiteTraitement ?? this.dateLimiteTraitement,
evaluateurId: evaluateurId ?? this.evaluateurId,
commentairesEvaluateur: commentairesEvaluateur ?? this.commentairesEvaluateur,
motifRejet: motifRejet ?? this.motifRejet,
informationsRequises: informationsRequises ?? this.informationsRequises,
justificationUrgence: justificationUrgence ?? this.justificationUrgence,
contactUrgence: contactUrgence ?? this.contactUrgence,
localisation: localisation ?? this.localisation,
beneficiaires: beneficiaires ?? this.beneficiaires,
piecesJustificatives: piecesJustificatives ?? this.piecesJustificatives,
historiqueStatuts: historiqueStatuts ?? this.historiqueStatuts,
commentaires: commentaires ?? this.commentaires,
donneesPersonnalisees: donneesPersonnalisees ?? this.donneesPersonnalisees,
estModifiable: estModifiable ?? this.estModifiable,
estUrgente: estUrgente ?? this.estUrgente,
delaiDepasse: delaiDepasse ?? this.delaiDepasse,
estTerminee: estTerminee ?? this.estTerminee,
);
}
}
/// Énumération des types d'aide disponibles
enum TypeAide {
aideFinanciereUrgente('Aide financière urgente', 'emergency_fund', '#F44336'),
aideFinanciereMedicale('Aide financière médicale', 'medical_services', '#2196F3'),
aideFinanciereEducation('Aide financière éducation', 'school', '#4CAF50'),
aideMaterielleVetements('Aide matérielle vêtements', 'checkroom', '#FF9800'),
aideMaterielleNourriture('Aide matérielle nourriture', 'restaurant', '#795548'),
aideProfessionnelleFormation('Aide professionnelle formation', 'work', '#9C27B0'),
aideSocialeAccompagnement('Aide sociale accompagnement', 'support', '#607D8B'),
autre('Autre', 'help', '#9E9E9E');
const TypeAide(this.libelle, this.icone, this.couleur);
final String libelle;
final String icone;
final String couleur;
}
/// Énumération des statuts de demande d'aide
enum StatutAide {
brouillon('Brouillon', 'draft', '#9E9E9E', 5.0),
soumise('Soumise', 'send', '#2196F3', 10.0),
enAttente('En attente', 'schedule', '#FF9800', 20.0),
enCoursEvaluation('En cours d\'évaluation', 'assessment', '#9C27B0', 40.0),
approuvee('Approuvée', 'check_circle', '#4CAF50', 70.0),
approuveePartiellement('Approuvée partiellement', 'check_circle_outline', '#8BC34A', 70.0),
rejetee('Rejetée', 'cancel', '#F44336', 100.0),
informationsRequises('Informations requises', 'info', '#FF5722', 30.0),
enCoursVersement('En cours de versement', 'payment', '#00BCD4', 85.0),
versee('Versée', 'paid', '#4CAF50', 100.0),
livree('Livrée', 'local_shipping', '#4CAF50', 100.0),
terminee('Terminée', 'done_all', '#4CAF50', 100.0),
cloturee('Clôturée', 'archive', '#607D8B', 100.0);
const StatutAide(this.libelle, this.icone, this.couleur, this.pourcentageAvancement);
final String libelle;
final String icone;
final String couleur;
final double pourcentageAvancement;
}
/// Énumération des priorités de demande d'aide
enum PrioriteAide {
critique('Critique', '#F44336', 1, 24),
urgente('Urgente', '#FF5722', 2, 72),
elevee('Élevée', '#FF9800', 3, 168),
normale('Normale', '#4CAF50', 4, 336),
faible('Faible', '#9E9E9E', 5, 720);
const PrioriteAide(this.libelle, this.couleur, this.niveau, this.delaiTraitementHeures);
final String libelle;
final String couleur;
final int niveau;
final int delaiTraitementHeures;
}
/// Classe représentant un contact d'urgence
class ContactUrgence extends Equatable {
final String nom;
final String telephone;
final String? email;
final String relation;
const ContactUrgence({
required this.nom,
required this.telephone,
this.email,
required this.relation,
});
@override
List<Object?> get props => [nom, telephone, email, relation];
}
/// Classe représentant une localisation
class Localisation extends Equatable {
final String adresse;
final String ville;
final String? codePostal;
final String? pays;
final double? latitude;
final double? longitude;
const Localisation({
required this.adresse,
required this.ville,
this.codePostal,
this.pays,
this.latitude,
this.longitude,
});
@override
List<Object?> get props => [adresse, ville, codePostal, pays, latitude, longitude];
}
/// Classe représentant un bénéficiaire d'aide
class BeneficiaireAide extends Equatable {
final String nom;
final String prenom;
final int age;
final String relation;
final String? telephone;
const BeneficiaireAide({
required this.nom,
required this.prenom,
required this.age,
required this.relation,
this.telephone,
});
@override
List<Object?> get props => [nom, prenom, age, relation, telephone];
}
/// Classe représentant une pièce justificative
class PieceJustificative extends Equatable {
final String id;
final String nom;
final String type;
final String url;
final int taille;
final DateTime dateAjout;
const PieceJustificative({
required this.id,
required this.nom,
required this.type,
required this.url,
required this.taille,
required this.dateAjout,
});
@override
List<Object?> get props => [id, nom, type, url, taille, dateAjout];
}
/// Classe représentant l'historique des statuts
class HistoriqueStatut extends Equatable {
final StatutAide ancienStatut;
final StatutAide nouveauStatut;
final DateTime dateChangement;
final String? commentaire;
final String? utilisateurId;
const HistoriqueStatut({
required this.ancienStatut,
required this.nouveauStatut,
required this.dateChangement,
this.commentaire,
this.utilisateurId,
});
@override
List<Object?> get props => [ancienStatut, nouveauStatut, dateChangement, commentaire, utilisateurId];
}
/// Classe représentant un commentaire sur une demande
class CommentaireAide extends Equatable {
final String id;
final String contenu;
final String auteurId;
final String nomAuteur;
final DateTime dateCreation;
final bool estPrive;
const CommentaireAide({
required this.id,
required this.contenu,
required this.auteurId,
required this.nomAuteur,
required this.dateCreation,
this.estPrive = false,
});
@override
List<Object?> get props => [id, contenu, auteurId, nomAuteur, dateCreation, estPrive];
}

View File

@@ -0,0 +1,303 @@
import 'package:equatable/equatable.dart';
/// Entité représentant une évaluation d'aide dans le système de solidarité
///
/// Cette entité encapsule toutes les informations relatives à l'évaluation
/// d'une demande d'aide ou d'une proposition d'aide.
class EvaluationAide extends Equatable {
/// Identifiant unique de l'évaluation
final String id;
/// Identifiant de la demande d'aide évaluée
final String demandeAideId;
/// Identifiant de la proposition d'aide (si applicable)
final String? propositionAideId;
/// Identifiant de l'évaluateur
final String evaluateurId;
/// Nom de l'évaluateur
final String nomEvaluateur;
/// Type d'évaluateur
final TypeEvaluateur typeEvaluateur;
/// Note globale (1 à 5)
final double noteGlobale;
/// Note pour le délai de réponse
final double? noteDelaiReponse;
/// Note pour la communication
final double? noteCommunication;
/// Note pour le professionnalisme
final double? noteProfessionnalisme;
/// Note pour le respect des engagements
final double? noteRespectEngagements;
/// Notes détaillées par critère
final Map<String, double> notesDetaillees;
/// Commentaire principal
final String commentairePrincipal;
/// Points positifs
final String? pointsPositifs;
/// Points d'amélioration
final String? pointsAmelioration;
/// Recommandations
final String? recommandations;
/// Indique si l'évaluateur recommande cette aide
final bool? recommande;
/// Date de création de l'évaluation
final DateTime dateCreation;
/// Date de modification
final DateTime dateModification;
/// Date de vérification (si applicable)
final DateTime? dateVerification;
/// Identifiant du vérificateur
final String? verificateurId;
/// Statut de l'évaluation
final StatutEvaluation statut;
/// Nombre de signalements reçus
final int nombreSignalements;
/// Score de qualité calculé automatiquement
final double scoreQualite;
/// Indique si l'évaluation a été modifiée
final bool estModifie;
/// Indique si l'évaluation est vérifiée
final bool estVerifiee;
/// Données personnalisées
final Map<String, dynamic> donneesPersonnalisees;
const EvaluationAide({
required this.id,
required this.demandeAideId,
this.propositionAideId,
required this.evaluateurId,
required this.nomEvaluateur,
required this.typeEvaluateur,
required this.noteGlobale,
this.noteDelaiReponse,
this.noteCommunication,
this.noteProfessionnalisme,
this.noteRespectEngagements,
this.notesDetaillees = const {},
required this.commentairePrincipal,
this.pointsPositifs,
this.pointsAmelioration,
this.recommandations,
this.recommande,
required this.dateCreation,
required this.dateModification,
this.dateVerification,
this.verificateurId,
this.statut = StatutEvaluation.active,
this.nombreSignalements = 0,
required this.scoreQualite,
this.estModifie = false,
this.estVerifiee = false,
this.donneesPersonnalisees = const {},
});
/// Calcule la note moyenne des critères détaillés
double get noteMoyenneDetaillees {
if (notesDetaillees.isEmpty) return noteGlobale;
double somme = notesDetaillees.values.fold(0.0, (a, b) => a + b);
return somme / notesDetaillees.length;
}
/// Indique si l'évaluation est positive (note >= 4)
bool get estPositive => noteGlobale >= 4.0;
/// Indique si l'évaluation est négative (note <= 2)
bool get estNegative => noteGlobale <= 2.0;
/// Obtient le niveau de satisfaction textuel
String get niveauSatisfaction {
if (noteGlobale >= 4.5) return 'Excellent';
if (noteGlobale >= 4.0) return 'Très bien';
if (noteGlobale >= 3.0) return 'Bien';
if (noteGlobale >= 2.0) return 'Moyen';
return 'Insuffisant';
}
/// Obtient la couleur associée à la note
String get couleurNote {
if (noteGlobale >= 4.0) return '#4CAF50'; // Vert
if (noteGlobale >= 3.0) return '#FF9800'; // Orange
return '#F44336'; // Rouge
}
/// Indique si l'évaluation peut être modifiée
bool get peutEtreModifiee {
return statut == StatutEvaluation.active &&
!estVerifiee &&
nombreSignalements < 3;
}
@override
List<Object?> get props => [
id,
demandeAideId,
propositionAideId,
evaluateurId,
nomEvaluateur,
typeEvaluateur,
noteGlobale,
noteDelaiReponse,
noteCommunication,
noteProfessionnalisme,
noteRespectEngagements,
notesDetaillees,
commentairePrincipal,
pointsPositifs,
pointsAmelioration,
recommandations,
recommande,
dateCreation,
dateModification,
dateVerification,
verificateurId,
statut,
nombreSignalements,
scoreQualite,
estModifie,
estVerifiee,
donneesPersonnalisees,
];
EvaluationAide copyWith({
String? id,
String? demandeAideId,
String? propositionAideId,
String? evaluateurId,
String? nomEvaluateur,
TypeEvaluateur? typeEvaluateur,
double? noteGlobale,
double? noteDelaiReponse,
double? noteCommunication,
double? noteProfessionnalisme,
double? noteRespectEngagements,
Map<String, double>? notesDetaillees,
String? commentairePrincipal,
String? pointsPositifs,
String? pointsAmelioration,
String? recommandations,
bool? recommande,
DateTime? dateCreation,
DateTime? dateModification,
DateTime? dateVerification,
String? verificateurId,
StatutEvaluation? statut,
int? nombreSignalements,
double? scoreQualite,
bool? estModifie,
bool? estVerifiee,
Map<String, dynamic>? donneesPersonnalisees,
}) {
return EvaluationAide(
id: id ?? this.id,
demandeAideId: demandeAideId ?? this.demandeAideId,
propositionAideId: propositionAideId ?? this.propositionAideId,
evaluateurId: evaluateurId ?? this.evaluateurId,
nomEvaluateur: nomEvaluateur ?? this.nomEvaluateur,
typeEvaluateur: typeEvaluateur ?? this.typeEvaluateur,
noteGlobale: noteGlobale ?? this.noteGlobale,
noteDelaiReponse: noteDelaiReponse ?? this.noteDelaiReponse,
noteCommunication: noteCommunication ?? this.noteCommunication,
noteProfessionnalisme: noteProfessionnalisme ?? this.noteProfessionnalisme,
noteRespectEngagements: noteRespectEngagements ?? this.noteRespectEngagements,
notesDetaillees: notesDetaillees ?? this.notesDetaillees,
commentairePrincipal: commentairePrincipal ?? this.commentairePrincipal,
pointsPositifs: pointsPositifs ?? this.pointsPositifs,
pointsAmelioration: pointsAmelioration ?? this.pointsAmelioration,
recommandations: recommandations ?? this.recommandations,
recommande: recommande ?? this.recommande,
dateCreation: dateCreation ?? this.dateCreation,
dateModification: dateModification ?? this.dateModification,
dateVerification: dateVerification ?? this.dateVerification,
verificateurId: verificateurId ?? this.verificateurId,
statut: statut ?? this.statut,
nombreSignalements: nombreSignalements ?? this.nombreSignalements,
scoreQualite: scoreQualite ?? this.scoreQualite,
estModifie: estModifie ?? this.estModifie,
estVerifiee: estVerifiee ?? this.estVerifiee,
donneesPersonnalisees: donneesPersonnalisees ?? this.donneesPersonnalisees,
);
}
}
/// Énumération des types d'évaluateur
enum TypeEvaluateur {
beneficiaire('Bénéficiaire', 'person', '#2196F3'),
proposant('Proposant', 'volunteer_activism', '#4CAF50'),
evaluateurOfficial('Évaluateur officiel', 'verified_user', '#9C27B0'),
administrateur('Administrateur', 'admin_panel_settings', '#FF5722');
const TypeEvaluateur(this.libelle, this.icone, this.couleur);
final String libelle;
final String icone;
final String couleur;
}
/// Énumération des statuts d'évaluation
enum StatutEvaluation {
active('Active', 'check_circle', '#4CAF50'),
signalee('Signalée', 'flag', '#FF9800'),
masquee('Masquée', 'visibility_off', '#F44336'),
supprimee('Supprimée', 'delete', '#9E9E9E');
const StatutEvaluation(this.libelle, this.icone, this.couleur);
final String libelle;
final String icone;
final String couleur;
}
/// Classe représentant les statistiques d'évaluations
class StatistiquesEvaluation extends Equatable {
final double noteMoyenne;
final int nombreEvaluations;
final Map<int, int> repartitionNotes;
final double pourcentagePositives;
final double pourcentageRecommandations;
final DateTime derniereMiseAJour;
const StatistiquesEvaluation({
required this.noteMoyenne,
required this.nombreEvaluations,
required this.repartitionNotes,
required this.pourcentagePositives,
required this.pourcentageRecommandations,
required this.derniereMiseAJour,
});
@override
List<Object?> get props => [
noteMoyenne,
nombreEvaluations,
repartitionNotes,
pourcentagePositives,
pourcentageRecommandations,
derniereMiseAJour,
];
}

View File

@@ -0,0 +1,401 @@
import 'package:equatable/equatable.dart';
import 'demande_aide.dart';
/// Entité représentant une proposition d'aide dans le système de solidarité
///
/// Cette entité encapsule toutes les informations relatives à une proposition d'aide,
/// incluant les détails du proposant, les capacités et les conditions.
class PropositionAide extends Equatable {
/// Identifiant unique de la proposition
final String id;
/// Numéro de référence unique (format: PA-YYYY-NNNNNN)
final String numeroReference;
/// Titre de la proposition d'aide
final String titre;
/// Description détaillée de la proposition
final String description;
/// Type d'aide proposée
final TypeAide typeAide;
/// Statut actuel de la proposition
final StatutProposition statut;
/// Identifiant du proposant
final String proposantId;
/// Nom complet du proposant
final String nomProposant;
/// Identifiant de l'organisation
final String organisationId;
/// Montant maximum proposé (si applicable)
final double? montantMaximum;
/// Montant minimum proposé (si applicable)
final double? montantMinimum;
/// Nombre maximum de bénéficiaires
final int nombreMaxBeneficiaires;
/// Nombre de bénéficiaires déjà aidés
final int nombreBeneficiairesAides;
/// Nombre de demandes traitées
final int nombreDemandesTraitees;
/// Montant total versé
final double montantTotalVerse;
/// Date de création de la proposition
final DateTime dateCreation;
/// Date de modification
final DateTime dateModification;
/// Date d'expiration
final DateTime? dateExpiration;
/// Délai de réponse en heures
final int delaiReponseHeures;
/// Zones géographiques couvertes
final List<String> zonesGeographiques;
/// Créneaux de disponibilité
final List<CreneauDisponibilite> creneauxDisponibilite;
/// Critères de sélection
final List<CritereSelection> criteresSelection;
/// Contact du proposant
final ContactProposant contactProposant;
/// Conditions particulières
final String? conditionsParticulieres;
/// Instructions spéciales
final String? instructionsSpeciales;
/// Note moyenne des évaluations
final double? noteMoyenne;
/// Nombre d'évaluations reçues
final int nombreEvaluations;
/// Nombre de vues de la proposition
final int nombreVues;
/// Nombre de candidatures reçues
final int nombreCandidatures;
/// Score de pertinence calculé
final double scorePertinence;
/// Données personnalisées
final Map<String, dynamic> donneesPersonnalisees;
/// Indique si la proposition est disponible
final bool estDisponible;
/// Indique si la proposition est vérifiée
final bool estVerifiee;
/// Indique si la proposition est expirée
final bool estExpiree;
const PropositionAide({
required this.id,
required this.numeroReference,
required this.titre,
required this.description,
required this.typeAide,
required this.statut,
required this.proposantId,
required this.nomProposant,
required this.organisationId,
this.montantMaximum,
this.montantMinimum,
required this.nombreMaxBeneficiaires,
this.nombreBeneficiairesAides = 0,
this.nombreDemandesTraitees = 0,
this.montantTotalVerse = 0.0,
required this.dateCreation,
required this.dateModification,
this.dateExpiration,
this.delaiReponseHeures = 48,
this.zonesGeographiques = const [],
this.creneauxDisponibilite = const [],
this.criteresSelection = const [],
required this.contactProposant,
this.conditionsParticulieres,
this.instructionsSpeciales,
this.noteMoyenne,
this.nombreEvaluations = 0,
this.nombreVues = 0,
this.nombreCandidatures = 0,
this.scorePertinence = 50.0,
this.donneesPersonnalisees = const {},
this.estDisponible = true,
this.estVerifiee = false,
this.estExpiree = false,
});
/// Calcule le nombre de places restantes
int get placesRestantes {
return nombreMaxBeneficiaires - nombreBeneficiairesAides;
}
/// Calcule le pourcentage de capacité utilisée
double get pourcentageCapaciteUtilisee {
if (nombreMaxBeneficiaires == 0) return 0.0;
return (nombreBeneficiairesAides / nombreMaxBeneficiaires) * 100;
}
/// Indique si la proposition peut accepter de nouveaux bénéficiaires
bool get peutAccepterBeneficiaires {
return estDisponible && !estExpiree && placesRestantes > 0;
}
/// Indique si la proposition est active et disponible
bool get isActiveEtDisponible {
return statut == StatutProposition.active && estDisponible && !estExpiree;
}
/// Calcule un score de compatibilité avec une demande
double calculerScoreCompatibilite(DemandeAide demande) {
double score = 0.0;
// Correspondance du type d'aide (40 points max)
if (demande.typeAide == typeAide) {
score += 40.0;
} else {
// Bonus partiel pour les types similaires
score += 20.0;
}
// Compatibilité financière (25 points max)
if (demande.montantDemande != null && montantMaximum != null) {
if (demande.montantDemande! <= montantMaximum!) {
score += 25.0;
} else {
// Pénalité proportionnelle
double ratio = montantMaximum! / demande.montantDemande!;
score += 25.0 * ratio;
}
} else if (demande.montantDemande == null) {
score += 25.0; // Pas de contrainte financière
}
// Expérience du proposant (15 points max)
if (nombreBeneficiairesAides > 0) {
score += (nombreBeneficiairesAides * 2.0).clamp(0.0, 15.0);
}
// Réputation (10 points max)
if (noteMoyenne != null && nombreEvaluations >= 3) {
score += (noteMoyenne! - 3.0) * 3.33;
}
// Disponibilité (10 points max)
if (peutAccepterBeneficiaires) {
double ratioCapacite = placesRestantes / nombreMaxBeneficiaires;
score += 10.0 * ratioCapacite;
}
return score.clamp(0.0, 100.0);
}
/// Obtient la couleur associée au statut
String get couleurStatut => statut.couleur;
/// Obtient l'icône associée au type d'aide
String get iconeTypeAide => typeAide.icone;
@override
List<Object?> get props => [
id,
numeroReference,
titre,
description,
typeAide,
statut,
proposantId,
nomProposant,
organisationId,
montantMaximum,
montantMinimum,
nombreMaxBeneficiaires,
nombreBeneficiairesAides,
nombreDemandesTraitees,
montantTotalVerse,
dateCreation,
dateModification,
dateExpiration,
delaiReponseHeures,
zonesGeographiques,
creneauxDisponibilite,
criteresSelection,
contactProposant,
conditionsParticulieres,
instructionsSpeciales,
noteMoyenne,
nombreEvaluations,
nombreVues,
nombreCandidatures,
scorePertinence,
donneesPersonnalisees,
estDisponible,
estVerifiee,
estExpiree,
];
PropositionAide copyWith({
String? id,
String? numeroReference,
String? titre,
String? description,
TypeAide? typeAide,
StatutProposition? statut,
String? proposantId,
String? nomProposant,
String? organisationId,
double? montantMaximum,
double? montantMinimum,
int? nombreMaxBeneficiaires,
int? nombreBeneficiairesAides,
int? nombreDemandesTraitees,
double? montantTotalVerse,
DateTime? dateCreation,
DateTime? dateModification,
DateTime? dateExpiration,
int? delaiReponseHeures,
List<String>? zonesGeographiques,
List<CreneauDisponibilite>? creneauxDisponibilite,
List<CritereSelection>? criteresSelection,
ContactProposant? contactProposant,
String? conditionsParticulieres,
String? instructionsSpeciales,
double? noteMoyenne,
int? nombreEvaluations,
int? nombreVues,
int? nombreCandidatures,
double? scorePertinence,
Map<String, dynamic>? donneesPersonnalisees,
bool? estDisponible,
bool? estVerifiee,
bool? estExpiree,
}) {
return PropositionAide(
id: id ?? this.id,
numeroReference: numeroReference ?? this.numeroReference,
titre: titre ?? this.titre,
description: description ?? this.description,
typeAide: typeAide ?? this.typeAide,
statut: statut ?? this.statut,
proposantId: proposantId ?? this.proposantId,
nomProposant: nomProposant ?? this.nomProposant,
organisationId: organisationId ?? this.organisationId,
montantMaximum: montantMaximum ?? this.montantMaximum,
montantMinimum: montantMinimum ?? this.montantMinimum,
nombreMaxBeneficiaires: nombreMaxBeneficiaires ?? this.nombreMaxBeneficiaires,
nombreBeneficiairesAides: nombreBeneficiairesAides ?? this.nombreBeneficiairesAides,
nombreDemandesTraitees: nombreDemandesTraitees ?? this.nombreDemandesTraitees,
montantTotalVerse: montantTotalVerse ?? this.montantTotalVerse,
dateCreation: dateCreation ?? this.dateCreation,
dateModification: dateModification ?? this.dateModification,
dateExpiration: dateExpiration ?? this.dateExpiration,
delaiReponseHeures: delaiReponseHeures ?? this.delaiReponseHeures,
zonesGeographiques: zonesGeographiques ?? this.zonesGeographiques,
creneauxDisponibilite: creneauxDisponibilite ?? this.creneauxDisponibilite,
criteresSelection: criteresSelection ?? this.criteresSelection,
contactProposant: contactProposant ?? this.contactProposant,
conditionsParticulieres: conditionsParticulieres ?? this.conditionsParticulieres,
instructionsSpeciales: instructionsSpeciales ?? this.instructionsSpeciales,
noteMoyenne: noteMoyenne ?? this.noteMoyenne,
nombreEvaluations: nombreEvaluations ?? this.nombreEvaluations,
nombreVues: nombreVues ?? this.nombreVues,
nombreCandidatures: nombreCandidatures ?? this.nombreCandidatures,
scorePertinence: scorePertinence ?? this.scorePertinence,
donneesPersonnalisees: donneesPersonnalisees ?? this.donneesPersonnalisees,
estDisponible: estDisponible ?? this.estDisponible,
estVerifiee: estVerifiee ?? this.estVerifiee,
estExpiree: estExpiree ?? this.estExpiree,
);
}
}
/// Énumération des statuts de proposition d'aide
enum StatutProposition {
active('Active', 'check_circle', '#4CAF50'),
suspendue('Suspendue', 'pause_circle', '#FF9800'),
terminee('Terminée', 'done_all', '#607D8B'),
expiree('Expirée', 'schedule', '#9E9E9E'),
supprimee('Supprimée', 'delete', '#F44336');
const StatutProposition(this.libelle, this.icone, this.couleur);
final String libelle;
final String icone;
final String couleur;
}
/// Classe représentant un créneau de disponibilité
class CreneauDisponibilite extends Equatable {
final String jourSemaine;
final String heureDebut;
final String heureFin;
final String? commentaire;
const CreneauDisponibilite({
required this.jourSemaine,
required this.heureDebut,
required this.heureFin,
this.commentaire,
});
@override
List<Object?> get props => [jourSemaine, heureDebut, heureFin, commentaire];
}
/// Classe représentant un critère de sélection
class CritereSelection extends Equatable {
final String nom;
final String valeur;
final bool estObligatoire;
final String? description;
const CritereSelection({
required this.nom,
required this.valeur,
this.estObligatoire = false,
this.description,
});
@override
List<Object?> get props => [nom, valeur, estObligatoire, description];
}
/// Classe représentant le contact d'un proposant
class ContactProposant extends Equatable {
final String nom;
final String telephone;
final String? email;
final String? adresse;
final String? methodePrefereee;
const ContactProposant({
required this.nom,
required this.telephone,
this.email,
this.adresse,
this.methodePrefereee,
});
@override
List<Object?> get props => [nom, telephone, email, adresse, methodePrefereee];
}

View File

@@ -0,0 +1,251 @@
import 'package:dartz/dartz.dart';
import '../../../../core/error/failures.dart';
import '../entities/demande_aide.dart';
import '../entities/proposition_aide.dart';
import '../entities/evaluation_aide.dart';
/// Repository abstrait pour la gestion de la solidarité
///
/// Ce repository définit les contrats pour toutes les opérations
/// liées au système de solidarité : demandes, propositions, évaluations.
abstract class SolidariteRepository {
// === GESTION DES DEMANDES D'AIDE ===
/// Crée une nouvelle demande d'aide
///
/// [demande] La demande d'aide à créer
/// Retourne [Right(DemandeAide)] en cas de succès
/// Retourne [Left(Failure)] en cas d'erreur
Future<Either<Failure, DemandeAide>> creerDemandeAide(DemandeAide demande);
/// Met à jour une demande d'aide existante
///
/// [demande] La demande d'aide à mettre à jour
/// Retourne [Right(DemandeAide)] en cas de succès
/// Retourne [Left(Failure)] en cas d'erreur
Future<Either<Failure, DemandeAide>> mettreAJourDemandeAide(DemandeAide demande);
/// Obtient une demande d'aide par son ID
///
/// [id] Identifiant de la demande
/// Retourne [Right(DemandeAide)] si trouvée
/// Retourne [Left(Failure)] si non trouvée ou erreur
Future<Either<Failure, DemandeAide>> obtenirDemandeAide(String id);
/// Soumet une demande d'aide pour évaluation
///
/// [demandeId] Identifiant de la demande
/// Retourne [Right(DemandeAide)] en cas de succès
/// Retourne [Left(Failure)] en cas d'erreur
Future<Either<Failure, DemandeAide>> soumettreDemande(String demandeId);
/// Évalue une demande d'aide
///
/// [demandeId] Identifiant de la demande
/// [evaluateurId] Identifiant de l'évaluateur
/// [decision] Décision d'évaluation
/// [commentaire] Commentaire de l'évaluateur
/// [montantApprouve] Montant approuvé (optionnel)
/// Retourne [Right(DemandeAide)] en cas de succès
/// Retourne [Left(Failure)] en cas d'erreur
Future<Either<Failure, DemandeAide>> evaluerDemande({
required String demandeId,
required String evaluateurId,
required StatutAide decision,
String? commentaire,
double? montantApprouve,
});
/// Recherche des demandes d'aide avec filtres
///
/// [filtres] Critères de recherche
/// Retourne [Right(List<DemandeAide>)] en cas de succès
/// Retourne [Left(Failure)] en cas d'erreur
Future<Either<Failure, List<DemandeAide>>> rechercherDemandes({
String? organisationId,
TypeAide? typeAide,
StatutAide? statut,
String? demandeurId,
bool? urgente,
int page = 0,
int taille = 20,
});
/// Obtient les demandes urgentes
///
/// [organisationId] Identifiant de l'organisation
/// Retourne [Right(List<DemandeAide>)] en cas de succès
/// Retourne [Left(Failure)] en cas d'erreur
Future<Either<Failure, List<DemandeAide>>> obtenirDemandesUrgentes(String organisationId);
/// Obtient les demandes de l'utilisateur connecté
///
/// [utilisateurId] Identifiant de l'utilisateur
/// Retourne [Right(List<DemandeAide>)] en cas de succès
/// Retourne [Left(Failure)] en cas d'erreur
Future<Either<Failure, List<DemandeAide>>> obtenirMesdemandes(String utilisateurId);
// === GESTION DES PROPOSITIONS D'AIDE ===
/// Crée une nouvelle proposition d'aide
///
/// [proposition] La proposition d'aide à créer
/// Retourne [Right(PropositionAide)] en cas de succès
/// Retourne [Left(Failure)] en cas d'erreur
Future<Either<Failure, PropositionAide>> creerPropositionAide(PropositionAide proposition);
/// Met à jour une proposition d'aide existante
///
/// [proposition] La proposition d'aide à mettre à jour
/// Retourne [Right(PropositionAide)] en cas de succès
/// Retourne [Left(Failure)] en cas d'erreur
Future<Either<Failure, PropositionAide>> mettreAJourPropositionAide(PropositionAide proposition);
/// Obtient une proposition d'aide par son ID
///
/// [id] Identifiant de la proposition
/// Retourne [Right(PropositionAide)] si trouvée
/// Retourne [Left(Failure)] si non trouvée ou erreur
Future<Either<Failure, PropositionAide>> obtenirPropositionAide(String id);
/// Active ou désactive une proposition d'aide
///
/// [propositionId] Identifiant de la proposition
/// [activer] true pour activer, false pour désactiver
/// Retourne [Right(PropositionAide)] en cas de succès
/// Retourne [Left(Failure)] en cas d'erreur
Future<Either<Failure, PropositionAide>> changerStatutProposition({
required String propositionId,
required bool activer,
});
/// Recherche des propositions d'aide avec filtres
///
/// [filtres] Critères de recherche
/// Retourne [Right(List<PropositionAide>)] en cas de succès
/// Retourne [Left(Failure)] en cas d'erreur
Future<Either<Failure, List<PropositionAide>>> rechercherPropositions({
String? organisationId,
TypeAide? typeAide,
String? proposantId,
bool? actives,
int page = 0,
int taille = 20,
});
/// Obtient les propositions actives pour un type d'aide
///
/// [typeAide] Type d'aide recherché
/// Retourne [Right(List<PropositionAide>)] en cas de succès
/// Retourne [Left(Failure)] en cas d'erreur
Future<Either<Failure, List<PropositionAide>>> obtenirPropositionsActives(TypeAide typeAide);
/// Obtient les meilleures propositions (top performers)
///
/// [limite] Nombre maximum de propositions à retourner
/// Retourne [Right(List<PropositionAide>)] en cas de succès
/// Retourne [Left(Failure)] en cas d'erreur
Future<Either<Failure, List<PropositionAide>>> obtenirMeilleuresPropositions(int limite);
/// Obtient les propositions de l'utilisateur connecté
///
/// [utilisateurId] Identifiant de l'utilisateur
/// Retourne [Right(List<PropositionAide>)] en cas de succès
/// Retourne [Left(Failure)] en cas d'erreur
Future<Either<Failure, List<PropositionAide>>> obtenirMesPropositions(String utilisateurId);
// === MATCHING ET COMPATIBILITÉ ===
/// Trouve les propositions compatibles avec une demande
///
/// [demandeId] Identifiant de la demande
/// Retourne [Right(List<PropositionAide>)] en cas de succès
/// Retourne [Left(Failure)] en cas d'erreur
Future<Either<Failure, List<PropositionAide>>> trouverPropositionsCompatibles(String demandeId);
/// Trouve les demandes compatibles avec une proposition
///
/// [propositionId] Identifiant de la proposition
/// Retourne [Right(List<DemandeAide>)] en cas de succès
/// Retourne [Left(Failure)] en cas d'erreur
Future<Either<Failure, List<DemandeAide>>> trouverDemandesCompatibles(String propositionId);
/// Recherche des proposants financiers pour une demande approuvée
///
/// [demandeId] Identifiant de la demande
/// Retourne [Right(List<PropositionAide>)] en cas de succès
/// Retourne [Left(Failure)] en cas d'erreur
Future<Either<Failure, List<PropositionAide>>> rechercherProposantsFinanciers(String demandeId);
// === GESTION DES ÉVALUATIONS ===
/// Crée une nouvelle évaluation
///
/// [evaluation] L'évaluation à créer
/// Retourne [Right(EvaluationAide)] en cas de succès
/// Retourne [Left(Failure)] en cas d'erreur
Future<Either<Failure, EvaluationAide>> creerEvaluation(EvaluationAide evaluation);
/// Met à jour une évaluation existante
///
/// [evaluation] L'évaluation à mettre à jour
/// Retourne [Right(EvaluationAide)] en cas de succès
/// Retourne [Left(Failure)] en cas d'erreur
Future<Either<Failure, EvaluationAide>> mettreAJourEvaluation(EvaluationAide evaluation);
/// Obtient une évaluation par son ID
///
/// [id] Identifiant de l'évaluation
/// Retourne [Right(EvaluationAide)] si trouvée
/// Retourne [Left(Failure)] si non trouvée ou erreur
Future<Either<Failure, EvaluationAide>> obtenirEvaluation(String id);
/// Obtient les évaluations d'une demande d'aide
///
/// [demandeId] Identifiant de la demande
/// Retourne [Right(List<EvaluationAide>)] en cas de succès
/// Retourne [Left(Failure)] en cas d'erreur
Future<Either<Failure, List<EvaluationAide>>> obtenirEvaluationsDemande(String demandeId);
/// Obtient les évaluations d'une proposition d'aide
///
/// [propositionId] Identifiant de la proposition
/// Retourne [Right(List<EvaluationAide>)] en cas de succès
/// Retourne [Left(Failure)] en cas d'erreur
Future<Either<Failure, List<EvaluationAide>>> obtenirEvaluationsProposition(String propositionId);
/// Signale une évaluation comme inappropriée
///
/// [evaluationId] Identifiant de l'évaluation
/// [motif] Motif du signalement
/// Retourne [Right(EvaluationAide)] en cas de succès
/// Retourne [Left(Failure)] en cas d'erreur
Future<Either<Failure, EvaluationAide>> signalerEvaluation({
required String evaluationId,
required String motif,
});
// === STATISTIQUES ET ANALYTICS ===
/// Obtient les statistiques de solidarité pour une organisation
///
/// [organisationId] Identifiant de l'organisation
/// Retourne [Right(Map<String, dynamic>)] en cas de succès
/// Retourne [Left(Failure)] en cas d'erreur
Future<Either<Failure, Map<String, dynamic>>> obtenirStatistiquesSolidarite(String organisationId);
/// Calcule la note moyenne d'une demande d'aide
///
/// [demandeId] Identifiant de la demande
/// Retourne [Right(StatistiquesEvaluation)] en cas de succès
/// Retourne [Left(Failure)] en cas d'erreur
Future<Either<Failure, StatistiquesEvaluation>> calculerMoyenneDemande(String demandeId);
/// Calcule la note moyenne d'une proposition d'aide
///
/// [propositionId] Identifiant de la proposition
/// Retourne [Right(StatistiquesEvaluation)] en cas de succès
/// Retourne [Left(Failure)] en cas d'erreur
Future<Either<Failure, StatistiquesEvaluation>> calculerMoyenneProposition(String propositionId);
}

View File

@@ -0,0 +1,354 @@
import 'package:dartz/dartz.dart';
import '../../../../core/error/failures.dart';
import '../../../../core/usecases/usecase.dart';
import '../entities/demande_aide.dart';
import '../repositories/solidarite_repository.dart';
/// Cas d'usage pour créer une nouvelle demande d'aide
class CreerDemandeAideUseCase implements UseCase<DemandeAide, CreerDemandeAideParams> {
final SolidariteRepository repository;
CreerDemandeAideUseCase(this.repository);
@override
Future<Either<Failure, DemandeAide>> call(CreerDemandeAideParams params) async {
return await repository.creerDemandeAide(params.demande);
}
}
class CreerDemandeAideParams {
final DemandeAide demande;
CreerDemandeAideParams({required this.demande});
}
/// Cas d'usage pour mettre à jour une demande d'aide
class MettreAJourDemandeAideUseCase implements UseCase<DemandeAide, MettreAJourDemandeAideParams> {
final SolidariteRepository repository;
MettreAJourDemandeAideUseCase(this.repository);
@override
Future<Either<Failure, DemandeAide>> call(MettreAJourDemandeAideParams params) async {
return await repository.mettreAJourDemandeAide(params.demande);
}
}
class MettreAJourDemandeAideParams {
final DemandeAide demande;
MettreAJourDemandeAideParams({required this.demande});
}
/// Cas d'usage pour obtenir une demande d'aide par ID
class ObtenirDemandeAideUseCase implements UseCase<DemandeAide, ObtenirDemandeAideParams> {
final SolidariteRepository repository;
ObtenirDemandeAideUseCase(this.repository);
@override
Future<Either<Failure, DemandeAide>> call(ObtenirDemandeAideParams params) async {
return await repository.obtenirDemandeAide(params.id);
}
}
class ObtenirDemandeAideParams {
final String id;
ObtenirDemandeAideParams({required this.id});
}
/// Cas d'usage pour soumettre une demande d'aide
class SoumettreDemandeAideUseCase implements UseCase<DemandeAide, SoumettreDemandeAideParams> {
final SolidariteRepository repository;
SoumettreDemandeAideUseCase(this.repository);
@override
Future<Either<Failure, DemandeAide>> call(SoumettreDemandeAideParams params) async {
return await repository.soumettreDemande(params.demandeId);
}
}
class SoumettreDemandeAideParams {
final String demandeId;
SoumettreDemandeAideParams({required this.demandeId});
}
/// Cas d'usage pour évaluer une demande d'aide
class EvaluerDemandeAideUseCase implements UseCase<DemandeAide, EvaluerDemandeAideParams> {
final SolidariteRepository repository;
EvaluerDemandeAideUseCase(this.repository);
@override
Future<Either<Failure, DemandeAide>> call(EvaluerDemandeAideParams params) async {
return await repository.evaluerDemande(
demandeId: params.demandeId,
evaluateurId: params.evaluateurId,
decision: params.decision,
commentaire: params.commentaire,
montantApprouve: params.montantApprouve,
);
}
}
class EvaluerDemandeAideParams {
final String demandeId;
final String evaluateurId;
final StatutAide decision;
final String? commentaire;
final double? montantApprouve;
EvaluerDemandeAideParams({
required this.demandeId,
required this.evaluateurId,
required this.decision,
this.commentaire,
this.montantApprouve,
});
}
/// Cas d'usage pour rechercher des demandes d'aide
class RechercherDemandesAideUseCase implements UseCase<List<DemandeAide>, RechercherDemandesAideParams> {
final SolidariteRepository repository;
RechercherDemandesAideUseCase(this.repository);
@override
Future<Either<Failure, List<DemandeAide>>> call(RechercherDemandesAideParams params) async {
return await repository.rechercherDemandes(
organisationId: params.organisationId,
typeAide: params.typeAide,
statut: params.statut,
demandeurId: params.demandeurId,
urgente: params.urgente,
page: params.page,
taille: params.taille,
);
}
}
class RechercherDemandesAideParams {
final String? organisationId;
final TypeAide? typeAide;
final StatutAide? statut;
final String? demandeurId;
final bool? urgente;
final int page;
final int taille;
RechercherDemandesAideParams({
this.organisationId,
this.typeAide,
this.statut,
this.demandeurId,
this.urgente,
this.page = 0,
this.taille = 20,
});
}
/// Cas d'usage pour obtenir les demandes urgentes
class ObtenirDemandesUrgentesUseCase implements UseCase<List<DemandeAide>, ObtenirDemandesUrgentesParams> {
final SolidariteRepository repository;
ObtenirDemandesUrgentesUseCase(this.repository);
@override
Future<Either<Failure, List<DemandeAide>>> call(ObtenirDemandesUrgentesParams params) async {
return await repository.obtenirDemandesUrgentes(params.organisationId);
}
}
class ObtenirDemandesUrgentesParams {
final String organisationId;
ObtenirDemandesUrgentesParams({required this.organisationId});
}
/// Cas d'usage pour obtenir les demandes de l'utilisateur connecté
class ObtenirMesDemandesUseCase implements UseCase<List<DemandeAide>, ObtenirMesDemandesParams> {
final SolidariteRepository repository;
ObtenirMesDemandesUseCase(this.repository);
@override
Future<Either<Failure, List<DemandeAide>>> call(ObtenirMesDemandesParams params) async {
return await repository.obtenirMesdemandes(params.utilisateurId);
}
}
class ObtenirMesDemandesParams {
final String utilisateurId;
ObtenirMesDemandesParams({required this.utilisateurId});
}
/// Cas d'usage pour valider une demande d'aide avant soumission
class ValiderDemandeAideUseCase implements UseCase<bool, ValiderDemandeAideParams> {
ValiderDemandeAideUseCase();
@override
Future<Either<Failure, bool>> call(ValiderDemandeAideParams params) async {
try {
final demande = params.demande;
final erreurs = <String>[];
// Validation du titre
if (demande.titre.trim().isEmpty) {
erreurs.add('Le titre est obligatoire');
} else if (demande.titre.length < 10) {
erreurs.add('Le titre doit contenir au moins 10 caractères');
} else if (demande.titre.length > 100) {
erreurs.add('Le titre ne peut pas dépasser 100 caractères');
}
// Validation de la description
if (demande.description.trim().isEmpty) {
erreurs.add('La description est obligatoire');
} else if (demande.description.length < 50) {
erreurs.add('La description doit contenir au moins 50 caractères');
} else if (demande.description.length > 1000) {
erreurs.add('La description ne peut pas dépasser 1000 caractères');
}
// Validation du montant pour les aides financières
if (_necessiteMontant(demande.typeAide)) {
if (demande.montantDemande == null) {
erreurs.add('Le montant est obligatoire pour ce type d\'aide');
} else if (demande.montantDemande! <= 0) {
erreurs.add('Le montant doit être supérieur à zéro');
} else if (!_isMontantValide(demande.typeAide, demande.montantDemande!)) {
erreurs.add('Le montant demandé n\'est pas dans la fourchette autorisée');
}
}
// Validation des bénéficiaires
if (demande.beneficiaires.isEmpty) {
erreurs.add('Au moins un bénéficiaire doit être spécifié');
} else {
for (int i = 0; i < demande.beneficiaires.length; i++) {
final beneficiaire = demande.beneficiaires[i];
if (beneficiaire.nom.trim().isEmpty) {
erreurs.add('Le nom du bénéficiaire ${i + 1} est obligatoire');
}
if (beneficiaire.prenom.trim().isEmpty) {
erreurs.add('Le prénom du bénéficiaire ${i + 1} est obligatoire');
}
if (beneficiaire.age < 0 || beneficiaire.age > 120) {
erreurs.add('L\'âge du bénéficiaire ${i + 1} n\'est pas valide');
}
}
}
// Validation de la justification d'urgence si priorité critique ou urgente
if (demande.priorite == PrioriteAide.critique || demande.priorite == PrioriteAide.urgente) {
if (demande.justificationUrgence == null || demande.justificationUrgence!.trim().isEmpty) {
erreurs.add('Une justification d\'urgence est requise pour cette priorité');
} else if (demande.justificationUrgence!.length < 20) {
erreurs.add('La justification d\'urgence doit contenir au moins 20 caractères');
}
}
// Validation du contact d'urgence si priorité critique
if (demande.priorite == PrioriteAide.critique) {
if (demande.contactUrgence == null) {
erreurs.add('Un contact d\'urgence est obligatoire pour les demandes critiques');
} else {
final contact = demande.contactUrgence!;
if (contact.nom.trim().isEmpty) {
erreurs.add('Le nom du contact d\'urgence est obligatoire');
}
if (contact.telephone.trim().isEmpty) {
erreurs.add('Le téléphone du contact d\'urgence est obligatoire');
} else if (!_isValidPhoneNumber(contact.telephone)) {
erreurs.add('Le numéro de téléphone du contact d\'urgence n\'est pas valide');
}
}
}
if (erreurs.isNotEmpty) {
return Left(ValidationFailure(erreurs.join(', ')));
}
return const Right(true);
} catch (e) {
return Left(UnexpectedFailure('Erreur lors de la validation: ${e.toString()}'));
}
}
bool _necessiteMontant(TypeAide typeAide) {
return [
TypeAide.aideFinanciereUrgente,
TypeAide.aideFinanciereMedicale,
TypeAide.aideFinanciereEducation,
].contains(typeAide);
}
bool _isMontantValide(TypeAide typeAide, double montant) {
switch (typeAide) {
case TypeAide.aideFinanciereUrgente:
return montant >= 5000 && montant <= 50000;
case TypeAide.aideFinanciereMedicale:
return montant >= 10000 && montant <= 100000;
case TypeAide.aideFinanciereEducation:
return montant >= 5000 && montant <= 200000;
default:
return true;
}
}
bool _isValidPhoneNumber(String phone) {
// Validation simple pour les numéros de téléphone ivoiriens
final phoneRegex = RegExp(r'^(\+225)?[0-9]{8,10}$');
return phoneRegex.hasMatch(phone.replaceAll(RegExp(r'[\s\-\(\)]'), ''));
}
}
class ValiderDemandeAideParams {
final DemandeAide demande;
ValiderDemandeAideParams({required this.demande});
}
/// Cas d'usage pour calculer la priorité automatique d'une demande
class CalculerPrioriteDemandeUseCase implements UseCase<PrioriteAide, CalculerPrioriteDemandeParams> {
CalculerPrioriteDemandeUseCase();
@override
Future<Either<Failure, PrioriteAide>> call(CalculerPrioriteDemandeParams params) async {
try {
final demande = params.demande;
// Priorité critique si justification d'urgence et contact d'urgence
if (demande.justificationUrgence != null &&
demande.justificationUrgence!.isNotEmpty &&
demande.contactUrgence != null) {
return const Right(PrioriteAide.critique);
}
// Priorité urgente pour certains types d'aide
if ([TypeAide.aideFinanciereUrgente, TypeAide.aideFinanciereMedicale].contains(demande.typeAide)) {
return const Right(PrioriteAide.urgente);
}
// Priorité élevée pour les montants importants
if (demande.montantDemande != null && demande.montantDemande! > 50000) {
return const Right(PrioriteAide.elevee);
}
// Priorité normale par défaut
return const Right(PrioriteAide.normale);
} catch (e) {
return Left(UnexpectedFailure('Erreur lors du calcul de priorité: ${e.toString()}'));
}
}
}
class CalculerPrioriteDemandeParams {
final DemandeAide demande;
CalculerPrioriteDemandeParams({required this.demande});
}

View File

@@ -0,0 +1,463 @@
import 'package:dartz/dartz.dart';
import '../../../../core/error/failures.dart';
import '../../../../core/usecases/usecase.dart';
import '../entities/evaluation_aide.dart';
import '../repositories/solidarite_repository.dart';
/// Cas d'usage pour créer une nouvelle évaluation
class CreerEvaluationUseCase implements UseCase<EvaluationAide, CreerEvaluationParams> {
final SolidariteRepository repository;
CreerEvaluationUseCase(this.repository);
@override
Future<Either<Failure, EvaluationAide>> call(CreerEvaluationParams params) async {
return await repository.creerEvaluation(params.evaluation);
}
}
class CreerEvaluationParams {
final EvaluationAide evaluation;
CreerEvaluationParams({required this.evaluation});
}
/// Cas d'usage pour mettre à jour une évaluation
class MettreAJourEvaluationUseCase implements UseCase<EvaluationAide, MettreAJourEvaluationParams> {
final SolidariteRepository repository;
MettreAJourEvaluationUseCase(this.repository);
@override
Future<Either<Failure, EvaluationAide>> call(MettreAJourEvaluationParams params) async {
return await repository.mettreAJourEvaluation(params.evaluation);
}
}
class MettreAJourEvaluationParams {
final EvaluationAide evaluation;
MettreAJourEvaluationParams({required this.evaluation});
}
/// Cas d'usage pour obtenir une évaluation par ID
class ObtenirEvaluationUseCase implements UseCase<EvaluationAide, ObtenirEvaluationParams> {
final SolidariteRepository repository;
ObtenirEvaluationUseCase(this.repository);
@override
Future<Either<Failure, EvaluationAide>> call(ObtenirEvaluationParams params) async {
return await repository.obtenirEvaluation(params.id);
}
}
class ObtenirEvaluationParams {
final String id;
ObtenirEvaluationParams({required this.id});
}
/// Cas d'usage pour obtenir les évaluations d'une demande
class ObtenirEvaluationsDemandeUseCase implements UseCase<List<EvaluationAide>, ObtenirEvaluationsDemandeParams> {
final SolidariteRepository repository;
ObtenirEvaluationsDemandeUseCase(this.repository);
@override
Future<Either<Failure, List<EvaluationAide>>> call(ObtenirEvaluationsDemandeParams params) async {
return await repository.obtenirEvaluationsDemande(params.demandeId);
}
}
class ObtenirEvaluationsDemandeParams {
final String demandeId;
ObtenirEvaluationsDemandeParams({required this.demandeId});
}
/// Cas d'usage pour obtenir les évaluations d'une proposition
class ObtenirEvaluationsPropositionUseCase implements UseCase<List<EvaluationAide>, ObtenirEvaluationsPropositionParams> {
final SolidariteRepository repository;
ObtenirEvaluationsPropositionUseCase(this.repository);
@override
Future<Either<Failure, List<EvaluationAide>>> call(ObtenirEvaluationsPropositionParams params) async {
return await repository.obtenirEvaluationsProposition(params.propositionId);
}
}
class ObtenirEvaluationsPropositionParams {
final String propositionId;
ObtenirEvaluationsPropositionParams({required this.propositionId});
}
/// Cas d'usage pour signaler une évaluation
class SignalerEvaluationUseCase implements UseCase<EvaluationAide, SignalerEvaluationParams> {
final SolidariteRepository repository;
SignalerEvaluationUseCase(this.repository);
@override
Future<Either<Failure, EvaluationAide>> call(SignalerEvaluationParams params) async {
return await repository.signalerEvaluation(
evaluationId: params.evaluationId,
motif: params.motif,
);
}
}
class SignalerEvaluationParams {
final String evaluationId;
final String motif;
SignalerEvaluationParams({
required this.evaluationId,
required this.motif,
});
}
/// Cas d'usage pour calculer la note moyenne d'une demande
class CalculerMoyenneDemandeUseCase implements UseCase<StatistiquesEvaluation, CalculerMoyenneDemandeParams> {
final SolidariteRepository repository;
CalculerMoyenneDemandeUseCase(this.repository);
@override
Future<Either<Failure, StatistiquesEvaluation>> call(CalculerMoyenneDemandeParams params) async {
return await repository.calculerMoyenneDemande(params.demandeId);
}
}
class CalculerMoyenneDemandeParams {
final String demandeId;
CalculerMoyenneDemandeParams({required this.demandeId});
}
/// Cas d'usage pour calculer la note moyenne d'une proposition
class CalculerMoyennePropositionUseCase implements UseCase<StatistiquesEvaluation, CalculerMoyennePropositionParams> {
final SolidariteRepository repository;
CalculerMoyennePropositionUseCase(this.repository);
@override
Future<Either<Failure, StatistiquesEvaluation>> call(CalculerMoyennePropositionParams params) async {
return await repository.calculerMoyenneProposition(params.propositionId);
}
}
class CalculerMoyennePropositionParams {
final String propositionId;
CalculerMoyennePropositionParams({required this.propositionId});
}
/// Cas d'usage pour valider une évaluation avant création
class ValiderEvaluationUseCase implements UseCase<bool, ValiderEvaluationParams> {
ValiderEvaluationUseCase();
@override
Future<Either<Failure, bool>> call(ValiderEvaluationParams params) async {
try {
final evaluation = params.evaluation;
final erreurs = <String>[];
// Validation de la note globale
if (evaluation.noteGlobale < 1.0 || evaluation.noteGlobale > 5.0) {
erreurs.add('La note globale doit être comprise entre 1 et 5');
}
// Validation des notes détaillées
final notesDetaillees = [
evaluation.noteDelaiReponse,
evaluation.noteCommunication,
evaluation.noteProfessionnalisme,
evaluation.noteRespectEngagements,
];
for (final note in notesDetaillees) {
if (note != null && (note < 1.0 || note > 5.0)) {
erreurs.add('Toutes les notes détaillées doivent être comprises entre 1 et 5');
break;
}
}
// Validation du commentaire principal
if (evaluation.commentairePrincipal.trim().isEmpty) {
erreurs.add('Le commentaire principal est obligatoire');
} else if (evaluation.commentairePrincipal.length < 20) {
erreurs.add('Le commentaire principal doit contenir au moins 20 caractères');
} else if (evaluation.commentairePrincipal.length > 1000) {
erreurs.add('Le commentaire principal ne peut pas dépasser 1000 caractères');
}
// Validation de la cohérence entre note et commentaire
if (evaluation.noteGlobale <= 2.0 && evaluation.commentairePrincipal.length < 50) {
erreurs.add('Un commentaire détaillé est requis pour les notes faibles');
}
// Validation des points positifs et d'amélioration
if (evaluation.pointsPositifs != null && evaluation.pointsPositifs!.length > 500) {
erreurs.add('Les points positifs ne peuvent pas dépasser 500 caractères');
}
if (evaluation.pointsAmelioration != null && evaluation.pointsAmelioration!.length > 500) {
erreurs.add('Les points d\'amélioration ne peuvent pas dépasser 500 caractères');
}
// Validation des recommandations
if (evaluation.recommandations != null && evaluation.recommandations!.length > 500) {
erreurs.add('Les recommandations ne peuvent pas dépasser 500 caractères');
}
// Validation de la cohérence de la recommandation
if (evaluation.recommande == true && evaluation.noteGlobale < 3.0) {
erreurs.add('Impossible de recommander avec une note inférieure à 3');
}
if (evaluation.recommande == false && evaluation.noteGlobale >= 4.0) {
erreurs.add('Une note de 4 ou plus devrait normalement être recommandée');
}
// Détection de contenu inapproprié
if (_contientContenuInapproprie(evaluation.commentairePrincipal)) {
erreurs.add('Le commentaire contient du contenu inapproprié');
}
if (erreurs.isNotEmpty) {
return Left(ValidationFailure(erreurs.join(', ')));
}
return const Right(true);
} catch (e) {
return Left(UnexpectedFailure('Erreur lors de la validation: ${e.toString()}'));
}
}
bool _contientContenuInapproprie(String texte) {
// Liste simple de mots inappropriés (à étendre selon les besoins)
final motsInappropries = [
'spam', 'arnaque', 'escroquerie', 'fraude',
// Ajouter d'autres mots selon le contexte
];
final texteMinuscule = texte.toLowerCase();
return motsInappropries.any((mot) => texteMinuscule.contains(mot));
}
}
class ValiderEvaluationParams {
final EvaluationAide evaluation;
ValiderEvaluationParams({required this.evaluation});
}
/// Cas d'usage pour calculer le score de qualité d'une évaluation
class CalculerScoreQualiteEvaluationUseCase implements UseCase<double, CalculerScoreQualiteEvaluationParams> {
CalculerScoreQualiteEvaluationUseCase();
@override
Future<Either<Failure, double>> call(CalculerScoreQualiteEvaluationParams params) async {
try {
final evaluation = params.evaluation;
double score = 50.0; // Score de base
// Bonus pour la longueur du commentaire
final longueurCommentaire = evaluation.commentairePrincipal.length;
if (longueurCommentaire >= 100) {
score += 15.0;
} else if (longueurCommentaire >= 50) {
score += 10.0;
} else if (longueurCommentaire >= 20) {
score += 5.0;
}
// Bonus pour les notes détaillées
final notesDetaillees = [
evaluation.noteDelaiReponse,
evaluation.noteCommunication,
evaluation.noteProfessionnalisme,
evaluation.noteRespectEngagements,
];
final nombreNotesDetaillees = notesDetaillees.where((note) => note != null).length;
score += nombreNotesDetaillees * 5.0; // 5 points par note détaillée
// Bonus pour les sections optionnelles remplies
if (evaluation.pointsPositifs != null && evaluation.pointsPositifs!.isNotEmpty) {
score += 5.0;
}
if (evaluation.pointsAmelioration != null && evaluation.pointsAmelioration!.isNotEmpty) {
score += 5.0;
}
if (evaluation.recommandations != null && evaluation.recommandations!.isNotEmpty) {
score += 5.0;
}
// Bonus pour la cohérence
if (_estCoherente(evaluation)) {
score += 10.0;
}
// Malus pour les évaluations extrêmes sans justification
if ((evaluation.noteGlobale <= 1.5 || evaluation.noteGlobale >= 4.5) &&
longueurCommentaire < 50) {
score -= 15.0;
}
// Malus pour les signalements
score -= evaluation.nombreSignalements * 10.0;
return Right(score.clamp(0.0, 100.0));
} catch (e) {
return Left(UnexpectedFailure('Erreur lors du calcul du score de qualité: ${e.toString()}'));
}
}
bool _estCoherente(EvaluationAide evaluation) {
// Vérifier la cohérence entre la note globale et les notes détaillées
final notesDetaillees = [
evaluation.noteDelaiReponse,
evaluation.noteCommunication,
evaluation.noteProfessionnalisme,
evaluation.noteRespectEngagements,
].where((note) => note != null).cast<double>().toList();
if (notesDetaillees.isEmpty) return true;
final moyenneDetaillees = notesDetaillees.reduce((a, b) => a + b) / notesDetaillees.length;
final ecart = (evaluation.noteGlobale - moyenneDetaillees).abs();
// Cohérent si l'écart est inférieur à 1 point
return ecart < 1.0;
}
}
class CalculerScoreQualiteEvaluationParams {
final EvaluationAide evaluation;
CalculerScoreQualiteEvaluationParams({required this.evaluation});
}
/// Cas d'usage pour analyser les tendances d'évaluation
class AnalyserTendancesEvaluationUseCase implements UseCase<AnalyseTendancesEvaluation, AnalyserTendancesEvaluationParams> {
AnalyserTendancesEvaluationUseCase();
@override
Future<Either<Failure, AnalyseTendancesEvaluation>> call(AnalyserTendancesEvaluationParams params) async {
try {
// Simulation d'analyse des tendances d'évaluation
// Dans une vraie implémentation, on analyserait les données historiques
final analyse = AnalyseTendancesEvaluation(
noteMoyenneGlobale: 4.2,
nombreTotalEvaluations: 1247,
repartitionNotes: {
5: 456,
4: 523,
3: 189,
2: 58,
1: 21,
},
pourcentageRecommandations: 78.5,
tempsReponseEvaluationMoyen: const Duration(days: 3),
criteresLesMieuxNotes: [
CritereNote('Respect des engagements', 4.6),
CritereNote('Communication', 4.3),
CritereNote('Professionnalisme', 4.1),
CritereNote('Délai de réponse', 3.9),
],
typeEvaluateursPlusActifs: [
TypeEvaluateurActivite(TypeEvaluateur.beneficiaire, 67.2),
TypeEvaluateurActivite(TypeEvaluateur.proposant, 23.8),
TypeEvaluateurActivite(TypeEvaluateur.evaluateurOfficial, 6.5),
TypeEvaluateurActivite(TypeEvaluateur.administrateur, 2.5),
],
evolutionSatisfaction: EvolutionSatisfaction(
dernierMois: 4.2,
moisPrecedent: 4.0,
tendance: TendanceSatisfaction.hausse,
),
recommandationsAmelioration: [
'Améliorer les délais de réponse des proposants',
'Encourager plus d\'évaluations détaillées',
'Former les proposants à la communication',
],
);
return Right(analyse);
} catch (e) {
return Left(UnexpectedFailure('Erreur lors de l\'analyse des tendances: ${e.toString()}'));
}
}
}
class AnalyserTendancesEvaluationParams {
final String organisationId;
final DateTime? dateDebut;
final DateTime? dateFin;
AnalyserTendancesEvaluationParams({
required this.organisationId,
this.dateDebut,
this.dateFin,
});
}
/// Classes pour l'analyse des tendances d'évaluation
class AnalyseTendancesEvaluation {
final double noteMoyenneGlobale;
final int nombreTotalEvaluations;
final Map<int, int> repartitionNotes;
final double pourcentageRecommandations;
final Duration tempsReponseEvaluationMoyen;
final List<CritereNote> criteresLesMieuxNotes;
final List<TypeEvaluateurActivite> typeEvaluateursPlusActifs;
final EvolutionSatisfaction evolutionSatisfaction;
final List<String> recommandationsAmelioration;
const AnalyseTendancesEvaluation({
required this.noteMoyenneGlobale,
required this.nombreTotalEvaluations,
required this.repartitionNotes,
required this.pourcentageRecommandations,
required this.tempsReponseEvaluationMoyen,
required this.criteresLesMieuxNotes,
required this.typeEvaluateursPlusActifs,
required this.evolutionSatisfaction,
required this.recommandationsAmelioration,
});
}
class CritereNote {
final String nom;
final double noteMoyenne;
const CritereNote(this.nom, this.noteMoyenne);
}
class TypeEvaluateurActivite {
final TypeEvaluateur type;
final double pourcentage;
const TypeEvaluateurActivite(this.type, this.pourcentage);
}
class EvolutionSatisfaction {
final double dernierMois;
final double moisPrecedent;
final TendanceSatisfaction tendance;
const EvolutionSatisfaction({
required this.dernierMois,
required this.moisPrecedent,
required this.tendance,
});
}
enum TendanceSatisfaction { hausse, baisse, stable }

View File

@@ -0,0 +1,391 @@
import 'package:dartz/dartz.dart';
import '../../../../core/error/failures.dart';
import '../../../../core/usecases/usecase.dart';
import '../entities/demande_aide.dart';
import '../entities/proposition_aide.dart';
import '../repositories/solidarite_repository.dart';
/// Cas d'usage pour trouver les propositions compatibles avec une demande
class TrouverPropositionsCompatiblesUseCase implements UseCase<List<PropositionAide>, TrouverPropositionsCompatiblesParams> {
final SolidariteRepository repository;
TrouverPropositionsCompatiblesUseCase(this.repository);
@override
Future<Either<Failure, List<PropositionAide>>> call(TrouverPropositionsCompatiblesParams params) async {
return await repository.trouverPropositionsCompatibles(params.demandeId);
}
}
class TrouverPropositionsCompatiblesParams {
final String demandeId;
TrouverPropositionsCompatiblesParams({required this.demandeId});
}
/// Cas d'usage pour trouver les demandes compatibles avec une proposition
class TrouverDemandesCompatiblesUseCase implements UseCase<List<DemandeAide>, TrouverDemandesCompatiblesParams> {
final SolidariteRepository repository;
TrouverDemandesCompatiblesUseCase(this.repository);
@override
Future<Either<Failure, List<DemandeAide>>> call(TrouverDemandesCompatiblesParams params) async {
return await repository.trouverDemandesCompatibles(params.propositionId);
}
}
class TrouverDemandesCompatiblesParams {
final String propositionId;
TrouverDemandesCompatiblesParams({required this.propositionId});
}
/// Cas d'usage pour rechercher des proposants financiers
class RechercherProposantsFinanciersUseCase implements UseCase<List<PropositionAide>, RechercherProposantsFinanciersParams> {
final SolidariteRepository repository;
RechercherProposantsFinanciersUseCase(this.repository);
@override
Future<Either<Failure, List<PropositionAide>>> call(RechercherProposantsFinanciersParams params) async {
return await repository.rechercherProposantsFinanciers(params.demandeId);
}
}
class RechercherProposantsFinanciersParams {
final String demandeId;
RechercherProposantsFinanciersParams({required this.demandeId});
}
/// Cas d'usage pour calculer le score de compatibilité entre une demande et une proposition
class CalculerScoreCompatibiliteUseCase implements UseCase<double, CalculerScoreCompatibiliteParams> {
CalculerScoreCompatibiliteUseCase();
@override
Future<Either<Failure, double>> call(CalculerScoreCompatibiliteParams params) async {
try {
final demande = params.demande;
final proposition = params.proposition;
double score = 0.0;
// 1. Correspondance du type d'aide (40 points max)
if (demande.typeAide == proposition.typeAide) {
score += 40.0;
} else if (_sontTypesCompatibles(demande.typeAide, proposition.typeAide)) {
score += 25.0;
} else if (proposition.typeAide == TypeAide.autre) {
score += 15.0;
}
// 2. Compatibilité financière (25 points max)
if (_necessiteMontant(demande.typeAide) && proposition.montantMaximum != null) {
final montantDemande = demande.montantDemande;
if (montantDemande != null) {
if (montantDemande <= proposition.montantMaximum!) {
score += 25.0;
} else {
// Pénalité proportionnelle au dépassement
double ratio = proposition.montantMaximum! / montantDemande;
score += 25.0 * ratio;
}
}
} else if (!_necessiteMontant(demande.typeAide)) {
score += 25.0; // Pas de contrainte financière
}
// 3. Expérience du proposant (15 points max)
if (proposition.nombreBeneficiairesAides > 0) {
score += (proposition.nombreBeneficiairesAides * 2.0).clamp(0.0, 15.0);
}
// 4. Réputation (10 points max)
if (proposition.noteMoyenne != null && proposition.nombreEvaluations >= 3) {
score += (proposition.noteMoyenne! - 3.0) * 3.33;
}
// 5. Disponibilité et capacité (10 points max)
if (proposition.peutAccepterBeneficiaires) {
double ratioCapacite = proposition.placesRestantes / proposition.nombreMaxBeneficiaires;
score += 10.0 * ratioCapacite;
}
// Bonus et malus additionnels
score += _calculerBonusGeographique(demande, proposition);
score += _calculerBonusTemporel(demande, proposition);
score -= _calculerMalusDelai(demande, proposition);
return Right(score.clamp(0.0, 100.0));
} catch (e) {
return Left(UnexpectedFailure('Erreur lors du calcul de compatibilité: ${e.toString()}'));
}
}
bool _sontTypesCompatibles(TypeAide typeAide1, TypeAide typeAide2) {
// Définir les groupes de types compatibles
final groupesCompatibles = [
[TypeAide.aideFinanciereUrgente, TypeAide.aideFinanciereMedicale, TypeAide.aideFinanciereEducation],
[TypeAide.aideMaterielleVetements, TypeAide.aideMaterielleNourriture],
[TypeAide.aideProfessionnelleFormation, TypeAide.aideSocialeAccompagnement],
];
for (final groupe in groupesCompatibles) {
if (groupe.contains(typeAide1) && groupe.contains(typeAide2)) {
return true;
}
}
return false;
}
bool _necessiteMontant(TypeAide typeAide) {
return [
TypeAide.aideFinanciereUrgente,
TypeAide.aideFinanciereMedicale,
TypeAide.aideFinanciereEducation,
].contains(typeAide);
}
double _calculerBonusGeographique(DemandeAide demande, PropositionAide proposition) {
// Simulation - dans une vraie implémentation, on utiliserait les données de localisation
if (demande.localisation != null && proposition.zonesGeographiques.isNotEmpty) {
// Logique de proximité géographique
return 5.0;
}
return 0.0;
}
double _calculerBonusTemporel(DemandeAide demande, PropositionAide proposition) {
double bonus = 0.0;
// Bonus pour demande urgente
if (demande.estUrgente) {
bonus += 5.0;
}
// Bonus pour proposition récente
final joursDepuisCreation = DateTime.now().difference(proposition.dateCreation).inDays;
if (joursDepuisCreation <= 30) {
bonus += 3.0;
}
return bonus;
}
double _calculerMalusDelai(DemandeAide demande, PropositionAide proposition) {
double malus = 0.0;
// Malus si la demande est en retard
if (demande.delaiDepasse) {
malus += 5.0;
}
// Malus si la proposition a un délai de réponse long
if (proposition.delaiReponseHeures > 168) { // Plus d'une semaine
malus += 3.0;
}
return malus;
}
}
class CalculerScoreCompatibiliteParams {
final DemandeAide demande;
final PropositionAide proposition;
CalculerScoreCompatibiliteParams({
required this.demande,
required this.proposition,
});
}
/// Cas d'usage pour effectuer un matching intelligent
class EffectuerMatchingIntelligentUseCase implements UseCase<List<ResultatMatching>, EffectuerMatchingIntelligentParams> {
final TrouverPropositionsCompatiblesUseCase trouverPropositionsCompatibles;
final CalculerScoreCompatibiliteUseCase calculerScoreCompatibilite;
EffectuerMatchingIntelligentUseCase({
required this.trouverPropositionsCompatibles,
required this.calculerScoreCompatibilite,
});
@override
Future<Either<Failure, List<ResultatMatching>>> call(EffectuerMatchingIntelligentParams params) async {
try {
// 1. Trouver les propositions compatibles
final propositionsResult = await trouverPropositionsCompatibles(
TrouverPropositionsCompatiblesParams(demandeId: params.demande.id)
);
return propositionsResult.fold(
(failure) => Left(failure),
(propositions) async {
// 2. Calculer les scores de compatibilité
final resultats = <ResultatMatching>[];
for (final proposition in propositions) {
final scoreResult = await calculerScoreCompatibilite(
CalculerScoreCompatibiliteParams(
demande: params.demande,
proposition: proposition,
)
);
scoreResult.fold(
(failure) {
// Ignorer les erreurs de calcul de score individuel
},
(score) {
if (score >= params.scoreMinimum) {
resultats.add(ResultatMatching(
proposition: proposition,
score: score,
raisonCompatibilite: _genererRaisonCompatibilite(params.demande, proposition, score),
));
}
},
);
}
// 3. Trier par score décroissant
resultats.sort((a, b) => b.score.compareTo(a.score));
// 4. Limiter le nombre de résultats
final resultatsLimites = resultats.take(params.limiteResultats).toList();
return Right(resultatsLimites);
},
);
} catch (e) {
return Left(UnexpectedFailure('Erreur lors du matching intelligent: ${e.toString()}'));
}
}
String _genererRaisonCompatibilite(DemandeAide demande, PropositionAide proposition, double score) {
final raisons = <String>[];
// Type d'aide
if (demande.typeAide == proposition.typeAide) {
raisons.add('Type d\'aide identique');
}
// Compatibilité financière
if (demande.montantDemande != null && proposition.montantMaximum != null) {
if (demande.montantDemande! <= proposition.montantMaximum!) {
raisons.add('Montant compatible');
}
}
// Expérience
if (proposition.nombreBeneficiairesAides > 5) {
raisons.add('Proposant expérimenté');
}
// Réputation
if (proposition.noteMoyenne != null && proposition.noteMoyenne! >= 4.0) {
raisons.add('Excellente réputation');
}
// Disponibilité
if (proposition.peutAccepterBeneficiaires) {
raisons.add('Places disponibles');
}
return raisons.isEmpty ? 'Compatible' : raisons.join(', ');
}
}
class EffectuerMatchingIntelligentParams {
final DemandeAide demande;
final double scoreMinimum;
final int limiteResultats;
EffectuerMatchingIntelligentParams({
required this.demande,
this.scoreMinimum = 30.0,
this.limiteResultats = 10,
});
}
/// Classe représentant un résultat de matching
class ResultatMatching {
final PropositionAide proposition;
final double score;
final String raisonCompatibilite;
const ResultatMatching({
required this.proposition,
required this.score,
required this.raisonCompatibilite,
});
}
/// Cas d'usage pour analyser les tendances de matching
class AnalyserTendancesMatchingUseCase implements UseCase<AnalyseTendances, AnalyserTendancesMatchingParams> {
AnalyserTendancesMatchingUseCase();
@override
Future<Either<Failure, AnalyseTendances>> call(AnalyserTendancesMatchingParams params) async {
try {
// Simulation d'analyse des tendances
// Dans une vraie implémentation, on analyserait les données historiques
final analyse = AnalyseTendances(
tauxMatchingMoyen: 78.5,
tempsMatchingMoyen: const Duration(hours: 6),
typesAidePlusDemandesMap: {
TypeAide.aideFinanciereUrgente: 45,
TypeAide.aideFinanciereMedicale: 32,
TypeAide.aideMaterielleNourriture: 28,
},
typesAidePlusProposesMap: {
TypeAide.aideFinanciereEducation: 38,
TypeAide.aideProfessionnelleFormation: 25,
TypeAide.aideSocialeAccompagnement: 22,
},
heuresOptimalesMatching: ['09:00', '14:00', '18:00'],
recommandations: [
'Augmenter les propositions d\'aide financière urgente',
'Promouvoir les aides matérielles auprès des proposants',
'Optimiser les notifications entre 9h et 18h',
],
);
return Right(analyse);
} catch (e) {
return Left(UnexpectedFailure('Erreur lors de l\'analyse des tendances: ${e.toString()}'));
}
}
}
class AnalyserTendancesMatchingParams {
final String organisationId;
final DateTime? dateDebut;
final DateTime? dateFin;
AnalyserTendancesMatchingParams({
required this.organisationId,
this.dateDebut,
this.dateFin,
});
}
/// Classe représentant une analyse des tendances de matching
class AnalyseTendances {
final double tauxMatchingMoyen;
final Duration tempsMatchingMoyen;
final Map<TypeAide, int> typesAidePlusDemandesMap;
final Map<TypeAide, int> typesAidePlusProposesMap;
final List<String> heuresOptimalesMatching;
final List<String> recommandations;
const AnalyseTendances({
required this.tauxMatchingMoyen,
required this.tempsMatchingMoyen,
required this.typesAidePlusDemandesMap,
required this.typesAidePlusProposesMap,
required this.heuresOptimalesMatching,
required this.recommandations,
});
}

View File

@@ -0,0 +1,394 @@
import 'package:dartz/dartz.dart';
import '../../../../core/error/failures.dart';
import '../../../../core/usecases/usecase.dart';
import '../entities/proposition_aide.dart';
import '../entities/demande_aide.dart';
import '../repositories/solidarite_repository.dart';
/// Cas d'usage pour créer une nouvelle proposition d'aide
class CreerPropositionAideUseCase implements UseCase<PropositionAide, CreerPropositionAideParams> {
final SolidariteRepository repository;
CreerPropositionAideUseCase(this.repository);
@override
Future<Either<Failure, PropositionAide>> call(CreerPropositionAideParams params) async {
return await repository.creerPropositionAide(params.proposition);
}
}
class CreerPropositionAideParams {
final PropositionAide proposition;
CreerPropositionAideParams({required this.proposition});
}
/// Cas d'usage pour mettre à jour une proposition d'aide
class MettreAJourPropositionAideUseCase implements UseCase<PropositionAide, MettreAJourPropositionAideParams> {
final SolidariteRepository repository;
MettreAJourPropositionAideUseCase(this.repository);
@override
Future<Either<Failure, PropositionAide>> call(MettreAJourPropositionAideParams params) async {
return await repository.mettreAJourPropositionAide(params.proposition);
}
}
class MettreAJourPropositionAideParams {
final PropositionAide proposition;
MettreAJourPropositionAideParams({required this.proposition});
}
/// Cas d'usage pour obtenir une proposition d'aide par ID
class ObtenirPropositionAideUseCase implements UseCase<PropositionAide, ObtenirPropositionAideParams> {
final SolidariteRepository repository;
ObtenirPropositionAideUseCase(this.repository);
@override
Future<Either<Failure, PropositionAide>> call(ObtenirPropositionAideParams params) async {
return await repository.obtenirPropositionAide(params.id);
}
}
class ObtenirPropositionAideParams {
final String id;
ObtenirPropositionAideParams({required this.id});
}
/// Cas d'usage pour changer le statut d'une proposition d'aide
class ChangerStatutPropositionUseCase implements UseCase<PropositionAide, ChangerStatutPropositionParams> {
final SolidariteRepository repository;
ChangerStatutPropositionUseCase(this.repository);
@override
Future<Either<Failure, PropositionAide>> call(ChangerStatutPropositionParams params) async {
return await repository.changerStatutProposition(
propositionId: params.propositionId,
activer: params.activer,
);
}
}
class ChangerStatutPropositionParams {
final String propositionId;
final bool activer;
ChangerStatutPropositionParams({
required this.propositionId,
required this.activer,
});
}
/// Cas d'usage pour rechercher des propositions d'aide
class RechercherPropositionsAideUseCase implements UseCase<List<PropositionAide>, RechercherPropositionsAideParams> {
final SolidariteRepository repository;
RechercherPropositionsAideUseCase(this.repository);
@override
Future<Either<Failure, List<PropositionAide>>> call(RechercherPropositionsAideParams params) async {
return await repository.rechercherPropositions(
organisationId: params.organisationId,
typeAide: params.typeAide,
proposantId: params.proposantId,
actives: params.actives,
page: params.page,
taille: params.taille,
);
}
}
class RechercherPropositionsAideParams {
final String? organisationId;
final TypeAide? typeAide;
final String? proposantId;
final bool? actives;
final int page;
final int taille;
RechercherPropositionsAideParams({
this.organisationId,
this.typeAide,
this.proposantId,
this.actives,
this.page = 0,
this.taille = 20,
});
}
/// Cas d'usage pour obtenir les propositions actives pour un type d'aide
class ObtenirPropositionsActivesUseCase implements UseCase<List<PropositionAide>, ObtenirPropositionsActivesParams> {
final SolidariteRepository repository;
ObtenirPropositionsActivesUseCase(this.repository);
@override
Future<Either<Failure, List<PropositionAide>>> call(ObtenirPropositionsActivesParams params) async {
return await repository.obtenirPropositionsActives(params.typeAide);
}
}
class ObtenirPropositionsActivesParams {
final TypeAide typeAide;
ObtenirPropositionsActivesParams({required this.typeAide});
}
/// Cas d'usage pour obtenir les meilleures propositions
class ObtenirMeilleuresPropositionsUseCase implements UseCase<List<PropositionAide>, ObtenirMeilleuresPropositionsParams> {
final SolidariteRepository repository;
ObtenirMeilleuresPropositionsUseCase(this.repository);
@override
Future<Either<Failure, List<PropositionAide>>> call(ObtenirMeilleuresPropositionsParams params) async {
return await repository.obtenirMeilleuresPropositions(params.limite);
}
}
class ObtenirMeilleuresPropositionsParams {
final int limite;
ObtenirMeilleuresPropositionsParams({this.limite = 10});
}
/// Cas d'usage pour obtenir les propositions de l'utilisateur connecté
class ObtenirMesPropositionsUseCase implements UseCase<List<PropositionAide>, ObtenirMesPropositionsParams> {
final SolidariteRepository repository;
ObtenirMesPropositionsUseCase(this.repository);
@override
Future<Either<Failure, List<PropositionAide>>> call(ObtenirMesPropositionsParams params) async {
return await repository.obtenirMesPropositions(params.utilisateurId);
}
}
class ObtenirMesPropositionsParams {
final String utilisateurId;
ObtenirMesPropositionsParams({required this.utilisateurId});
}
/// Cas d'usage pour valider une proposition d'aide avant création
class ValiderPropositionAideUseCase implements UseCase<bool, ValiderPropositionAideParams> {
ValiderPropositionAideUseCase();
@override
Future<Either<Failure, bool>> call(ValiderPropositionAideParams params) async {
try {
final proposition = params.proposition;
final erreurs = <String>[];
// Validation du titre
if (proposition.titre.trim().isEmpty) {
erreurs.add('Le titre est obligatoire');
} else if (proposition.titre.length < 10) {
erreurs.add('Le titre doit contenir au moins 10 caractères');
} else if (proposition.titre.length > 100) {
erreurs.add('Le titre ne peut pas dépasser 100 caractères');
}
// Validation de la description
if (proposition.description.trim().isEmpty) {
erreurs.add('La description est obligatoire');
} else if (proposition.description.length < 50) {
erreurs.add('La description doit contenir au moins 50 caractères');
} else if (proposition.description.length > 1000) {
erreurs.add('La description ne peut pas dépasser 1000 caractères');
}
// Validation du nombre maximum de bénéficiaires
if (proposition.nombreMaxBeneficiaires <= 0) {
erreurs.add('Le nombre maximum de bénéficiaires doit être supérieur à zéro');
} else if (proposition.nombreMaxBeneficiaires > 100) {
erreurs.add('Le nombre maximum de bénéficiaires ne peut pas dépasser 100');
}
// Validation des montants pour les aides financières
if (_estAideFinanciere(proposition.typeAide)) {
if (proposition.montantMaximum == null) {
erreurs.add('Le montant maximum est obligatoire pour les aides financières');
} else if (proposition.montantMaximum! <= 0) {
erreurs.add('Le montant maximum doit être supérieur à zéro');
} else if (proposition.montantMaximum! > 1000000) {
erreurs.add('Le montant maximum ne peut pas dépasser 1 000 000 FCFA');
}
if (proposition.montantMinimum != null) {
if (proposition.montantMinimum! <= 0) {
erreurs.add('Le montant minimum doit être supérieur à zéro');
} else if (proposition.montantMaximum != null &&
proposition.montantMinimum! >= proposition.montantMaximum!) {
erreurs.add('Le montant minimum doit être inférieur au montant maximum');
}
}
}
// Validation du délai de réponse
if (proposition.delaiReponseHeures <= 0) {
erreurs.add('Le délai de réponse doit être supérieur à zéro');
} else if (proposition.delaiReponseHeures > 720) { // 30 jours max
erreurs.add('Le délai de réponse ne peut pas dépasser 30 jours');
}
// Validation du contact proposant
final contact = proposition.contactProposant;
if (contact.nom.trim().isEmpty) {
erreurs.add('Le nom du contact est obligatoire');
}
if (contact.telephone.trim().isEmpty) {
erreurs.add('Le téléphone du contact est obligatoire');
} else if (!_isValidPhoneNumber(contact.telephone)) {
erreurs.add('Le numéro de téléphone n\'est pas valide');
}
// Validation de l'email si fourni
if (contact.email != null && contact.email!.isNotEmpty) {
if (!_isValidEmail(contact.email!)) {
erreurs.add('L\'adresse email n\'est pas valide');
}
}
// Validation des zones géographiques
if (proposition.zonesGeographiques.isEmpty) {
erreurs.add('Au moins une zone géographique doit être spécifiée');
}
// Validation des créneaux de disponibilité
if (proposition.creneauxDisponibilite.isEmpty) {
erreurs.add('Au moins un créneau de disponibilité doit être spécifié');
} else {
for (int i = 0; i < proposition.creneauxDisponibilite.length; i++) {
final creneau = proposition.creneauxDisponibilite[i];
if (!_isValidTimeFormat(creneau.heureDebut)) {
erreurs.add('L\'heure de début du créneau ${i + 1} n\'est pas valide (format HH:MM)');
}
if (!_isValidTimeFormat(creneau.heureFin)) {
erreurs.add('L\'heure de fin du créneau ${i + 1} n\'est pas valide (format HH:MM)');
}
if (_isValidTimeFormat(creneau.heureDebut) &&
_isValidTimeFormat(creneau.heureFin) &&
_compareTime(creneau.heureDebut, creneau.heureFin) >= 0) {
erreurs.add('L\'heure de fin du créneau ${i + 1} doit être après l\'heure de début');
}
}
}
// Validation de la date d'expiration
if (proposition.dateExpiration != null) {
if (proposition.dateExpiration!.isBefore(DateTime.now())) {
erreurs.add('La date d\'expiration ne peut pas être dans le passé');
} else if (proposition.dateExpiration!.isAfter(DateTime.now().add(const Duration(days: 365)))) {
erreurs.add('La date d\'expiration ne peut pas dépasser un an');
}
}
if (erreurs.isNotEmpty) {
return Left(ValidationFailure(erreurs.join(', ')));
}
return const Right(true);
} catch (e) {
return Left(UnexpectedFailure('Erreur lors de la validation: ${e.toString()}'));
}
}
bool _estAideFinanciere(TypeAide typeAide) {
return [
TypeAide.aideFinanciereUrgente,
TypeAide.aideFinanciereMedicale,
TypeAide.aideFinanciereEducation,
].contains(typeAide);
}
bool _isValidPhoneNumber(String phone) {
final phoneRegex = RegExp(r'^(\+225)?[0-9]{8,10}$');
return phoneRegex.hasMatch(phone.replaceAll(RegExp(r'[\s\-\(\)]'), ''));
}
bool _isValidEmail(String email) {
final emailRegex = RegExp(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$');
return emailRegex.hasMatch(email);
}
bool _isValidTimeFormat(String time) {
final timeRegex = RegExp(r'^([01]?[0-9]|2[0-3]):[0-5][0-9]$');
return timeRegex.hasMatch(time);
}
int _compareTime(String time1, String time2) {
final parts1 = time1.split(':');
final parts2 = time2.split(':');
final minutes1 = int.parse(parts1[0]) * 60 + int.parse(parts1[1]);
final minutes2 = int.parse(parts2[0]) * 60 + int.parse(parts2[1]);
return minutes1.compareTo(minutes2);
}
}
class ValiderPropositionAideParams {
final PropositionAide proposition;
ValiderPropositionAideParams({required this.proposition});
}
/// Cas d'usage pour calculer le score de pertinence d'une proposition
class CalculerScorePropositionUseCase implements UseCase<double, CalculerScorePropositionParams> {
CalculerScorePropositionUseCase();
@override
Future<Either<Failure, double>> call(CalculerScorePropositionParams params) async {
try {
final proposition = params.proposition;
double score = 50.0; // Score de base
// Bonus pour l'expérience (nombre d'aides réalisées)
score += (proposition.nombreBeneficiairesAides * 2.0).clamp(0.0, 20.0);
// Bonus pour la note moyenne
if (proposition.noteMoyenne != null && proposition.nombreEvaluations >= 3) {
score += (proposition.noteMoyenne! - 3.0) * 10.0;
}
// Bonus pour la récence (proposition créée récemment)
final joursDepuisCreation = DateTime.now().difference(proposition.dateCreation).inDays;
if (joursDepuisCreation <= 30) {
score += 10.0;
} else if (joursDepuisCreation <= 90) {
score += 5.0;
}
// Bonus pour la disponibilité
if (proposition.isActiveEtDisponible) {
score += 15.0;
}
// Malus pour l'inactivité (pas de vues)
if (proposition.nombreVues == 0) {
score -= 10.0;
}
// Bonus pour la vérification
if (proposition.estVerifiee) {
score += 5.0;
}
return Right(score.clamp(0.0, 100.0));
} catch (e) {
return Left(UnexpectedFailure('Erreur lors du calcul du score: ${e.toString()}'));
}
}
}
class CalculerScorePropositionParams {
final PropositionAide proposition;
CalculerScorePropositionParams({required this.proposition});
}

View File

@@ -0,0 +1,428 @@
import 'package:dartz/dartz.dart';
import '../../../../core/error/failures.dart';
import '../../../../core/usecases/usecase.dart';
import '../entities/demande_aide.dart';
import '../entities/proposition_aide.dart';
import '../repositories/solidarite_repository.dart';
/// Cas d'usage pour obtenir les statistiques complètes de solidarité
class ObtenirStatistiquesSolidariteUseCase implements UseCase<StatistiquesSolidarite, ObtenirStatistiquesSolidariteParams> {
final SolidariteRepository repository;
ObtenirStatistiquesSolidariteUseCase(this.repository);
@override
Future<Either<Failure, StatistiquesSolidarite>> call(ObtenirStatistiquesSolidariteParams params) async {
final result = await repository.obtenirStatistiquesSolidarite(params.organisationId);
return result.fold(
(failure) => Left(failure),
(data) {
try {
final statistiques = StatistiquesSolidarite.fromMap(data);
return Right(statistiques);
} catch (e) {
return Left(UnexpectedFailure('Erreur lors du parsing des statistiques: ${e.toString()}'));
}
},
);
}
}
class ObtenirStatistiquesSolidariteParams {
final String organisationId;
ObtenirStatistiquesSolidariteParams({required this.organisationId});
}
/// Cas d'usage pour calculer les KPIs de performance
class CalculerKPIsPerformanceUseCase implements UseCase<KPIsPerformance, CalculerKPIsPerformanceParams> {
CalculerKPIsPerformanceUseCase();
@override
Future<Either<Failure, KPIsPerformance>> call(CalculerKPIsPerformanceParams params) async {
try {
// Simulation de calculs KPI - dans une vraie implémentation,
// ces calculs seraient basés sur des données réelles
final kpis = KPIsPerformance(
efficaciteMatching: _calculerEfficaciteMatching(params.statistiques),
tempsReponseMoyen: _calculerTempsReponseMoyen(params.statistiques),
satisfactionGlobale: _calculerSatisfactionGlobale(params.statistiques),
tauxResolution: _calculerTauxResolution(params.statistiques),
impactSocial: _calculerImpactSocial(params.statistiques),
engagementCommunautaire: _calculerEngagementCommunautaire(params.statistiques),
evolutionMensuelle: _calculerEvolutionMensuelle(params.statistiques),
objectifsAtteints: _verifierObjectifsAtteints(params.statistiques),
);
return Right(kpis);
} catch (e) {
return Left(UnexpectedFailure('Erreur lors du calcul des KPIs: ${e.toString()}'));
}
}
double _calculerEfficaciteMatching(StatistiquesSolidarite stats) {
if (stats.demandes.total == 0) return 0.0;
final demandesMatchees = stats.demandes.parStatut[StatutAide.approuvee] ?? 0;
return (demandesMatchees / stats.demandes.total) * 100;
}
Duration _calculerTempsReponseMoyen(StatistiquesSolidarite stats) {
return Duration(hours: stats.demandes.delaiMoyenTraitementHeures.toInt());
}
double _calculerSatisfactionGlobale(StatistiquesSolidarite stats) {
// Simulation basée sur le taux d'approbation
return (stats.demandes.tauxApprobation / 100) * 5.0;
}
double _calculerTauxResolution(StatistiquesSolidarite stats) {
if (stats.demandes.total == 0) return 0.0;
final demandesResolues = (stats.demandes.parStatut[StatutAide.terminee] ?? 0) +
(stats.demandes.parStatut[StatutAide.versee] ?? 0) +
(stats.demandes.parStatut[StatutAide.livree] ?? 0);
return (demandesResolues / stats.demandes.total) * 100;
}
int _calculerImpactSocial(StatistiquesSolidarite stats) {
// Estimation du nombre de personnes aidées
return (stats.demandes.total * 2.3).round(); // Moyenne de 2.3 personnes par demande
}
double _calculerEngagementCommunautaire(StatistiquesSolidarite stats) {
if (stats.propositions.total == 0) return 0.0;
return (stats.propositions.actives / stats.propositions.total) * 100;
}
EvolutionMensuelle _calculerEvolutionMensuelle(StatistiquesSolidarite stats) {
// Simulation d'évolution - dans une vraie implémentation,
// on comparerait avec les données du mois précédent
return const EvolutionMensuelle(
demandes: 12.5,
propositions: 8.3,
montants: 15.7,
satisfaction: 2.1,
);
}
Map<String, bool> _verifierObjectifsAtteints(StatistiquesSolidarite stats) {
return {
'tauxApprobation': stats.demandes.tauxApprobation >= 80.0,
'delaiTraitement': stats.demandes.delaiMoyenTraitementHeures <= 48.0,
'satisfactionMinimum': true, // Simulation
'propositionsActives': stats.propositions.actives >= 10,
};
}
}
class CalculerKPIsPerformanceParams {
final StatistiquesSolidarite statistiques;
CalculerKPIsPerformanceParams({required this.statistiques});
}
/// Cas d'usage pour générer un rapport d'activité
class GenererRapportActiviteUseCase implements UseCase<RapportActivite, GenererRapportActiviteParams> {
GenererRapportActiviteUseCase();
@override
Future<Either<Failure, RapportActivite>> call(GenererRapportActiviteParams params) async {
try {
final rapport = RapportActivite(
periode: params.periode,
dateGeneration: DateTime.now(),
resumeExecutif: _genererResumeExecutif(params.statistiques),
metriquesClees: _extraireMetriquesClees(params.statistiques),
analyseTendances: _analyserTendances(params.statistiques),
recommandations: _genererRecommandations(params.statistiques),
annexes: _genererAnnexes(params.statistiques),
);
return Right(rapport);
} catch (e) {
return Left(UnexpectedFailure('Erreur lors de la génération du rapport: ${e.toString()}'));
}
}
String _genererResumeExecutif(StatistiquesSolidarite stats) {
return '''
Durant cette période, ${stats.demandes.total} demandes d'aide ont été traitées avec un taux d'approbation de ${stats.demandes.tauxApprobation.toStringAsFixed(1)}%.
${stats.propositions.total} propositions d'aide ont été créées, dont ${stats.propositions.actives} sont actuellement actives.
Le montant total versé s'élève à ${stats.financier.montantTotalVerse.toStringAsFixed(0)} FCFA, représentant ${stats.financier.tauxVersement.toStringAsFixed(1)}% des montants approuvés.
Le délai moyen de traitement des demandes est de ${stats.demandes.delaiMoyenTraitementHeures.toStringAsFixed(1)} heures.
''';
}
Map<String, dynamic> _extraireMetriquesClees(StatistiquesSolidarite stats) {
return {
'totalDemandes': stats.demandes.total,
'tauxApprobation': stats.demandes.tauxApprobation,
'montantVerse': stats.financier.montantTotalVerse,
'propositionsActives': stats.propositions.actives,
'delaiMoyenTraitement': stats.demandes.delaiMoyenTraitementHeures,
};
}
String _analyserTendances(StatistiquesSolidarite stats) {
return '''
Tendances observées :
- Augmentation de 12.5% des demandes par rapport au mois précédent
- Amélioration du taux d'approbation (+3.2%)
- Réduction du délai moyen de traitement (-8 heures)
- Croissance de l'engagement communautaire (+5.7%)
''';
}
List<String> _genererRecommandations(StatistiquesSolidarite stats) {
final recommandations = <String>[];
if (stats.demandes.tauxApprobation < 80.0) {
recommandations.add('Améliorer le processus d\'évaluation pour augmenter le taux d\'approbation');
}
if (stats.demandes.delaiMoyenTraitementHeures > 48.0) {
recommandations.add('Optimiser les délais de traitement des demandes');
}
if (stats.propositions.actives < 10) {
recommandations.add('Encourager plus de propositions d\'aide de la part des membres');
}
if (stats.financier.tauxVersement < 90.0) {
recommandations.add('Améliorer le suivi des versements approuvés');
}
if (recommandations.isEmpty) {
recommandations.add('Maintenir l\'excellent niveau de performance actuel');
}
return recommandations;
}
Map<String, dynamic> _genererAnnexes(StatistiquesSolidarite stats) {
return {
'repartitionParType': stats.demandes.parType,
'repartitionParStatut': stats.demandes.parStatut,
'repartitionParPriorite': stats.demandes.parPriorite,
'statistiquesFinancieres': {
'montantTotalDemande': stats.financier.montantTotalDemande,
'montantTotalApprouve': stats.financier.montantTotalApprouve,
'montantTotalVerse': stats.financier.montantTotalVerse,
'capaciteFinanciereDisponible': stats.financier.capaciteFinanciereDisponible,
},
};
}
}
class GenererRapportActiviteParams {
final StatistiquesSolidarite statistiques;
final PeriodeRapport periode;
GenererRapportActiviteParams({
required this.statistiques,
required this.periode,
});
}
/// Classes de données pour les statistiques
class StatistiquesSolidarite {
final StatistiquesDemandes demandes;
final StatistiquesPropositions propositions;
final StatistiquesFinancieres financier;
final Map<String, dynamic> kpis;
final Map<String, dynamic> tendances;
final DateTime dateCalcul;
final String organisationId;
const StatistiquesSolidarite({
required this.demandes,
required this.propositions,
required this.financier,
required this.kpis,
required this.tendances,
required this.dateCalcul,
required this.organisationId,
});
factory StatistiquesSolidarite.fromMap(Map<String, dynamic> map) {
return StatistiquesSolidarite(
demandes: StatistiquesDemandes.fromMap(map['demandes']),
propositions: StatistiquesPropositions.fromMap(map['propositions']),
financier: StatistiquesFinancieres.fromMap(map['financier']),
kpis: Map<String, dynamic>.from(map['kpis']),
tendances: Map<String, dynamic>.from(map['tendances']),
dateCalcul: DateTime.parse(map['dateCalcul']),
organisationId: map['organisationId'],
);
}
}
class StatistiquesDemandes {
final int total;
final Map<StatutAide, int> parStatut;
final Map<TypeAide, int> parType;
final Map<PrioriteAide, int> parPriorite;
final int urgentes;
final int enRetard;
final double tauxApprobation;
final double delaiMoyenTraitementHeures;
const StatistiquesDemandes({
required this.total,
required this.parStatut,
required this.parType,
required this.parPriorite,
required this.urgentes,
required this.enRetard,
required this.tauxApprobation,
required this.delaiMoyenTraitementHeures,
});
factory StatistiquesDemandes.fromMap(Map<String, dynamic> map) {
return StatistiquesDemandes(
total: map['total'],
parStatut: Map<StatutAide, int>.from(map['parStatut']),
parType: Map<TypeAide, int>.from(map['parType']),
parPriorite: Map<PrioriteAide, int>.from(map['parPriorite']),
urgentes: map['urgentes'],
enRetard: map['enRetard'],
tauxApprobation: map['tauxApprobation'].toDouble(),
delaiMoyenTraitementHeures: map['delaiMoyenTraitementHeures'].toDouble(),
);
}
}
class StatistiquesPropositions {
final int total;
final int actives;
final Map<TypeAide, int> parType;
final int capaciteDisponible;
final double tauxUtilisationMoyen;
final double noteMoyenne;
const StatistiquesPropositions({
required this.total,
required this.actives,
required this.parType,
required this.capaciteDisponible,
required this.tauxUtilisationMoyen,
required this.noteMoyenne,
});
factory StatistiquesPropositions.fromMap(Map<String, dynamic> map) {
return StatistiquesPropositions(
total: map['total'],
actives: map['actives'],
parType: Map<TypeAide, int>.from(map['parType']),
capaciteDisponible: map['capaciteDisponible'],
tauxUtilisationMoyen: map['tauxUtilisationMoyen'].toDouble(),
noteMoyenne: map['noteMoyenne'].toDouble(),
);
}
}
class StatistiquesFinancieres {
final double montantTotalDemande;
final double montantTotalApprouve;
final double montantTotalVerse;
final double capaciteFinanciereDisponible;
final double montantMoyenDemande;
final double tauxVersement;
const StatistiquesFinancieres({
required this.montantTotalDemande,
required this.montantTotalApprouve,
required this.montantTotalVerse,
required this.capaciteFinanciereDisponible,
required this.montantMoyenDemande,
required this.tauxVersement,
});
factory StatistiquesFinancieres.fromMap(Map<String, dynamic> map) {
return StatistiquesFinancieres(
montantTotalDemande: map['montantTotalDemande'].toDouble(),
montantTotalApprouve: map['montantTotalApprouve'].toDouble(),
montantTotalVerse: map['montantTotalVerse'].toDouble(),
capaciteFinanciereDisponible: map['capaciteFinanciereDisponible'].toDouble(),
montantMoyenDemande: map['montantMoyenDemande'].toDouble(),
tauxVersement: map['tauxVersement'].toDouble(),
);
}
}
class KPIsPerformance {
final double efficaciteMatching;
final Duration tempsReponseMoyen;
final double satisfactionGlobale;
final double tauxResolution;
final int impactSocial;
final double engagementCommunautaire;
final EvolutionMensuelle evolutionMensuelle;
final Map<String, bool> objectifsAtteints;
const KPIsPerformance({
required this.efficaciteMatching,
required this.tempsReponseMoyen,
required this.satisfactionGlobale,
required this.tauxResolution,
required this.impactSocial,
required this.engagementCommunautaire,
required this.evolutionMensuelle,
required this.objectifsAtteints,
});
}
class EvolutionMensuelle {
final double demandes;
final double propositions;
final double montants;
final double satisfaction;
const EvolutionMensuelle({
required this.demandes,
required this.propositions,
required this.montants,
required this.satisfaction,
});
}
class RapportActivite {
final PeriodeRapport periode;
final DateTime dateGeneration;
final String resumeExecutif;
final Map<String, dynamic> metriquesClees;
final String analyseTendances;
final List<String> recommandations;
final Map<String, dynamic> annexes;
const RapportActivite({
required this.periode,
required this.dateGeneration,
required this.resumeExecutif,
required this.metriquesClees,
required this.analyseTendances,
required this.recommandations,
required this.annexes,
});
}
class PeriodeRapport {
final DateTime debut;
final DateTime fin;
final String libelle;
const PeriodeRapport({
required this.debut,
required this.fin,
required this.libelle,
});
}

View File

@@ -0,0 +1,843 @@
import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:dartz/dartz.dart';
import '../../../../../core/error/failures.dart';
import '../../../domain/entities/demande_aide.dart';
import '../../../domain/usecases/gerer_demandes_aide_usecase.dart';
import 'demandes_aide_event.dart';
import 'demandes_aide_state.dart';
/// BLoC pour la gestion des demandes d'aide
///
/// Ce BLoC gère tous les états et événements liés aux demandes d'aide,
/// incluant le chargement, la création, la modification, la validation,
/// le filtrage, le tri et l'export des demandes.
class DemandesAideBloc extends Bloc<DemandesAideEvent, DemandesAideState> {
final CreerDemandeAideUseCase creerDemandeAideUseCase;
final MettreAJourDemandeAideUseCase mettreAJourDemandeAideUseCase;
final ObtenirDemandeAideUseCase obtenirDemandeAideUseCase;
final SoumettreDemandeAideUseCase soumettreDemandeAideUseCase;
final EvaluerDemandeAideUseCase evaluerDemandeAideUseCase;
final RechercherDemandesAideUseCase rechercherDemandesAideUseCase;
final ObtenirDemandesUrgentesUseCase obtenirDemandesUrgentesUseCase;
final ObtenirMesDemandesUseCase obtenirMesDemandesUseCase;
final ValiderDemandeAideUseCase validerDemandeAideUseCase;
final CalculerPrioriteDemandeUseCase calculerPrioriteDemandeUseCase;
// Cache des paramètres de recherche pour la pagination
String? _lastOrganisationId;
TypeAide? _lastTypeAide;
StatutAide? _lastStatut;
String? _lastDemandeurId;
bool? _lastUrgente;
DemandesAideBloc({
required this.creerDemandeAideUseCase,
required this.mettreAJourDemandeAideUseCase,
required this.obtenirDemandeAideUseCase,
required this.soumettreDemandeAideUseCase,
required this.evaluerDemandeAideUseCase,
required this.rechercherDemandesAideUseCase,
required this.obtenirDemandesUrgentesUseCase,
required this.obtenirMesDemandesUseCase,
required this.validerDemandeAideUseCase,
required this.calculerPrioriteDemandeUseCase,
}) : super(const DemandesAideInitial()) {
// Enregistrement des handlers d'événements
on<ChargerDemandesAideEvent>(_onChargerDemandesAide);
on<ChargerPlusDemandesAideEvent>(_onChargerPlusDemandesAide);
on<CreerDemandeAideEvent>(_onCreerDemandeAide);
on<MettreAJourDemandeAideEvent>(_onMettreAJourDemandeAide);
on<ObtenirDemandeAideEvent>(_onObtenirDemandeAide);
on<SoumettreDemandeAideEvent>(_onSoumettreDemandeAide);
on<EvaluerDemandeAideEvent>(_onEvaluerDemandeAide);
on<ChargerDemandesUrgentesEvent>(_onChargerDemandesUrgentes);
on<ChargerMesDemandesEvent>(_onChargerMesdemandes);
on<RechercherDemandesAideEvent>(_onRechercherDemandesAide);
on<ValiderDemandeAideEvent>(_onValiderDemandeAide);
on<CalculerPrioriteDemandeEvent>(_onCalculerPrioriteDemande);
on<FiltrerDemandesAideEvent>(_onFiltrerDemandesAide);
on<TrierDemandesAideEvent>(_onTrierDemandesAide);
on<RafraichirDemandesAideEvent>(_onRafraichirDemandesAide);
on<ReinitialiserDemandesAideEvent>(_onReinitialiserDemandesAide);
on<SelectionnerDemandeAideEvent>(_onSelectionnerDemandeAide);
on<SelectionnerToutesDemandesAideEvent>(_onSelectionnerToutesDemandesAide);
on<SupprimerDemandesSelectionnees>(_onSupprimerDemandesSelectionnees);
on<ExporterDemandesAideEvent>(_onExporterDemandesAide);
}
/// Handler pour charger les demandes d'aide
Future<void> _onChargerDemandesAide(
ChargerDemandesAideEvent event,
Emitter<DemandesAideState> emit,
) async {
// Sauvegarder les paramètres pour la pagination
_lastOrganisationId = event.organisationId;
_lastTypeAide = event.typeAide;
_lastStatut = event.statut;
_lastDemandeurId = event.demandeurId;
_lastUrgente = event.urgente;
if (event.forceRefresh || state is! DemandesAideLoaded) {
emit(const DemandesAideLoading());
} else if (state is DemandesAideLoaded) {
emit((state as DemandesAideLoaded).copyWith(isRefreshing: true));
}
final result = await rechercherDemandesAideUseCase(
RechercherDemandesAideParams(
organisationId: event.organisationId,
typeAide: event.typeAide,
statut: event.statut,
demandeurId: event.demandeurId,
urgente: event.urgente,
page: 0,
taille: 20,
),
);
result.fold(
(failure) => emit(DemandesAideError(
message: _mapFailureToMessage(failure),
isNetworkError: failure is NetworkFailure,
canRetry: true,
cachedData: state is DemandesAideLoaded
? (state as DemandesAideLoaded).demandes
: null,
)),
(demandes) {
final demandesFiltrees = _appliquerFiltres(demandes, const FiltresDemandesAide());
emit(DemandesAideLoaded(
demandes: demandes,
demandesFiltrees: demandesFiltrees,
hasReachedMax: demandes.length < 20,
currentPage: 0,
totalElements: demandes.length,
lastUpdated: DateTime.now(),
));
},
);
}
/// Handler pour charger plus de demandes (pagination)
Future<void> _onChargerPlusDemandesAide(
ChargerPlusDemandesAideEvent event,
Emitter<DemandesAideState> emit,
) async {
if (state is! DemandesAideLoaded) return;
final currentState = state as DemandesAideLoaded;
if (currentState.hasReachedMax || currentState.isLoadingMore) return;
emit(currentState.copyWith(isLoadingMore: true));
final result = await rechercherDemandesAideUseCase(
RechercherDemandesAideParams(
organisationId: _lastOrganisationId,
typeAide: _lastTypeAide,
statut: _lastStatut,
demandeurId: _lastDemandeurId,
urgente: _lastUrgente,
page: currentState.currentPage + 1,
taille: 20,
),
);
result.fold(
(failure) => emit(currentState.copyWith(
isLoadingMore: false,
)),
(nouvellesDemandes) {
final toutesLesdemandes = [...currentState.demandes, ...nouvellesDemandes];
final demandesFiltrees = _appliquerFiltres(toutesLesdemandes, currentState.filtres);
emit(currentState.copyWith(
demandes: toutesLesdemandes,
demandesFiltrees: demandesFiltrees,
hasReachedMax: nouvellesDemandes.length < 20,
currentPage: currentState.currentPage + 1,
totalElements: toutesLesdemandes.length,
isLoadingMore: false,
lastUpdated: DateTime.now(),
));
},
);
}
/// Handler pour créer une demande d'aide
Future<void> _onCreerDemandeAide(
CreerDemandeAideEvent event,
Emitter<DemandesAideState> emit,
) async {
emit(const DemandesAideLoading());
final result = await creerDemandeAideUseCase(
CreerDemandeAideParams(demande: event.demande),
);
result.fold(
(failure) => emit(DemandesAideError(
message: _mapFailureToMessage(failure),
isNetworkError: failure is NetworkFailure,
canRetry: true,
)),
(demande) {
emit(DemandesAideOperationSuccess(
message: TypeOperationDemande.creation.messageSucces,
demande: demande,
operation: TypeOperationDemande.creation,
));
// Recharger la liste après création
add(const ChargerDemandesAideEvent(forceRefresh: true));
},
);
}
/// Handler pour mettre à jour une demande d'aide
Future<void> _onMettreAJourDemandeAide(
MettreAJourDemandeAideEvent event,
Emitter<DemandesAideState> emit,
) async {
emit(const DemandesAideLoading());
final result = await mettreAJourDemandeAideUseCase(
MettreAJourDemandeAideParams(demande: event.demande),
);
result.fold(
(failure) => emit(DemandesAideError(
message: _mapFailureToMessage(failure),
isNetworkError: failure is NetworkFailure,
canRetry: true,
)),
(demande) {
emit(DemandesAideOperationSuccess(
message: TypeOperationDemande.modification.messageSucces,
demande: demande,
operation: TypeOperationDemande.modification,
));
// Mettre à jour la demande dans la liste si elle existe
if (state is DemandesAideLoaded) {
final currentState = state as DemandesAideLoaded;
final demandesUpdated = currentState.demandes.map((d) =>
d.id == demande.id ? demande : d
).toList();
final demandesFiltrees = _appliquerFiltres(demandesUpdated, currentState.filtres);
emit(currentState.copyWith(
demandes: demandesUpdated,
demandesFiltrees: demandesFiltrees,
lastUpdated: DateTime.now(),
));
}
},
);
}
/// Handler pour obtenir une demande d'aide spécifique
Future<void> _onObtenirDemandeAide(
ObtenirDemandeAideEvent event,
Emitter<DemandesAideState> emit,
) async {
emit(const DemandesAideLoading());
final result = await obtenirDemandeAideUseCase(
ObtenirDemandeAideParams(id: event.demandeId),
);
result.fold(
(failure) => emit(DemandesAideError(
message: _mapFailureToMessage(failure),
isNetworkError: failure is NetworkFailure,
canRetry: true,
)),
(demande) {
// Si on a déjà une liste chargée, mettre à jour la demande
if (state is DemandesAideLoaded) {
final currentState = state as DemandesAideLoaded;
final demandesUpdated = currentState.demandes.map((d) =>
d.id == demande.id ? demande : d
).toList();
// Ajouter la demande si elle n'existe pas
if (!demandesUpdated.any((d) => d.id == demande.id)) {
demandesUpdated.insert(0, demande);
}
final demandesFiltrees = _appliquerFiltres(demandesUpdated, currentState.filtres);
emit(currentState.copyWith(
demandes: demandesUpdated,
demandesFiltrees: demandesFiltrees,
lastUpdated: DateTime.now(),
));
} else {
// Créer un nouvel état avec cette demande
emit(DemandesAideLoaded(
demandes: [demande],
demandesFiltrees: [demande],
hasReachedMax: true,
currentPage: 0,
totalElements: 1,
lastUpdated: DateTime.now(),
));
}
},
);
}
/// Handler pour soumettre une demande d'aide
Future<void> _onSoumettreDemandeAide(
SoumettreDemandeAideEvent event,
Emitter<DemandesAideState> emit,
) async {
emit(const DemandesAideLoading());
final result = await soumettreDemandeAideUseCase(
SoumettreDemandeAideParams(demandeId: event.demandeId),
);
result.fold(
(failure) => emit(DemandesAideError(
message: _mapFailureToMessage(failure),
isNetworkError: failure is NetworkFailure,
canRetry: true,
)),
(demande) {
emit(DemandesAideOperationSuccess(
message: TypeOperationDemande.soumission.messageSucces,
demande: demande,
operation: TypeOperationDemande.soumission,
));
// Mettre à jour la demande dans la liste
if (state is DemandesAideLoaded) {
final currentState = state as DemandesAideLoaded;
final demandesUpdated = currentState.demandes.map((d) =>
d.id == demande.id ? demande : d
).toList();
final demandesFiltrees = _appliquerFiltres(demandesUpdated, currentState.filtres);
emit(currentState.copyWith(
demandes: demandesUpdated,
demandesFiltrees: demandesFiltrees,
lastUpdated: DateTime.now(),
));
}
},
);
}
/// Handler pour évaluer une demande d'aide
Future<void> _onEvaluerDemandeAide(
EvaluerDemandeAideEvent event,
Emitter<DemandesAideState> emit,
) async {
emit(const DemandesAideLoading());
final result = await evaluerDemandeAideUseCase(
EvaluerDemandeAideParams(
demandeId: event.demandeId,
evaluateurId: event.evaluateurId,
decision: event.decision,
commentaire: event.commentaire,
montantApprouve: event.montantApprouve,
),
);
result.fold(
(failure) => emit(DemandesAideError(
message: _mapFailureToMessage(failure),
isNetworkError: failure is NetworkFailure,
canRetry: true,
)),
(demande) {
emit(DemandesAideOperationSuccess(
message: TypeOperationDemande.evaluation.messageSucces,
demande: demande,
operation: TypeOperationDemande.evaluation,
));
// Mettre à jour la demande dans la liste
if (state is DemandesAideLoaded) {
final currentState = state as DemandesAideLoaded;
final demandesUpdated = currentState.demandes.map((d) =>
d.id == demande.id ? demande : d
).toList();
final demandesFiltrees = _appliquerFiltres(demandesUpdated, currentState.filtres);
emit(currentState.copyWith(
demandes: demandesUpdated,
demandesFiltrees: demandesFiltrees,
lastUpdated: DateTime.now(),
));
}
},
);
}
/// Handler pour charger les demandes urgentes
Future<void> _onChargerDemandesUrgentes(
ChargerDemandesUrgentesEvent event,
Emitter<DemandesAideState> emit,
) async {
emit(const DemandesAideLoading());
final result = await obtenirDemandesUrgentesUseCase(
ObtenirDemandesUrgentesParams(organisationId: event.organisationId),
);
result.fold(
(failure) => emit(DemandesAideError(
message: _mapFailureToMessage(failure),
isNetworkError: failure is NetworkFailure,
canRetry: true,
)),
(demandes) {
final demandesFiltrees = _appliquerFiltres(demandes, const FiltresDemandesAide());
emit(DemandesAideLoaded(
demandes: demandes,
demandesFiltrees: demandesFiltrees,
hasReachedMax: true,
currentPage: 0,
totalElements: demandes.length,
lastUpdated: DateTime.now(),
));
},
);
}
/// Handler pour charger mes demandes
Future<void> _onChargerMesdemandes(
ChargerMesDemandesEvent event,
Emitter<DemandesAideState> emit,
) async {
emit(const DemandesAideLoading());
final result = await obtenirMesDemandesUseCase(
ObtenirMesDemandesParams(utilisateurId: event.utilisateurId),
);
result.fold(
(failure) => emit(DemandesAideError(
message: _mapFailureToMessage(failure),
isNetworkError: failure is NetworkFailure,
canRetry: true,
)),
(demandes) {
final demandesFiltrees = _appliquerFiltres(demandes, const FiltresDemandesAide());
emit(DemandesAideLoaded(
demandes: demandes,
demandesFiltrees: demandesFiltrees,
hasReachedMax: true,
currentPage: 0,
totalElements: demandes.length,
lastUpdated: DateTime.now(),
));
},
);
}
/// Handler pour rechercher des demandes d'aide
Future<void> _onRechercherDemandesAide(
RechercherDemandesAideEvent event,
Emitter<DemandesAideState> emit,
) async {
emit(const DemandesAideLoading());
final result = await rechercherDemandesAideUseCase(
RechercherDemandesAideParams(
organisationId: event.organisationId,
typeAide: event.typeAide,
statut: event.statut,
demandeurId: event.demandeurId,
urgente: event.urgente,
page: event.page,
taille: event.taille,
),
);
result.fold(
(failure) => emit(DemandesAideError(
message: _mapFailureToMessage(failure),
isNetworkError: failure is NetworkFailure,
canRetry: true,
)),
(demandes) {
// Appliquer le filtre par mot-clé localement
var demandesFiltrees = demandes;
if (event.motCle != null && event.motCle!.isNotEmpty) {
demandesFiltrees = demandes.where((demande) =>
demande.titre.toLowerCase().contains(event.motCle!.toLowerCase()) ||
demande.description.toLowerCase().contains(event.motCle!.toLowerCase()) ||
demande.nomDemandeur.toLowerCase().contains(event.motCle!.toLowerCase())
).toList();
}
emit(DemandesAideLoaded(
demandes: demandes,
demandesFiltrees: demandesFiltrees,
hasReachedMax: demandes.length < event.taille,
currentPage: event.page,
totalElements: demandes.length,
lastUpdated: DateTime.now(),
));
},
);
}
/// Méthode utilitaire pour appliquer les filtres
List<DemandeAide> _appliquerFiltres(List<DemandeAide> demandes, FiltresDemandesAide filtres) {
var demandesFiltrees = demandes;
if (filtres.typeAide != null) {
demandesFiltrees = demandesFiltrees.where((d) => d.typeAide == filtres.typeAide).toList();
}
if (filtres.statut != null) {
demandesFiltrees = demandesFiltrees.where((d) => d.statut == filtres.statut).toList();
}
if (filtres.priorite != null) {
demandesFiltrees = demandesFiltrees.where((d) => d.priorite == filtres.priorite).toList();
}
if (filtres.urgente != null) {
demandesFiltrees = demandesFiltrees.where((d) => d.estUrgente == filtres.urgente).toList();
}
if (filtres.motCle != null && filtres.motCle!.isNotEmpty) {
final motCle = filtres.motCle!.toLowerCase();
demandesFiltrees = demandesFiltrees.where((d) =>
d.titre.toLowerCase().contains(motCle) ||
d.description.toLowerCase().contains(motCle) ||
d.nomDemandeur.toLowerCase().contains(motCle)
).toList();
}
if (filtres.montantMin != null) {
demandesFiltrees = demandesFiltrees.where((d) =>
d.montantDemande != null && d.montantDemande! >= filtres.montantMin!
).toList();
}
if (filtres.montantMax != null) {
demandesFiltrees = demandesFiltrees.where((d) =>
d.montantDemande != null && d.montantDemande! <= filtres.montantMax!
).toList();
}
if (filtres.dateDebutCreation != null) {
demandesFiltrees = demandesFiltrees.where((d) =>
d.dateCreation.isAfter(filtres.dateDebutCreation!) ||
d.dateCreation.isAtSameMomentAs(filtres.dateDebutCreation!)
).toList();
}
if (filtres.dateFinCreation != null) {
demandesFiltrees = demandesFiltrees.where((d) =>
d.dateCreation.isBefore(filtres.dateFinCreation!) ||
d.dateCreation.isAtSameMomentAs(filtres.dateFinCreation!)
).toList();
}
return demandesFiltrees;
}
/// Handler pour valider une demande d'aide
Future<void> _onValiderDemandeAide(
ValiderDemandeAideEvent event,
Emitter<DemandesAideState> emit,
) async {
final result = await validerDemandeAideUseCase(
ValiderDemandeAideParams(demande: event.demande),
);
result.fold(
(failure) => emit(DemandesAideValidation(
erreurs: {'general': _mapFailureToMessage(failure)},
isValid: false,
demande: event.demande,
)),
(isValid) => emit(DemandesAideValidation(
erreurs: const {},
isValid: isValid,
demande: event.demande,
)),
);
}
/// Handler pour calculer la priorité d'une demande
Future<void> _onCalculerPrioriteDemande(
CalculerPrioriteDemandeEvent event,
Emitter<DemandesAideState> emit,
) async {
final result = await calculerPrioriteDemandeUseCase(
CalculerPrioriteDemandeParams(demande: event.demande),
);
result.fold(
(failure) => emit(DemandesAideError(
message: _mapFailureToMessage(failure),
canRetry: false,
)),
(priorite) {
final demandeUpdated = event.demande.copyWith(priorite: priorite);
emit(DemandesAideOperationSuccess(
message: 'Priorité calculée: ${priorite.libelle}',
demande: demandeUpdated,
operation: TypeOperationDemande.modification,
));
},
);
}
/// Handler pour filtrer les demandes localement
Future<void> _onFiltrerDemandesAide(
FiltrerDemandesAideEvent event,
Emitter<DemandesAideState> emit,
) async {
if (state is! DemandesAideLoaded) return;
final currentState = state as DemandesAideLoaded;
final nouveauxFiltres = FiltresDemandesAide(
typeAide: event.typeAide,
statut: event.statut,
priorite: event.priorite,
urgente: event.urgente,
motCle: event.motCle,
);
final demandesFiltrees = _appliquerFiltres(currentState.demandes, nouveauxFiltres);
emit(currentState.copyWith(
demandesFiltrees: demandesFiltrees,
filtres: nouveauxFiltres,
));
}
/// Handler pour trier les demandes
Future<void> _onTrierDemandesAide(
TrierDemandesAideEvent event,
Emitter<DemandesAideState> emit,
) async {
if (state is! DemandesAideLoaded) return;
final currentState = state as DemandesAideLoaded;
final demandesTriees = List<DemandeAide>.from(currentState.demandesFiltrees);
// Appliquer le tri
demandesTriees.sort((a, b) {
int comparison = 0;
switch (event.critere) {
case TriDemandes.dateCreation:
comparison = a.dateCreation.compareTo(b.dateCreation);
break;
case TriDemandes.dateModification:
comparison = a.dateModification.compareTo(b.dateModification);
break;
case TriDemandes.titre:
comparison = a.titre.compareTo(b.titre);
break;
case TriDemandes.statut:
comparison = a.statut.index.compareTo(b.statut.index);
break;
case TriDemandes.priorite:
comparison = a.priorite.index.compareTo(b.priorite.index);
break;
case TriDemandes.montant:
final montantA = a.montantDemande ?? 0.0;
final montantB = b.montantDemande ?? 0.0;
comparison = montantA.compareTo(montantB);
break;
case TriDemandes.demandeur:
comparison = a.nomDemandeur.compareTo(b.nomDemandeur);
break;
}
return event.croissant ? comparison : -comparison;
});
emit(currentState.copyWith(
demandesFiltrees: demandesTriees,
criterieTri: event.critere,
triCroissant: event.croissant,
));
}
/// Handler pour rafraîchir les demandes
Future<void> _onRafraichirDemandesAide(
RafraichirDemandesAideEvent event,
Emitter<DemandesAideState> emit,
) async {
add(ChargerDemandesAideEvent(
organisationId: _lastOrganisationId,
typeAide: _lastTypeAide,
statut: _lastStatut,
demandeurId: _lastDemandeurId,
urgente: _lastUrgente,
forceRefresh: true,
));
}
/// Handler pour réinitialiser l'état
Future<void> _onReinitialiserDemandesAide(
ReinitialiserDemandesAideEvent event,
Emitter<DemandesAideState> emit,
) async {
_lastOrganisationId = null;
_lastTypeAide = null;
_lastStatut = null;
_lastDemandeurId = null;
_lastUrgente = null;
emit(const DemandesAideInitial());
}
/// Handler pour sélectionner/désélectionner une demande
Future<void> _onSelectionnerDemandeAide(
SelectionnerDemandeAideEvent event,
Emitter<DemandesAideState> emit,
) async {
if (state is! DemandesAideLoaded) return;
final currentState = state as DemandesAideLoaded;
final nouvellesSelections = Map<String, bool>.from(currentState.demandesSelectionnees);
if (event.selectionne) {
nouvellesSelections[event.demandeId] = true;
} else {
nouvellesSelections.remove(event.demandeId);
}
emit(currentState.copyWith(demandesSelectionnees: nouvellesSelections));
}
/// Handler pour sélectionner/désélectionner toutes les demandes
Future<void> _onSelectionnerToutesDemandesAide(
SelectionnerToutesDemandesAideEvent event,
Emitter<DemandesAideState> emit,
) async {
if (state is! DemandesAideLoaded) return;
final currentState = state as DemandesAideLoaded;
final nouvellesSelections = <String, bool>{};
if (event.selectionne) {
for (final demande in currentState.demandesFiltrees) {
nouvellesSelections[demande.id] = true;
}
}
emit(currentState.copyWith(demandesSelectionnees: nouvellesSelections));
}
/// Handler pour supprimer les demandes sélectionnées
Future<void> _onSupprimerDemandesSelectionnees(
SupprimerDemandesSelectionnees event,
Emitter<DemandesAideState> emit,
) async {
if (state is! DemandesAideLoaded) return;
emit(const DemandesAideLoading());
// Simuler la suppression (à implémenter avec un vrai use case)
await Future.delayed(const Duration(seconds: 1));
final currentState = state as DemandesAideLoaded;
final demandesRestantes = currentState.demandes
.where((demande) => !event.demandeIds.contains(demande.id))
.toList();
final demandesFiltrees = _appliquerFiltres(demandesRestantes, currentState.filtres);
emit(DemandesAideOperationSuccess(
message: '${event.demandeIds.length} demande(s) supprimée(s) avec succès',
operation: TypeOperationDemande.suppression,
));
emit(currentState.copyWith(
demandes: demandesRestantes,
demandesFiltrees: demandesFiltrees,
demandesSelectionnees: const {},
totalElements: demandesRestantes.length,
lastUpdated: DateTime.now(),
));
}
/// Handler pour exporter les demandes
Future<void> _onExporterDemandesAide(
ExporterDemandesAideEvent event,
Emitter<DemandesAideState> emit,
) async {
if (state is! DemandesAideLoaded) return;
emit(const DemandesAideExporting(progress: 0.0, currentStep: 'Préparation...'));
// Simuler l'export avec progression
for (int i = 1; i <= 5; i++) {
await Future.delayed(const Duration(milliseconds: 500));
emit(DemandesAideExporting(
progress: i / 5,
currentStep: _getExportStep(i, event.format),
));
}
// Simuler la génération du fichier
final fileName = 'demandes_aide_${DateTime.now().millisecondsSinceEpoch}${event.format.extension}';
final filePath = '/storage/emulated/0/Download/$fileName';
emit(DemandesAideExported(
filePath: filePath,
format: event.format,
nombreDemandes: event.demandeIds.length,
));
emit(DemandesAideOperationSuccess(
message: 'Export réalisé avec succès: $fileName',
operation: TypeOperationDemande.export,
));
}
/// Méthode utilitaire pour obtenir l'étape d'export
String _getExportStep(int step, FormatExport format) {
switch (step) {
case 1:
return 'Récupération des données...';
case 2:
return 'Formatage des données...';
case 3:
return 'Génération du fichier ${format.libelle}...';
case 4:
return 'Optimisation...';
case 5:
return 'Finalisation...';
default:
return 'Traitement...';
}
}
/// Méthode utilitaire pour mapper les erreurs
String _mapFailureToMessage(Failure failure) {
switch (failure.runtimeType) {
case ServerFailure:
return 'Erreur serveur. Veuillez réessayer plus tard.';
case NetworkFailure:
return 'Pas de connexion internet. Vérifiez votre connexion.';
case CacheFailure:
return 'Erreur de cache local.';
case ValidationFailure:
return failure.message;
case NotFoundFailure:
return 'Demande d\'aide non trouvée.';
default:
return 'Une erreur inattendue s\'est produite.';
}
}
}

View File

@@ -0,0 +1,388 @@
import 'package:equatable/equatable.dart';
import '../../../domain/entities/demande_aide.dart';
/// Événements pour la gestion des demandes d'aide
///
/// Ces événements représentent toutes les actions possibles
/// que l'utilisateur peut effectuer sur les demandes d'aide.
abstract class DemandesAideEvent extends Equatable {
const DemandesAideEvent();
@override
List<Object?> get props => [];
}
/// Événement pour charger les demandes d'aide
class ChargerDemandesAideEvent extends DemandesAideEvent {
final String? organisationId;
final TypeAide? typeAide;
final StatutAide? statut;
final String? demandeurId;
final bool? urgente;
final bool forceRefresh;
const ChargerDemandesAideEvent({
this.organisationId,
this.typeAide,
this.statut,
this.demandeurId,
this.urgente,
this.forceRefresh = false,
});
@override
List<Object?> get props => [
organisationId,
typeAide,
statut,
demandeurId,
urgente,
forceRefresh,
];
}
/// Événement pour charger plus de demandes (pagination)
class ChargerPlusDemandesAideEvent extends DemandesAideEvent {
const ChargerPlusDemandesAideEvent();
}
/// Événement pour créer une nouvelle demande d'aide
class CreerDemandeAideEvent extends DemandesAideEvent {
final DemandeAide demande;
const CreerDemandeAideEvent({required this.demande});
@override
List<Object> get props => [demande];
}
/// Événement pour mettre à jour une demande d'aide
class MettreAJourDemandeAideEvent extends DemandesAideEvent {
final DemandeAide demande;
const MettreAJourDemandeAideEvent({required this.demande});
@override
List<Object> get props => [demande];
}
/// Événement pour obtenir une demande d'aide spécifique
class ObtenirDemandeAideEvent extends DemandesAideEvent {
final String demandeId;
const ObtenirDemandeAideEvent({required this.demandeId});
@override
List<Object> get props => [demandeId];
}
/// Événement pour soumettre une demande d'aide
class SoumettreDemandeAideEvent extends DemandesAideEvent {
final String demandeId;
const SoumettreDemandeAideEvent({required this.demandeId});
@override
List<Object> get props => [demandeId];
}
/// Événement pour évaluer une demande d'aide
class EvaluerDemandeAideEvent extends DemandesAideEvent {
final String demandeId;
final String evaluateurId;
final StatutAide decision;
final String? commentaire;
final double? montantApprouve;
const EvaluerDemandeAideEvent({
required this.demandeId,
required this.evaluateurId,
required this.decision,
this.commentaire,
this.montantApprouve,
});
@override
List<Object?> get props => [
demandeId,
evaluateurId,
decision,
commentaire,
montantApprouve,
];
}
/// Événement pour charger les demandes urgentes
class ChargerDemandesUrgentesEvent extends DemandesAideEvent {
final String organisationId;
const ChargerDemandesUrgentesEvent({required this.organisationId});
@override
List<Object> get props => [organisationId];
}
/// Événement pour charger mes demandes
class ChargerMesDemandesEvent extends DemandesAideEvent {
final String utilisateurId;
const ChargerMesDemandesEvent({required this.utilisateurId});
@override
List<Object> get props => [utilisateurId];
}
/// Événement pour rechercher des demandes d'aide
class RechercherDemandesAideEvent extends DemandesAideEvent {
final String? organisationId;
final TypeAide? typeAide;
final StatutAide? statut;
final String? demandeurId;
final bool? urgente;
final String? motCle;
final int page;
final int taille;
const RechercherDemandesAideEvent({
this.organisationId,
this.typeAide,
this.statut,
this.demandeurId,
this.urgente,
this.motCle,
this.page = 0,
this.taille = 20,
});
@override
List<Object?> get props => [
organisationId,
typeAide,
statut,
demandeurId,
urgente,
motCle,
page,
taille,
];
}
/// Événement pour valider une demande d'aide
class ValiderDemandeAideEvent extends DemandesAideEvent {
final DemandeAide demande;
const ValiderDemandeAideEvent({required this.demande});
@override
List<Object> get props => [demande];
}
/// Événement pour calculer la priorité d'une demande
class CalculerPrioriteDemandeEvent extends DemandesAideEvent {
final DemandeAide demande;
const CalculerPrioriteDemandeEvent({required this.demande});
@override
List<Object> get props => [demande];
}
/// Événement pour filtrer les demandes localement
class FiltrerDemandesAideEvent extends DemandesAideEvent {
final TypeAide? typeAide;
final StatutAide? statut;
final PrioriteAide? priorite;
final bool? urgente;
final String? motCle;
const FiltrerDemandesAideEvent({
this.typeAide,
this.statut,
this.priorite,
this.urgente,
this.motCle,
});
@override
List<Object?> get props => [
typeAide,
statut,
priorite,
urgente,
motCle,
];
}
/// Événement pour trier les demandes
class TrierDemandesAideEvent extends DemandesAideEvent {
final TriDemandes critere;
final bool croissant;
const TrierDemandesAideEvent({
required this.critere,
this.croissant = true,
});
@override
List<Object> get props => [critere, croissant];
}
/// Événement pour rafraîchir les demandes
class RafraichirDemandesAideEvent extends DemandesAideEvent {
const RafraichirDemandesAideEvent();
}
/// Événement pour réinitialiser l'état
class ReinitialiserDemandesAideEvent extends DemandesAideEvent {
const ReinitialiserDemandesAideEvent();
}
/// Événement pour sélectionner/désélectionner une demande
class SelectionnerDemandeAideEvent extends DemandesAideEvent {
final String demandeId;
final bool selectionne;
const SelectionnerDemandeAideEvent({
required this.demandeId,
required this.selectionne,
});
@override
List<Object> get props => [demandeId, selectionne];
}
/// Événement pour sélectionner/désélectionner toutes les demandes
class SelectionnerToutesDemandesAideEvent extends DemandesAideEvent {
final bool selectionne;
const SelectionnerToutesDemandesAideEvent({required this.selectionne});
@override
List<Object> get props => [selectionne];
}
/// Événement pour supprimer des demandes sélectionnées
class SupprimerDemandesSelectionnees extends DemandesAideEvent {
final List<String> demandeIds;
const SupprimerDemandesSelectionnees({required this.demandeIds});
@override
List<Object> get props => [demandeIds];
}
/// Événement pour exporter des demandes
class ExporterDemandesAideEvent extends DemandesAideEvent {
final List<String> demandeIds;
final FormatExport format;
const ExporterDemandesAideEvent({
required this.demandeIds,
required this.format,
});
@override
List<Object> get props => [demandeIds, format];
}
/// Énumération pour les critères de tri
enum TriDemandes {
dateCreation,
dateModification,
titre,
statut,
priorite,
montant,
demandeur,
}
/// Énumération pour les formats d'export
enum FormatExport {
pdf,
excel,
csv,
json,
}
/// Extension pour obtenir le libellé des critères de tri
extension TriDemandesExtension on TriDemandes {
String get libelle {
switch (this) {
case TriDemandes.dateCreation:
return 'Date de création';
case TriDemandes.dateModification:
return 'Date de modification';
case TriDemandes.titre:
return 'Titre';
case TriDemandes.statut:
return 'Statut';
case TriDemandes.priorite:
return 'Priorité';
case TriDemandes.montant:
return 'Montant';
case TriDemandes.demandeur:
return 'Demandeur';
}
}
String get icone {
switch (this) {
case TriDemandes.dateCreation:
return 'calendar_today';
case TriDemandes.dateModification:
return 'update';
case TriDemandes.titre:
return 'title';
case TriDemandes.statut:
return 'flag';
case TriDemandes.priorite:
return 'priority_high';
case TriDemandes.montant:
return 'attach_money';
case TriDemandes.demandeur:
return 'person';
}
}
}
/// Extension pour obtenir le libellé des formats d'export
extension FormatExportExtension on FormatExport {
String get libelle {
switch (this) {
case FormatExport.pdf:
return 'PDF';
case FormatExport.excel:
return 'Excel';
case FormatExport.csv:
return 'CSV';
case FormatExport.json:
return 'JSON';
}
}
String get extension {
switch (this) {
case FormatExport.pdf:
return '.pdf';
case FormatExport.excel:
return '.xlsx';
case FormatExport.csv:
return '.csv';
case FormatExport.json:
return '.json';
}
}
String get mimeType {
switch (this) {
case FormatExport.pdf:
return 'application/pdf';
case FormatExport.excel:
return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
case FormatExport.csv:
return 'text/csv';
case FormatExport.json:
return 'application/json';
}
}
}

View File

@@ -0,0 +1,434 @@
import 'package:equatable/equatable.dart';
import '../../../domain/entities/demande_aide.dart';
import 'demandes_aide_event.dart';
/// États pour la gestion des demandes d'aide
///
/// Ces états représentent tous les états possibles
/// de l'interface utilisateur pour les demandes d'aide.
abstract class DemandesAideState extends Equatable {
const DemandesAideState();
@override
List<Object?> get props => [];
}
/// État initial
class DemandesAideInitial extends DemandesAideState {
const DemandesAideInitial();
}
/// État de chargement
class DemandesAideLoading extends DemandesAideState {
final bool isRefreshing;
final bool isLoadingMore;
const DemandesAideLoading({
this.isRefreshing = false,
this.isLoadingMore = false,
});
@override
List<Object> get props => [isRefreshing, isLoadingMore];
}
/// État de succès avec données chargées
class DemandesAideLoaded extends DemandesAideState {
final List<DemandeAide> demandes;
final List<DemandeAide> demandesFiltrees;
final bool hasReachedMax;
final int currentPage;
final int totalElements;
final Map<String, bool> demandesSelectionnees;
final TriDemandes? criterieTri;
final bool triCroissant;
final FiltresDemandesAide filtres;
final bool isRefreshing;
final bool isLoadingMore;
final DateTime lastUpdated;
const DemandesAideLoaded({
required this.demandes,
required this.demandesFiltrees,
this.hasReachedMax = false,
this.currentPage = 0,
this.totalElements = 0,
this.demandesSelectionnees = const {},
this.criterieTri,
this.triCroissant = true,
this.filtres = const FiltresDemandesAide(),
this.isRefreshing = false,
this.isLoadingMore = false,
required this.lastUpdated,
});
@override
List<Object?> get props => [
demandes,
demandesFiltrees,
hasReachedMax,
currentPage,
totalElements,
demandesSelectionnees,
criterieTri,
triCroissant,
filtres,
isRefreshing,
isLoadingMore,
lastUpdated,
];
/// Copie l'état avec de nouvelles valeurs
DemandesAideLoaded copyWith({
List<DemandeAide>? demandes,
List<DemandeAide>? demandesFiltrees,
bool? hasReachedMax,
int? currentPage,
int? totalElements,
Map<String, bool>? demandesSelectionnees,
TriDemandes? criterieTri,
bool? triCroissant,
FiltresDemandesAide? filtres,
bool? isRefreshing,
bool? isLoadingMore,
DateTime? lastUpdated,
}) {
return DemandesAideLoaded(
demandes: demandes ?? this.demandes,
demandesFiltrees: demandesFiltrees ?? this.demandesFiltrees,
hasReachedMax: hasReachedMax ?? this.hasReachedMax,
currentPage: currentPage ?? this.currentPage,
totalElements: totalElements ?? this.totalElements,
demandesSelectionnees: demandesSelectionnees ?? this.demandesSelectionnees,
criterieTri: criterieTri ?? this.criterieTri,
triCroissant: triCroissant ?? this.triCroissant,
filtres: filtres ?? this.filtres,
isRefreshing: isRefreshing ?? this.isRefreshing,
isLoadingMore: isLoadingMore ?? this.isLoadingMore,
lastUpdated: lastUpdated ?? this.lastUpdated,
);
}
/// Obtient le nombre de demandes sélectionnées
int get nombreDemandesSelectionnees {
return demandesSelectionnees.values.where((selected) => selected).length;
}
/// Vérifie si toutes les demandes sont sélectionnées
bool get toutesDemandesSelectionnees {
if (demandesFiltrees.isEmpty) return false;
return demandesFiltrees.every((demande) =>
demandesSelectionnees[demande.id] == true
);
}
/// Obtient les IDs des demandes sélectionnées
List<String> get demandesSelectionneesIds {
return demandesSelectionnees.entries
.where((entry) => entry.value)
.map((entry) => entry.key)
.toList();
}
/// Obtient les demandes sélectionnées
List<DemandeAide> get demandesSelectionneesEntities {
return demandes.where((demande) =>
demandesSelectionnees[demande.id] == true
).toList();
}
/// Vérifie si des données sont disponibles
bool get hasData => demandes.isNotEmpty;
/// Vérifie si des filtres sont appliqués
bool get hasFiltres => !filtres.isEmpty;
/// Obtient le texte de statut
String get statusText {
if (isRefreshing) return 'Actualisation...';
if (isLoadingMore) return 'Chargement...';
if (demandesFiltrees.isEmpty && hasData) return 'Aucun résultat pour les filtres appliqués';
if (demandesFiltrees.isEmpty) return 'Aucune demande d\'aide';
return '${demandesFiltrees.length} demande${demandesFiltrees.length > 1 ? 's' : ''}';
}
}
/// État d'erreur
class DemandesAideError extends DemandesAideState {
final String message;
final String? code;
final bool isNetworkError;
final bool canRetry;
final List<DemandeAide>? cachedData;
const DemandesAideError({
required this.message,
this.code,
this.isNetworkError = false,
this.canRetry = true,
this.cachedData,
});
@override
List<Object?> get props => [
message,
code,
isNetworkError,
canRetry,
cachedData,
];
/// Vérifie si des données en cache sont disponibles
bool get hasCachedData => cachedData != null && cachedData!.isNotEmpty;
}
/// État de succès pour une opération spécifique
class DemandesAideOperationSuccess extends DemandesAideState {
final String message;
final DemandeAide? demande;
final TypeOperationDemande operation;
const DemandesAideOperationSuccess({
required this.message,
this.demande,
required this.operation,
});
@override
List<Object?> get props => [message, demande, operation];
}
/// État de validation
class DemandesAideValidation extends DemandesAideState {
final Map<String, String> erreurs;
final bool isValid;
final DemandeAide? demande;
const DemandesAideValidation({
required this.erreurs,
required this.isValid,
this.demande,
});
@override
List<Object?> get props => [erreurs, isValid, demande];
/// Obtient la première erreur
String? get premiereErreur {
return erreurs.values.isNotEmpty ? erreurs.values.first : null;
}
/// Obtient les erreurs pour un champ spécifique
String? getErreurPourChamp(String champ) {
return erreurs[champ];
}
}
/// État d'export
class DemandesAideExporting extends DemandesAideState {
final double progress;
final String? currentStep;
const DemandesAideExporting({
required this.progress,
this.currentStep,
});
@override
List<Object?> get props => [progress, currentStep];
}
/// État d'export terminé
class DemandesAideExported extends DemandesAideState {
final String filePath;
final FormatExport format;
final int nombreDemandes;
const DemandesAideExported({
required this.filePath,
required this.format,
required this.nombreDemandes,
});
@override
List<Object> get props => [filePath, format, nombreDemandes];
}
/// Classe pour les filtres des demandes d'aide
class FiltresDemandesAide extends Equatable {
final TypeAide? typeAide;
final StatutAide? statut;
final PrioriteAide? priorite;
final bool? urgente;
final String? motCle;
final String? organisationId;
final String? demandeurId;
final DateTime? dateDebutCreation;
final DateTime? dateFinCreation;
final double? montantMin;
final double? montantMax;
const FiltresDemandesAide({
this.typeAide,
this.statut,
this.priorite,
this.urgente,
this.motCle,
this.organisationId,
this.demandeurId,
this.dateDebutCreation,
this.dateFinCreation,
this.montantMin,
this.montantMax,
});
@override
List<Object?> get props => [
typeAide,
statut,
priorite,
urgente,
motCle,
organisationId,
demandeurId,
dateDebutCreation,
dateFinCreation,
montantMin,
montantMax,
];
/// Copie les filtres avec de nouvelles valeurs
FiltresDemandesAide copyWith({
TypeAide? typeAide,
StatutAide? statut,
PrioriteAide? priorite,
bool? urgente,
String? motCle,
String? organisationId,
String? demandeurId,
DateTime? dateDebutCreation,
DateTime? dateFinCreation,
double? montantMin,
double? montantMax,
}) {
return FiltresDemandesAide(
typeAide: typeAide ?? this.typeAide,
statut: statut ?? this.statut,
priorite: priorite ?? this.priorite,
urgente: urgente ?? this.urgente,
motCle: motCle ?? this.motCle,
organisationId: organisationId ?? this.organisationId,
demandeurId: demandeurId ?? this.demandeurId,
dateDebutCreation: dateDebutCreation ?? this.dateDebutCreation,
dateFinCreation: dateFinCreation ?? this.dateFinCreation,
montantMin: montantMin ?? this.montantMin,
montantMax: montantMax ?? this.montantMax,
);
}
/// Réinitialise tous les filtres
FiltresDemandesAide clear() {
return const FiltresDemandesAide();
}
/// Vérifie si les filtres sont vides
bool get isEmpty {
return typeAide == null &&
statut == null &&
priorite == null &&
urgente == null &&
(motCle == null || motCle!.isEmpty) &&
organisationId == null &&
demandeurId == null &&
dateDebutCreation == null &&
dateFinCreation == null &&
montantMin == null &&
montantMax == null;
}
/// Obtient le nombre de filtres actifs
int get nombreFiltresActifs {
int count = 0;
if (typeAide != null) count++;
if (statut != null) count++;
if (priorite != null) count++;
if (urgente != null) count++;
if (motCle != null && motCle!.isNotEmpty) count++;
if (organisationId != null) count++;
if (demandeurId != null) count++;
if (dateDebutCreation != null) count++;
if (dateFinCreation != null) count++;
if (montantMin != null) count++;
if (montantMax != null) count++;
return count;
}
/// Obtient une description textuelle des filtres
String get description {
final parts = <String>[];
if (typeAide != null) parts.add('Type: ${typeAide!.libelle}');
if (statut != null) parts.add('Statut: ${statut!.libelle}');
if (priorite != null) parts.add('Priorité: ${priorite!.libelle}');
if (urgente == true) parts.add('Urgente uniquement');
if (motCle != null && motCle!.isNotEmpty) parts.add('Recherche: "$motCle"');
if (montantMin != null || montantMax != null) {
if (montantMin != null && montantMax != null) {
parts.add('Montant: ${montantMin!.toInt()} - ${montantMax!.toInt()} FCFA');
} else if (montantMin != null) {
parts.add('Montant min: ${montantMin!.toInt()} FCFA');
} else {
parts.add('Montant max: ${montantMax!.toInt()} FCFA');
}
}
return parts.join(', ');
}
}
/// Énumération pour les types d'opération
enum TypeOperationDemande {
creation,
modification,
soumission,
evaluation,
suppression,
export,
}
/// Extension pour obtenir le libellé des opérations
extension TypeOperationDemandeExtension on TypeOperationDemande {
String get libelle {
switch (this) {
case TypeOperationDemande.creation:
return 'Création';
case TypeOperationDemande.modification:
return 'Modification';
case TypeOperationDemande.soumission:
return 'Soumission';
case TypeOperationDemande.evaluation:
return 'Évaluation';
case TypeOperationDemande.suppression:
return 'Suppression';
case TypeOperationDemande.export:
return 'Export';
}
}
String get messageSucces {
switch (this) {
case TypeOperationDemande.creation:
return 'Demande d\'aide créée avec succès';
case TypeOperationDemande.modification:
return 'Demande d\'aide modifiée avec succès';
case TypeOperationDemande.soumission:
return 'Demande d\'aide soumise avec succès';
case TypeOperationDemande.evaluation:
return 'Demande d\'aide évaluée avec succès';
case TypeOperationDemande.suppression:
return 'Demande d\'aide supprimée avec succès';
case TypeOperationDemande.export:
return 'Export réalisé avec succès';
}
}
}

View File

@@ -0,0 +1,438 @@
import 'package:equatable/equatable.dart';
import '../../../domain/entities/evaluation_aide.dart';
/// Événements pour la gestion des évaluations d'aide
///
/// Ces événements représentent toutes les actions possibles
/// que l'utilisateur peut effectuer sur les évaluations d'aide.
abstract class EvaluationsEvent extends Equatable {
const EvaluationsEvent();
@override
List<Object?> get props => [];
}
/// Événement pour charger les évaluations
class ChargerEvaluationsEvent extends EvaluationsEvent {
final String? demandeId;
final String? evaluateurId;
final TypeEvaluateur? typeEvaluateur;
final StatutAide? decision;
final bool forceRefresh;
const ChargerEvaluationsEvent({
this.demandeId,
this.evaluateurId,
this.typeEvaluateur,
this.decision,
this.forceRefresh = false,
});
@override
List<Object?> get props => [
demandeId,
evaluateurId,
typeEvaluateur,
decision,
forceRefresh,
];
}
/// Événement pour charger plus d'évaluations (pagination)
class ChargerPlusEvaluationsEvent extends EvaluationsEvent {
const ChargerPlusEvaluationsEvent();
}
/// Événement pour créer une nouvelle évaluation
class CreerEvaluationEvent extends EvaluationsEvent {
final EvaluationAide evaluation;
const CreerEvaluationEvent({required this.evaluation});
@override
List<Object> get props => [evaluation];
}
/// Événement pour mettre à jour une évaluation
class MettreAJourEvaluationEvent extends EvaluationsEvent {
final EvaluationAide evaluation;
const MettreAJourEvaluationEvent({required this.evaluation});
@override
List<Object> get props => [evaluation];
}
/// Événement pour obtenir une évaluation spécifique
class ObtenirEvaluationEvent extends EvaluationsEvent {
final String evaluationId;
const ObtenirEvaluationEvent({required this.evaluationId});
@override
List<Object> get props => [evaluationId];
}
/// Événement pour soumettre une évaluation
class SoumettreEvaluationEvent extends EvaluationsEvent {
final String evaluationId;
const SoumettreEvaluationEvent({required this.evaluationId});
@override
List<Object> get props => [evaluationId];
}
/// Événement pour approuver une évaluation
class ApprouverEvaluationEvent extends EvaluationsEvent {
final String evaluationId;
final String? commentaire;
const ApprouverEvaluationEvent({
required this.evaluationId,
this.commentaire,
});
@override
List<Object?> get props => [evaluationId, commentaire];
}
/// Événement pour rejeter une évaluation
class RejeterEvaluationEvent extends EvaluationsEvent {
final String evaluationId;
final String motifRejet;
const RejeterEvaluationEvent({
required this.evaluationId,
required this.motifRejet,
});
@override
List<Object> get props => [evaluationId, motifRejet];
}
/// Événement pour rechercher des évaluations
class RechercherEvaluationsEvent extends EvaluationsEvent {
final String? demandeId;
final String? evaluateurId;
final TypeEvaluateur? typeEvaluateur;
final StatutAide? decision;
final DateTime? dateDebut;
final DateTime? dateFin;
final double? noteMin;
final double? noteMax;
final String? motCle;
final int page;
final int taille;
const RechercherEvaluationsEvent({
this.demandeId,
this.evaluateurId,
this.typeEvaluateur,
this.decision,
this.dateDebut,
this.dateFin,
this.noteMin,
this.noteMax,
this.motCle,
this.page = 0,
this.taille = 20,
});
@override
List<Object?> get props => [
demandeId,
evaluateurId,
typeEvaluateur,
decision,
dateDebut,
dateFin,
noteMin,
noteMax,
motCle,
page,
taille,
];
}
/// Événement pour charger mes évaluations
class ChargerMesEvaluationsEvent extends EvaluationsEvent {
final String evaluateurId;
const ChargerMesEvaluationsEvent({required this.evaluateurId});
@override
List<Object> get props => [evaluateurId];
}
/// Événement pour charger les évaluations en attente
class ChargerEvaluationsEnAttenteEvent extends EvaluationsEvent {
final String? evaluateurId;
final TypeEvaluateur? typeEvaluateur;
const ChargerEvaluationsEnAttenteEvent({
this.evaluateurId,
this.typeEvaluateur,
});
@override
List<Object?> get props => [evaluateurId, typeEvaluateur];
}
/// Événement pour valider une évaluation
class ValiderEvaluationEvent extends EvaluationsEvent {
final EvaluationAide evaluation;
const ValiderEvaluationEvent({required this.evaluation});
@override
List<Object> get props => [evaluation];
}
/// Événement pour calculer la note globale
class CalculerNoteGlobaleEvent extends EvaluationsEvent {
final Map<String, double> criteres;
const CalculerNoteGlobaleEvent({required this.criteres});
@override
List<Object> get props => [criteres];
}
/// Événement pour filtrer les évaluations localement
class FiltrerEvaluationsEvent extends EvaluationsEvent {
final TypeEvaluateur? typeEvaluateur;
final StatutAide? decision;
final double? noteMin;
final double? noteMax;
final String? motCle;
final DateTime? dateDebut;
final DateTime? dateFin;
const FiltrerEvaluationsEvent({
this.typeEvaluateur,
this.decision,
this.noteMin,
this.noteMax,
this.motCle,
this.dateDebut,
this.dateFin,
});
@override
List<Object?> get props => [
typeEvaluateur,
decision,
noteMin,
noteMax,
motCle,
dateDebut,
dateFin,
];
}
/// Événement pour trier les évaluations
class TrierEvaluationsEvent extends EvaluationsEvent {
final TriEvaluations critere;
final bool croissant;
const TrierEvaluationsEvent({
required this.critere,
this.croissant = true,
});
@override
List<Object> get props => [critere, croissant];
}
/// Événement pour rafraîchir les évaluations
class RafraichirEvaluationsEvent extends EvaluationsEvent {
const RafraichirEvaluationsEvent();
}
/// Événement pour réinitialiser l'état
class ReinitialiserEvaluationsEvent extends EvaluationsEvent {
const ReinitialiserEvaluationsEvent();
}
/// Événement pour sélectionner/désélectionner une évaluation
class SelectionnerEvaluationEvent extends EvaluationsEvent {
final String evaluationId;
final bool selectionne;
const SelectionnerEvaluationEvent({
required this.evaluationId,
required this.selectionne,
});
@override
List<Object> get props => [evaluationId, selectionne];
}
/// Événement pour sélectionner/désélectionner toutes les évaluations
class SelectionnerToutesEvaluationsEvent extends EvaluationsEvent {
final bool selectionne;
const SelectionnerToutesEvaluationsEvent({required this.selectionne});
@override
List<Object> get props => [selectionne];
}
/// Événement pour supprimer des évaluations sélectionnées
class SupprimerEvaluationsSelectionnees extends EvaluationsEvent {
final List<String> evaluationIds;
const SupprimerEvaluationsSelectionnees({required this.evaluationIds});
@override
List<Object> get props => [evaluationIds];
}
/// Événement pour exporter des évaluations
class ExporterEvaluationsEvent extends EvaluationsEvent {
final List<String> evaluationIds;
final FormatExport format;
const ExporterEvaluationsEvent({
required this.evaluationIds,
required this.format,
});
@override
List<Object> get props => [evaluationIds, format];
}
/// Événement pour obtenir les statistiques d'évaluation
class ObtenirStatistiquesEvaluationEvent extends EvaluationsEvent {
final String? evaluateurId;
final DateTime? dateDebut;
final DateTime? dateFin;
const ObtenirStatistiquesEvaluationEvent({
this.evaluateurId,
this.dateDebut,
this.dateFin,
});
@override
List<Object?> get props => [evaluateurId, dateDebut, dateFin];
}
/// Événement pour signaler une évaluation
class SignalerEvaluationEvent extends EvaluationsEvent {
final String evaluationId;
final String motifSignalement;
final String? description;
const SignalerEvaluationEvent({
required this.evaluationId,
required this.motifSignalement,
this.description,
});
@override
List<Object?> get props => [evaluationId, motifSignalement, description];
}
/// Énumération pour les critères de tri
enum TriEvaluations {
dateEvaluation,
dateCreation,
noteGlobale,
decision,
evaluateur,
typeEvaluateur,
demandeId,
}
/// Énumération pour les formats d'export
enum FormatExport {
pdf,
excel,
csv,
json,
}
/// Extension pour obtenir le libellé des critères de tri
extension TriEvaluationsExtension on TriEvaluations {
String get libelle {
switch (this) {
case TriEvaluations.dateEvaluation:
return 'Date d\'évaluation';
case TriEvaluations.dateCreation:
return 'Date de création';
case TriEvaluations.noteGlobale:
return 'Note globale';
case TriEvaluations.decision:
return 'Décision';
case TriEvaluations.evaluateur:
return 'Évaluateur';
case TriEvaluations.typeEvaluateur:
return 'Type d\'évaluateur';
case TriEvaluations.demandeId:
return 'Demande';
}
}
String get icone {
switch (this) {
case TriEvaluations.dateEvaluation:
return 'calendar_today';
case TriEvaluations.dateCreation:
return 'schedule';
case TriEvaluations.noteGlobale:
return 'star';
case TriEvaluations.decision:
return 'gavel';
case TriEvaluations.evaluateur:
return 'person';
case TriEvaluations.typeEvaluateur:
return 'badge';
case TriEvaluations.demandeId:
return 'description';
}
}
}
/// Extension pour obtenir le libellé des formats d'export
extension FormatExportExtension on FormatExport {
String get libelle {
switch (this) {
case FormatExport.pdf:
return 'PDF';
case FormatExport.excel:
return 'Excel';
case FormatExport.csv:
return 'CSV';
case FormatExport.json:
return 'JSON';
}
}
String get extension {
switch (this) {
case FormatExport.pdf:
return '.pdf';
case FormatExport.excel:
return '.xlsx';
case FormatExport.csv:
return '.csv';
case FormatExport.json:
return '.json';
}
}
String get mimeType {
switch (this) {
case FormatExport.pdf:
return 'application/pdf';
case FormatExport.excel:
return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
case FormatExport.csv:
return 'text/csv';
case FormatExport.json:
return 'application/json';
}
}
}

View File

@@ -0,0 +1,478 @@
import 'package:equatable/equatable.dart';
import '../../../domain/entities/evaluation_aide.dart';
import 'evaluations_event.dart';
/// États pour la gestion des évaluations d'aide
///
/// Ces états représentent tous les états possibles
/// de l'interface utilisateur pour les évaluations d'aide.
abstract class EvaluationsState extends Equatable {
const EvaluationsState();
@override
List<Object?> get props => [];
}
/// État initial
class EvaluationsInitial extends EvaluationsState {
const EvaluationsInitial();
}
/// État de chargement
class EvaluationsLoading extends EvaluationsState {
final bool isRefreshing;
final bool isLoadingMore;
const EvaluationsLoading({
this.isRefreshing = false,
this.isLoadingMore = false,
});
@override
List<Object> get props => [isRefreshing, isLoadingMore];
}
/// État de succès avec données chargées
class EvaluationsLoaded extends EvaluationsState {
final List<EvaluationAide> evaluations;
final List<EvaluationAide> evaluationsFiltrees;
final bool hasReachedMax;
final int currentPage;
final int totalElements;
final Map<String, bool> evaluationsSelectionnees;
final TriEvaluations? criterieTri;
final bool triCroissant;
final FiltresEvaluations filtres;
final bool isRefreshing;
final bool isLoadingMore;
final DateTime lastUpdated;
const EvaluationsLoaded({
required this.evaluations,
required this.evaluationsFiltrees,
this.hasReachedMax = false,
this.currentPage = 0,
this.totalElements = 0,
this.evaluationsSelectionnees = const {},
this.criterieTri,
this.triCroissant = true,
this.filtres = const FiltresEvaluations(),
this.isRefreshing = false,
this.isLoadingMore = false,
required this.lastUpdated,
});
@override
List<Object?> get props => [
evaluations,
evaluationsFiltrees,
hasReachedMax,
currentPage,
totalElements,
evaluationsSelectionnees,
criterieTri,
triCroissant,
filtres,
isRefreshing,
isLoadingMore,
lastUpdated,
];
/// Copie l'état avec de nouvelles valeurs
EvaluationsLoaded copyWith({
List<EvaluationAide>? evaluations,
List<EvaluationAide>? evaluationsFiltrees,
bool? hasReachedMax,
int? currentPage,
int? totalElements,
Map<String, bool>? evaluationsSelectionnees,
TriEvaluations? criterieTri,
bool? triCroissant,
FiltresEvaluations? filtres,
bool? isRefreshing,
bool? isLoadingMore,
DateTime? lastUpdated,
}) {
return EvaluationsLoaded(
evaluations: evaluations ?? this.evaluations,
evaluationsFiltrees: evaluationsFiltrees ?? this.evaluationsFiltrees,
hasReachedMax: hasReachedMax ?? this.hasReachedMax,
currentPage: currentPage ?? this.currentPage,
totalElements: totalElements ?? this.totalElements,
evaluationsSelectionnees: evaluationsSelectionnees ?? this.evaluationsSelectionnees,
criterieTri: criterieTri ?? this.criterieTri,
triCroissant: triCroissant ?? this.triCroissant,
filtres: filtres ?? this.filtres,
isRefreshing: isRefreshing ?? this.isRefreshing,
isLoadingMore: isLoadingMore ?? this.isLoadingMore,
lastUpdated: lastUpdated ?? this.lastUpdated,
);
}
/// Obtient le nombre d'évaluations sélectionnées
int get nombreEvaluationsSelectionnees {
return evaluationsSelectionnees.values.where((selected) => selected).length;
}
/// Vérifie si toutes les évaluations sont sélectionnées
bool get toutesEvaluationsSelectionnees {
if (evaluationsFiltrees.isEmpty) return false;
return evaluationsFiltrees.every((evaluation) =>
evaluationsSelectionnees[evaluation.id] == true
);
}
/// Obtient les IDs des évaluations sélectionnées
List<String> get evaluationsSelectionneesIds {
return evaluationsSelectionnees.entries
.where((entry) => entry.value)
.map((entry) => entry.key)
.toList();
}
/// Obtient les évaluations sélectionnées
List<EvaluationAide> get evaluationsSelectionneesEntities {
return evaluations.where((evaluation) =>
evaluationsSelectionnees[evaluation.id] == true
).toList();
}
/// Vérifie si des données sont disponibles
bool get hasData => evaluations.isNotEmpty;
/// Vérifie si des filtres sont appliqués
bool get hasFiltres => !filtres.isEmpty;
/// Obtient le texte de statut
String get statusText {
if (isRefreshing) return 'Actualisation...';
if (isLoadingMore) return 'Chargement...';
if (evaluationsFiltrees.isEmpty && hasData) return 'Aucun résultat pour les filtres appliqués';
if (evaluationsFiltrees.isEmpty) return 'Aucune évaluation';
return '${evaluationsFiltrees.length} évaluation${evaluationsFiltrees.length > 1 ? 's' : ''}';
}
/// Obtient la note moyenne
double get noteMoyenne {
if (evaluationsFiltrees.isEmpty) return 0.0;
final notesValides = evaluationsFiltrees
.where((e) => e.noteGlobale != null)
.map((e) => e.noteGlobale!)
.toList();
if (notesValides.isEmpty) return 0.0;
return notesValides.reduce((a, b) => a + b) / notesValides.length;
}
/// Obtient le nombre d'évaluations par décision
Map<StatutAide, int> get repartitionDecisions {
final repartition = <StatutAide, int>{};
for (final evaluation in evaluationsFiltrees) {
repartition[evaluation.decision] = (repartition[evaluation.decision] ?? 0) + 1;
}
return repartition;
}
}
/// État d'erreur
class EvaluationsError extends EvaluationsState {
final String message;
final String? code;
final bool isNetworkError;
final bool canRetry;
final List<EvaluationAide>? cachedData;
const EvaluationsError({
required this.message,
this.code,
this.isNetworkError = false,
this.canRetry = true,
this.cachedData,
});
@override
List<Object?> get props => [
message,
code,
isNetworkError,
canRetry,
cachedData,
];
/// Vérifie si des données en cache sont disponibles
bool get hasCachedData => cachedData != null && cachedData!.isNotEmpty;
}
/// État de succès pour une opération spécifique
class EvaluationsOperationSuccess extends EvaluationsState {
final String message;
final EvaluationAide? evaluation;
final TypeOperationEvaluation operation;
const EvaluationsOperationSuccess({
required this.message,
this.evaluation,
required this.operation,
});
@override
List<Object?> get props => [message, evaluation, operation];
}
/// État de validation
class EvaluationsValidation extends EvaluationsState {
final Map<String, String> erreurs;
final bool isValid;
final EvaluationAide? evaluation;
const EvaluationsValidation({
required this.erreurs,
required this.isValid,
this.evaluation,
});
@override
List<Object?> get props => [erreurs, isValid, evaluation];
/// Obtient la première erreur
String? get premiereErreur {
return erreurs.values.isNotEmpty ? erreurs.values.first : null;
}
/// Obtient les erreurs pour un champ spécifique
String? getErreurPourChamp(String champ) {
return erreurs[champ];
}
}
/// État de calcul de note globale
class EvaluationsNoteCalculee extends EvaluationsState {
final double noteGlobale;
final Map<String, double> criteres;
const EvaluationsNoteCalculee({
required this.noteGlobale,
required this.criteres,
});
@override
List<Object> get props => [noteGlobale, criteres];
}
/// État des statistiques d'évaluation
class EvaluationsStatistiques extends EvaluationsState {
final Map<String, dynamic> statistiques;
final DateTime? dateDebut;
final DateTime? dateFin;
const EvaluationsStatistiques({
required this.statistiques,
this.dateDebut,
this.dateFin,
});
@override
List<Object?> get props => [statistiques, dateDebut, dateFin];
}
/// État d'export
class EvaluationsExporting extends EvaluationsState {
final double progress;
final String? currentStep;
const EvaluationsExporting({
required this.progress,
this.currentStep,
});
@override
List<Object?> get props => [progress, currentStep];
}
/// État d'export terminé
class EvaluationsExported extends EvaluationsState {
final String filePath;
final FormatExport format;
final int nombreEvaluations;
const EvaluationsExported({
required this.filePath,
required this.format,
required this.nombreEvaluations,
});
@override
List<Object> get props => [filePath, format, nombreEvaluations];
}
/// Classe pour les filtres des évaluations
class FiltresEvaluations extends Equatable {
final TypeEvaluateur? typeEvaluateur;
final StatutAide? decision;
final double? noteMin;
final double? noteMax;
final String? motCle;
final String? evaluateurId;
final String? demandeId;
final DateTime? dateDebutEvaluation;
final DateTime? dateFinEvaluation;
const FiltresEvaluations({
this.typeEvaluateur,
this.decision,
this.noteMin,
this.noteMax,
this.motCle,
this.evaluateurId,
this.demandeId,
this.dateDebutEvaluation,
this.dateFinEvaluation,
});
@override
List<Object?> get props => [
typeEvaluateur,
decision,
noteMin,
noteMax,
motCle,
evaluateurId,
demandeId,
dateDebutEvaluation,
dateFinEvaluation,
];
/// Copie les filtres avec de nouvelles valeurs
FiltresEvaluations copyWith({
TypeEvaluateur? typeEvaluateur,
StatutAide? decision,
double? noteMin,
double? noteMax,
String? motCle,
String? evaluateurId,
String? demandeId,
DateTime? dateDebutEvaluation,
DateTime? dateFinEvaluation,
}) {
return FiltresEvaluations(
typeEvaluateur: typeEvaluateur ?? this.typeEvaluateur,
decision: decision ?? this.decision,
noteMin: noteMin ?? this.noteMin,
noteMax: noteMax ?? this.noteMax,
motCle: motCle ?? this.motCle,
evaluateurId: evaluateurId ?? this.evaluateurId,
demandeId: demandeId ?? this.demandeId,
dateDebutEvaluation: dateDebutEvaluation ?? this.dateDebutEvaluation,
dateFinEvaluation: dateFinEvaluation ?? this.dateFinEvaluation,
);
}
/// Réinitialise tous les filtres
FiltresEvaluations clear() {
return const FiltresEvaluations();
}
/// Vérifie si les filtres sont vides
bool get isEmpty {
return typeEvaluateur == null &&
decision == null &&
noteMin == null &&
noteMax == null &&
(motCle == null || motCle!.isEmpty) &&
evaluateurId == null &&
demandeId == null &&
dateDebutEvaluation == null &&
dateFinEvaluation == null;
}
/// Obtient le nombre de filtres actifs
int get nombreFiltresActifs {
int count = 0;
if (typeEvaluateur != null) count++;
if (decision != null) count++;
if (noteMin != null) count++;
if (noteMax != null) count++;
if (motCle != null && motCle!.isNotEmpty) count++;
if (evaluateurId != null) count++;
if (demandeId != null) count++;
if (dateDebutEvaluation != null) count++;
if (dateFinEvaluation != null) count++;
return count;
}
/// Obtient une description textuelle des filtres
String get description {
final parts = <String>[];
if (typeEvaluateur != null) parts.add('Type: ${typeEvaluateur!.libelle}');
if (decision != null) parts.add('Décision: ${decision!.libelle}');
if (motCle != null && motCle!.isNotEmpty) parts.add('Recherche: "$motCle"');
if (noteMin != null || noteMax != null) {
if (noteMin != null && noteMax != null) {
parts.add('Note: ${noteMin!.toStringAsFixed(1)} - ${noteMax!.toStringAsFixed(1)}');
} else if (noteMin != null) {
parts.add('Note min: ${noteMin!.toStringAsFixed(1)}');
} else {
parts.add('Note max: ${noteMax!.toStringAsFixed(1)}');
}
}
return parts.join(', ');
}
}
/// Énumération pour les types d'opération
enum TypeOperationEvaluation {
creation,
modification,
soumission,
approbation,
rejet,
suppression,
export,
signalement,
}
/// Extension pour obtenir le libellé des opérations
extension TypeOperationEvaluationExtension on TypeOperationEvaluation {
String get libelle {
switch (this) {
case TypeOperationEvaluation.creation:
return 'Création';
case TypeOperationEvaluation.modification:
return 'Modification';
case TypeOperationEvaluation.soumission:
return 'Soumission';
case TypeOperationEvaluation.approbation:
return 'Approbation';
case TypeOperationEvaluation.rejet:
return 'Rejet';
case TypeOperationEvaluation.suppression:
return 'Suppression';
case TypeOperationEvaluation.export:
return 'Export';
case TypeOperationEvaluation.signalement:
return 'Signalement';
}
}
String get messageSucces {
switch (this) {
case TypeOperationEvaluation.creation:
return 'Évaluation créée avec succès';
case TypeOperationEvaluation.modification:
return 'Évaluation modifiée avec succès';
case TypeOperationEvaluation.soumission:
return 'Évaluation soumise avec succès';
case TypeOperationEvaluation.approbation:
return 'Évaluation approuvée avec succès';
case TypeOperationEvaluation.rejet:
return 'Évaluation rejetée avec succès';
case TypeOperationEvaluation.suppression:
return 'Évaluation supprimée avec succès';
case TypeOperationEvaluation.export:
return 'Export réalisé avec succès';
case TypeOperationEvaluation.signalement:
return 'Évaluation signalée avec succès';
}
}
}

View File

@@ -0,0 +1,382 @@
import 'package:equatable/equatable.dart';
import '../../../domain/entities/proposition_aide.dart';
/// Événements pour la gestion des propositions d'aide
///
/// Ces événements représentent toutes les actions possibles
/// que l'utilisateur peut effectuer sur les propositions d'aide.
abstract class PropositionsAideEvent extends Equatable {
const PropositionsAideEvent();
@override
List<Object?> get props => [];
}
/// Événement pour charger les propositions d'aide
class ChargerPropositionsAideEvent extends PropositionsAideEvent {
final String? organisationId;
final TypeAide? typeAide;
final StatutProposition? statut;
final String? proposantId;
final bool? disponible;
final bool forceRefresh;
const ChargerPropositionsAideEvent({
this.organisationId,
this.typeAide,
this.statut,
this.proposantId,
this.disponible,
this.forceRefresh = false,
});
@override
List<Object?> get props => [
organisationId,
typeAide,
statut,
proposantId,
disponible,
forceRefresh,
];
}
/// Événement pour charger plus de propositions (pagination)
class ChargerPlusPropositionsAideEvent extends PropositionsAideEvent {
const ChargerPlusPropositionsAideEvent();
}
/// Événement pour créer une nouvelle proposition d'aide
class CreerPropositionAideEvent extends PropositionsAideEvent {
final PropositionAide proposition;
const CreerPropositionAideEvent({required this.proposition});
@override
List<Object> get props => [proposition];
}
/// Événement pour mettre à jour une proposition d'aide
class MettreAJourPropositionAideEvent extends PropositionsAideEvent {
final PropositionAide proposition;
const MettreAJourPropositionAideEvent({required this.proposition});
@override
List<Object> get props => [proposition];
}
/// Événement pour obtenir une proposition d'aide spécifique
class ObtenirPropositionAideEvent extends PropositionsAideEvent {
final String propositionId;
const ObtenirPropositionAideEvent({required this.propositionId});
@override
List<Object> get props => [propositionId];
}
/// Événement pour activer/désactiver une proposition
class ToggleDisponibilitePropositionEvent extends PropositionsAideEvent {
final String propositionId;
final bool disponible;
const ToggleDisponibilitePropositionEvent({
required this.propositionId,
required this.disponible,
});
@override
List<Object> get props => [propositionId, disponible];
}
/// Événement pour rechercher des propositions d'aide
class RechercherPropositionsAideEvent extends PropositionsAideEvent {
final String? organisationId;
final TypeAide? typeAide;
final StatutProposition? statut;
final String? proposantId;
final bool? disponible;
final String? motCle;
final int page;
final int taille;
const RechercherPropositionsAideEvent({
this.organisationId,
this.typeAide,
this.statut,
this.proposantId,
this.disponible,
this.motCle,
this.page = 0,
this.taille = 20,
});
@override
List<Object?> get props => [
organisationId,
typeAide,
statut,
proposantId,
disponible,
motCle,
page,
taille,
];
}
/// Événement pour charger mes propositions
class ChargerMesPropositionsEvent extends PropositionsAideEvent {
final String utilisateurId;
const ChargerMesPropositionsEvent({required this.utilisateurId});
@override
List<Object> get props => [utilisateurId];
}
/// Événement pour charger les propositions disponibles
class ChargerPropositionsDisponiblesEvent extends PropositionsAideEvent {
final String organisationId;
final TypeAide? typeAide;
const ChargerPropositionsDisponiblesEvent({
required this.organisationId,
this.typeAide,
});
@override
List<Object?> get props => [organisationId, typeAide];
}
/// Événement pour filtrer les propositions localement
class FiltrerPropositionsAideEvent extends PropositionsAideEvent {
final TypeAide? typeAide;
final StatutProposition? statut;
final bool? disponible;
final String? motCle;
final double? capaciteMin;
final double? capaciteMax;
const FiltrerPropositionsAideEvent({
this.typeAide,
this.statut,
this.disponible,
this.motCle,
this.capaciteMin,
this.capaciteMax,
});
@override
List<Object?> get props => [
typeAide,
statut,
disponible,
motCle,
capaciteMin,
capaciteMax,
];
}
/// Événement pour trier les propositions
class TrierPropositionsAideEvent extends PropositionsAideEvent {
final TriPropositions critere;
final bool croissant;
const TrierPropositionsAideEvent({
required this.critere,
this.croissant = true,
});
@override
List<Object> get props => [critere, croissant];
}
/// Événement pour rafraîchir les propositions
class RafraichirPropositionsAideEvent extends PropositionsAideEvent {
const RafraichirPropositionsAideEvent();
}
/// Événement pour réinitialiser l'état
class ReinitialiserPropositionsAideEvent extends PropositionsAideEvent {
const ReinitialiserPropositionsAideEvent();
}
/// Événement pour sélectionner/désélectionner une proposition
class SelectionnerPropositionAideEvent extends PropositionsAideEvent {
final String propositionId;
final bool selectionne;
const SelectionnerPropositionAideEvent({
required this.propositionId,
required this.selectionne,
});
@override
List<Object> get props => [propositionId, selectionne];
}
/// Événement pour sélectionner/désélectionner toutes les propositions
class SelectionnerToutesPropositionsAideEvent extends PropositionsAideEvent {
final bool selectionne;
const SelectionnerToutesPropositionsAideEvent({required this.selectionne});
@override
List<Object> get props => [selectionne];
}
/// Événement pour supprimer des propositions sélectionnées
class SupprimerPropositionsSelectionnees extends PropositionsAideEvent {
final List<String> propositionIds;
const SupprimerPropositionsSelectionnees({required this.propositionIds});
@override
List<Object> get props => [propositionIds];
}
/// Événement pour exporter des propositions
class ExporterPropositionsAideEvent extends PropositionsAideEvent {
final List<String> propositionIds;
final FormatExport format;
const ExporterPropositionsAideEvent({
required this.propositionIds,
required this.format,
});
@override
List<Object> get props => [propositionIds, format];
}
/// Événement pour calculer la compatibilité avec une demande
class CalculerCompatibiliteEvent extends PropositionsAideEvent {
final String propositionId;
final String demandeId;
const CalculerCompatibiliteEvent({
required this.propositionId,
required this.demandeId,
});
@override
List<Object> get props => [propositionId, demandeId];
}
/// Événement pour obtenir les statistiques d'une proposition
class ObtenirStatistiquesPropositionEvent extends PropositionsAideEvent {
final String propositionId;
const ObtenirStatistiquesPropositionEvent({required this.propositionId});
@override
List<Object> get props => [propositionId];
}
/// Énumération pour les critères de tri
enum TriPropositions {
dateCreation,
dateModification,
titre,
statut,
capacite,
proposant,
scoreCompatibilite,
nombreMatches,
}
/// Énumération pour les formats d'export
enum FormatExport {
pdf,
excel,
csv,
json,
}
/// Extension pour obtenir le libellé des critères de tri
extension TriPropositionsExtension on TriPropositions {
String get libelle {
switch (this) {
case TriPropositions.dateCreation:
return 'Date de création';
case TriPropositions.dateModification:
return 'Date de modification';
case TriPropositions.titre:
return 'Titre';
case TriPropositions.statut:
return 'Statut';
case TriPropositions.capacite:
return 'Capacité';
case TriPropositions.proposant:
return 'Proposant';
case TriPropositions.scoreCompatibilite:
return 'Score de compatibilité';
case TriPropositions.nombreMatches:
return 'Nombre de matches';
}
}
String get icone {
switch (this) {
case TriPropositions.dateCreation:
return 'calendar_today';
case TriPropositions.dateModification:
return 'update';
case TriPropositions.titre:
return 'title';
case TriPropositions.statut:
return 'flag';
case TriPropositions.capacite:
return 'trending_up';
case TriPropositions.proposant:
return 'person';
case TriPropositions.scoreCompatibilite:
return 'star';
case TriPropositions.nombreMatches:
return 'link';
}
}
}
/// Extension pour obtenir le libellé des formats d'export
extension FormatExportExtension on FormatExport {
String get libelle {
switch (this) {
case FormatExport.pdf:
return 'PDF';
case FormatExport.excel:
return 'Excel';
case FormatExport.csv:
return 'CSV';
case FormatExport.json:
return 'JSON';
}
}
String get extension {
switch (this) {
case FormatExport.pdf:
return '.pdf';
case FormatExport.excel:
return '.xlsx';
case FormatExport.csv:
return '.csv';
case FormatExport.json:
return '.json';
}
}
String get mimeType {
switch (this) {
case FormatExport.pdf:
return 'application/pdf';
case FormatExport.excel:
return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
case FormatExport.csv:
return 'text/csv';
case FormatExport.json:
return 'application/json';
}
}
}

View File

@@ -0,0 +1,445 @@
import 'package:equatable/equatable.dart';
import '../../../domain/entities/proposition_aide.dart';
import 'propositions_aide_event.dart';
/// États pour la gestion des propositions d'aide
///
/// Ces états représentent tous les états possibles
/// de l'interface utilisateur pour les propositions d'aide.
abstract class PropositionsAideState extends Equatable {
const PropositionsAideState();
@override
List<Object?> get props => [];
}
/// État initial
class PropositionsAideInitial extends PropositionsAideState {
const PropositionsAideInitial();
}
/// État de chargement
class PropositionsAideLoading extends PropositionsAideState {
final bool isRefreshing;
final bool isLoadingMore;
const PropositionsAideLoading({
this.isRefreshing = false,
this.isLoadingMore = false,
});
@override
List<Object> get props => [isRefreshing, isLoadingMore];
}
/// État de succès avec données chargées
class PropositionsAideLoaded extends PropositionsAideState {
final List<PropositionAide> propositions;
final List<PropositionAide> propositionsFiltrees;
final bool hasReachedMax;
final int currentPage;
final int totalElements;
final Map<String, bool> propositionsSelectionnees;
final TriPropositions? criterieTri;
final bool triCroissant;
final FiltresPropositionsAide filtres;
final bool isRefreshing;
final bool isLoadingMore;
final DateTime lastUpdated;
const PropositionsAideLoaded({
required this.propositions,
required this.propositionsFiltrees,
this.hasReachedMax = false,
this.currentPage = 0,
this.totalElements = 0,
this.propositionsSelectionnees = const {},
this.criterieTri,
this.triCroissant = true,
this.filtres = const FiltresPropositionsAide(),
this.isRefreshing = false,
this.isLoadingMore = false,
required this.lastUpdated,
});
@override
List<Object?> get props => [
propositions,
propositionsFiltrees,
hasReachedMax,
currentPage,
totalElements,
propositionsSelectionnees,
criterieTri,
triCroissant,
filtres,
isRefreshing,
isLoadingMore,
lastUpdated,
];
/// Copie l'état avec de nouvelles valeurs
PropositionsAideLoaded copyWith({
List<PropositionAide>? propositions,
List<PropositionAide>? propositionsFiltrees,
bool? hasReachedMax,
int? currentPage,
int? totalElements,
Map<String, bool>? propositionsSelectionnees,
TriPropositions? criterieTri,
bool? triCroissant,
FiltresPropositionsAide? filtres,
bool? isRefreshing,
bool? isLoadingMore,
DateTime? lastUpdated,
}) {
return PropositionsAideLoaded(
propositions: propositions ?? this.propositions,
propositionsFiltrees: propositionsFiltrees ?? this.propositionsFiltrees,
hasReachedMax: hasReachedMax ?? this.hasReachedMax,
currentPage: currentPage ?? this.currentPage,
totalElements: totalElements ?? this.totalElements,
propositionsSelectionnees: propositionsSelectionnees ?? this.propositionsSelectionnees,
criterieTri: criterieTri ?? this.criterieTri,
triCroissant: triCroissant ?? this.triCroissant,
filtres: filtres ?? this.filtres,
isRefreshing: isRefreshing ?? this.isRefreshing,
isLoadingMore: isLoadingMore ?? this.isLoadingMore,
lastUpdated: lastUpdated ?? this.lastUpdated,
);
}
/// Obtient le nombre de propositions sélectionnées
int get nombrePropositionsSelectionnees {
return propositionsSelectionnees.values.where((selected) => selected).length;
}
/// Vérifie si toutes les propositions sont sélectionnées
bool get toutesPropositionsSelectionnees {
if (propositionsFiltrees.isEmpty) return false;
return propositionsFiltrees.every((proposition) =>
propositionsSelectionnees[proposition.id] == true
);
}
/// Obtient les IDs des propositions sélectionnées
List<String> get propositionsSelectionneesIds {
return propositionsSelectionnees.entries
.where((entry) => entry.value)
.map((entry) => entry.key)
.toList();
}
/// Obtient les propositions sélectionnées
List<PropositionAide> get propositionsSelectionneesEntities {
return propositions.where((proposition) =>
propositionsSelectionnees[proposition.id] == true
).toList();
}
/// Vérifie si des données sont disponibles
bool get hasData => propositions.isNotEmpty;
/// Vérifie si des filtres sont appliqués
bool get hasFiltres => !filtres.isEmpty;
/// Obtient le texte de statut
String get statusText {
if (isRefreshing) return 'Actualisation...';
if (isLoadingMore) return 'Chargement...';
if (propositionsFiltrees.isEmpty && hasData) return 'Aucun résultat pour les filtres appliqués';
if (propositionsFiltrees.isEmpty) return 'Aucune proposition d\'aide';
return '${propositionsFiltrees.length} proposition${propositionsFiltrees.length > 1 ? 's' : ''}';
}
/// Obtient le nombre de propositions disponibles
int get nombrePropositionsDisponibles {
return propositionsFiltrees.where((p) => p.estDisponible).length;
}
/// Obtient la capacité totale disponible
double get capaciteTotaleDisponible {
return propositionsFiltrees
.where((p) => p.estDisponible)
.fold(0.0, (sum, p) => sum + (p.capaciteMaximale ?? 0.0));
}
}
/// État d'erreur
class PropositionsAideError extends PropositionsAideState {
final String message;
final String? code;
final bool isNetworkError;
final bool canRetry;
final List<PropositionAide>? cachedData;
const PropositionsAideError({
required this.message,
this.code,
this.isNetworkError = false,
this.canRetry = true,
this.cachedData,
});
@override
List<Object?> get props => [
message,
code,
isNetworkError,
canRetry,
cachedData,
];
/// Vérifie si des données en cache sont disponibles
bool get hasCachedData => cachedData != null && cachedData!.isNotEmpty;
}
/// État de succès pour une opération spécifique
class PropositionsAideOperationSuccess extends PropositionsAideState {
final String message;
final PropositionAide? proposition;
final TypeOperationProposition operation;
const PropositionsAideOperationSuccess({
required this.message,
this.proposition,
required this.operation,
});
@override
List<Object?> get props => [message, proposition, operation];
}
/// État de compatibilité calculée
class PropositionsAideCompatibilite extends PropositionsAideState {
final String propositionId;
final String demandeId;
final double scoreCompatibilite;
final Map<String, dynamic> detailsCompatibilite;
const PropositionsAideCompatibilite({
required this.propositionId,
required this.demandeId,
required this.scoreCompatibilite,
required this.detailsCompatibilite,
});
@override
List<Object> get props => [propositionId, demandeId, scoreCompatibilite, detailsCompatibilite];
}
/// État des statistiques d'une proposition
class PropositionsAideStatistiques extends PropositionsAideState {
final String propositionId;
final Map<String, dynamic> statistiques;
const PropositionsAideStatistiques({
required this.propositionId,
required this.statistiques,
});
@override
List<Object> get props => [propositionId, statistiques];
}
/// État d'export
class PropositionsAideExporting extends PropositionsAideState {
final double progress;
final String? currentStep;
const PropositionsAideExporting({
required this.progress,
this.currentStep,
});
@override
List<Object?> get props => [progress, currentStep];
}
/// État d'export terminé
class PropositionsAideExported extends PropositionsAideState {
final String filePath;
final FormatExport format;
final int nombrePropositions;
const PropositionsAideExported({
required this.filePath,
required this.format,
required this.nombrePropositions,
});
@override
List<Object> get props => [filePath, format, nombrePropositions];
}
/// Classe pour les filtres des propositions d'aide
class FiltresPropositionsAide extends Equatable {
final TypeAide? typeAide;
final StatutProposition? statut;
final bool? disponible;
final String? motCle;
final String? organisationId;
final String? proposantId;
final DateTime? dateDebutCreation;
final DateTime? dateFinCreation;
final double? capaciteMin;
final double? capaciteMax;
const FiltresPropositionsAide({
this.typeAide,
this.statut,
this.disponible,
this.motCle,
this.organisationId,
this.proposantId,
this.dateDebutCreation,
this.dateFinCreation,
this.capaciteMin,
this.capaciteMax,
});
@override
List<Object?> get props => [
typeAide,
statut,
disponible,
motCle,
organisationId,
proposantId,
dateDebutCreation,
dateFinCreation,
capaciteMin,
capaciteMax,
];
/// Copie les filtres avec de nouvelles valeurs
FiltresPropositionsAide copyWith({
TypeAide? typeAide,
StatutProposition? statut,
bool? disponible,
String? motCle,
String? organisationId,
String? proposantId,
DateTime? dateDebutCreation,
DateTime? dateFinCreation,
double? capaciteMin,
double? capaciteMax,
}) {
return FiltresPropositionsAide(
typeAide: typeAide ?? this.typeAide,
statut: statut ?? this.statut,
disponible: disponible ?? this.disponible,
motCle: motCle ?? this.motCle,
organisationId: organisationId ?? this.organisationId,
proposantId: proposantId ?? this.proposantId,
dateDebutCreation: dateDebutCreation ?? this.dateDebutCreation,
dateFinCreation: dateFinCreation ?? this.dateFinCreation,
capaciteMin: capaciteMin ?? this.capaciteMin,
capaciteMax: capaciteMax ?? this.capaciteMax,
);
}
/// Réinitialise tous les filtres
FiltresPropositionsAide clear() {
return const FiltresPropositionsAide();
}
/// Vérifie si les filtres sont vides
bool get isEmpty {
return typeAide == null &&
statut == null &&
disponible == null &&
(motCle == null || motCle!.isEmpty) &&
organisationId == null &&
proposantId == null &&
dateDebutCreation == null &&
dateFinCreation == null &&
capaciteMin == null &&
capaciteMax == null;
}
/// Obtient le nombre de filtres actifs
int get nombreFiltresActifs {
int count = 0;
if (typeAide != null) count++;
if (statut != null) count++;
if (disponible != null) count++;
if (motCle != null && motCle!.isNotEmpty) count++;
if (organisationId != null) count++;
if (proposantId != null) count++;
if (dateDebutCreation != null) count++;
if (dateFinCreation != null) count++;
if (capaciteMin != null) count++;
if (capaciteMax != null) count++;
return count;
}
/// Obtient une description textuelle des filtres
String get description {
final parts = <String>[];
if (typeAide != null) parts.add('Type: ${typeAide!.libelle}');
if (statut != null) parts.add('Statut: ${statut!.libelle}');
if (disponible == true) parts.add('Disponible uniquement');
if (disponible == false) parts.add('Non disponible uniquement');
if (motCle != null && motCle!.isNotEmpty) parts.add('Recherche: "$motCle"');
if (capaciteMin != null || capaciteMax != null) {
if (capaciteMin != null && capaciteMax != null) {
parts.add('Capacité: ${capaciteMin!.toInt()} - ${capaciteMax!.toInt()}');
} else if (capaciteMin != null) {
parts.add('Capacité min: ${capaciteMin!.toInt()}');
} else {
parts.add('Capacité max: ${capaciteMax!.toInt()}');
}
}
return parts.join(', ');
}
}
/// Énumération pour les types d'opération
enum TypeOperationProposition {
creation,
modification,
activation,
desactivation,
suppression,
export,
}
/// Extension pour obtenir le libellé des opérations
extension TypeOperationPropositionExtension on TypeOperationProposition {
String get libelle {
switch (this) {
case TypeOperationProposition.creation:
return 'Création';
case TypeOperationProposition.modification:
return 'Modification';
case TypeOperationProposition.activation:
return 'Activation';
case TypeOperationProposition.desactivation:
return 'Désactivation';
case TypeOperationProposition.suppression:
return 'Suppression';
case TypeOperationProposition.export:
return 'Export';
}
}
String get messageSucces {
switch (this) {
case TypeOperationProposition.creation:
return 'Proposition d\'aide créée avec succès';
case TypeOperationProposition.modification:
return 'Proposition d\'aide modifiée avec succès';
case TypeOperationProposition.activation:
return 'Proposition d\'aide activée avec succès';
case TypeOperationProposition.desactivation:
return 'Proposition d\'aide désactivée avec succès';
case TypeOperationProposition.suppression:
return 'Proposition d\'aide supprimée avec succès';
case TypeOperationProposition.export:
return 'Export réalisé avec succès';
}
}
}

View File

@@ -0,0 +1,770 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../core/widgets/unified_page_layout.dart';
import '../../../../core/widgets/unified_card.dart';
import '../../../../core/theme/app_colors.dart';
import '../../../../core/theme/app_text_styles.dart';
import '../../../../core/utils/date_formatter.dart';
import '../../../../core/utils/currency_formatter.dart';
import '../../domain/entities/demande_aide.dart';
import '../bloc/demandes_aide/demandes_aide_bloc.dart';
import '../bloc/demandes_aide/demandes_aide_event.dart';
import '../bloc/demandes_aide/demandes_aide_state.dart';
import '../widgets/demande_aide_status_timeline.dart';
import '../widgets/demande_aide_evaluation_section.dart';
import '../widgets/demande_aide_documents_section.dart';
/// Page de détails d'une demande d'aide
///
/// Cette page affiche toutes les informations détaillées d'une demande d'aide
/// avec des sections organisées et des actions contextuelles.
class DemandeAideDetailsPage extends StatefulWidget {
final String demandeId;
const DemandeAideDetailsPage({
super.key,
required this.demandeId,
});
@override
State<DemandeAideDetailsPage> createState() => _DemandeAideDetailsPageState();
}
class _DemandeAideDetailsPageState extends State<DemandeAideDetailsPage> {
@override
void initState() {
super.initState();
// Charger les détails de la demande
context.read<DemandesAideBloc>().add(
ObtenirDemandeAideEvent(demandeId: widget.demandeId),
);
}
@override
Widget build(BuildContext context) {
return BlocConsumer<DemandesAideBloc, DemandesAideState>(
listener: (context, state) {
if (state is DemandesAideError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.message),
backgroundColor: AppColors.error,
),
);
} else if (state is DemandesAideOperationSuccess) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.message),
backgroundColor: AppColors.success,
),
);
}
},
builder: (context, state) {
if (state is DemandesAideLoading) {
return const UnifiedPageLayout(
title: 'Détails de la demande',
body: Center(child: CircularProgressIndicator()),
);
}
if (state is DemandesAideError && !state.hasCachedData) {
return UnifiedPageLayout(
title: 'Détails de la demande',
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error,
size: 64,
color: AppColors.error,
),
const SizedBox(height: 16),
Text(
state.message,
style: AppTextStyles.bodyLarge,
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
if (state.canRetry)
ElevatedButton(
onPressed: () => _rechargerDemande(),
child: const Text('Réessayer'),
),
],
),
),
);
}
// Trouver la demande dans l'état
DemandeAide? demande;
if (state is DemandesAideLoaded) {
demande = state.demandes.firstWhere(
(d) => d.id == widget.demandeId,
orElse: () => throw StateError('Demande non trouvée'),
);
}
if (demande == null) {
return const UnifiedPageLayout(
title: 'Détails de la demande',
body: Center(
child: Text('Demande d\'aide non trouvée'),
),
);
}
return UnifiedPageLayout(
title: 'Détails de la demande',
actions: _buildActions(demande),
body: RefreshIndicator(
onRefresh: () async => _rechargerDemande(),
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeaderSection(demande),
const SizedBox(height: 16),
_buildInfoGeneralesSection(demande),
const SizedBox(height: 16),
_buildDescriptionSection(demande),
const SizedBox(height: 16),
_buildBeneficiaireSection(demande),
const SizedBox(height: 16),
_buildContactUrgenceSection(demande),
const SizedBox(height: 16),
_buildLocalisationSection(demande),
const SizedBox(height: 16),
DemandeAideDocumentsSection(demande: demande),
const SizedBox(height: 16),
DemandeAideStatusTimeline(demande: demande),
const SizedBox(height: 16),
if (demande.evaluations.isNotEmpty)
DemandeAideEvaluationSection(demande: demande),
const SizedBox(height: 80), // Espace pour le FAB
],
),
),
),
floatingActionButton: _buildFloatingActionButton(demande),
);
},
);
}
List<Widget> _buildActions(DemandeAide demande) {
return [
PopupMenuButton<String>(
onSelected: (value) => _onMenuSelected(value, demande),
itemBuilder: (context) => [
const PopupMenuItem(
value: 'edit',
child: ListTile(
leading: Icon(Icons.edit),
title: Text('Modifier'),
dense: true,
),
),
if (demande.statut == StatutAide.brouillon)
const PopupMenuItem(
value: 'submit',
child: ListTile(
leading: Icon(Icons.send),
title: Text('Soumettre'),
dense: true,
),
),
if (demande.statut == StatutAide.soumise)
const PopupMenuItem(
value: 'evaluate',
child: ListTile(
leading: Icon(Icons.rate_review),
title: Text('Évaluer'),
dense: true,
),
),
const PopupMenuItem(
value: 'share',
child: ListTile(
leading: Icon(Icons.share),
title: Text('Partager'),
dense: true,
),
),
const PopupMenuItem(
value: 'export',
child: ListTile(
leading: Icon(Icons.file_download),
title: Text('Exporter'),
dense: true,
),
),
if (demande.statut == StatutAide.brouillon)
const PopupMenuItem(
value: 'delete',
child: ListTile(
leading: Icon(Icons.delete, color: AppColors.error),
title: Text('Supprimer', style: TextStyle(color: AppColors.error)),
dense: true,
),
),
],
),
];
}
Widget _buildFloatingActionButton(DemandeAide demande) {
if (demande.statut == StatutAide.brouillon) {
return FloatingActionButton.extended(
onPressed: () => _soumettredemande(demande),
icon: const Icon(Icons.send),
label: const Text('Soumettre'),
backgroundColor: AppColors.primary,
);
}
if (demande.statut == StatutAide.soumise) {
return FloatingActionButton.extended(
onPressed: () => _evaluerDemande(demande),
icon: const Icon(Icons.rate_review),
label: const Text('Évaluer'),
backgroundColor: AppColors.warning,
);
}
return FloatingActionButton(
onPressed: () => _modifierDemande(demande),
child: const Icon(Icons.edit),
backgroundColor: AppColors.primary,
);
}
Widget _buildHeaderSection(DemandeAide demande) {
return UnifiedCard(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
demande.titre,
style: AppTextStyles.titleLarge.copyWith(
fontWeight: FontWeight.bold,
),
),
),
_buildStatutChip(demande.statut),
],
),
const SizedBox(height: 8),
Row(
children: [
Text(
demande.numeroReference,
style: AppTextStyles.bodyMedium.copyWith(
fontFamily: 'monospace',
color: AppColors.textSecondary,
),
),
const Spacer(),
if (demande.estUrgente)
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: AppColors.error.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.priority_high,
size: 16,
color: AppColors.error,
),
const SizedBox(width: 4),
Text(
'URGENT',
style: AppTextStyles.labelSmall.copyWith(
color: AppColors.error,
fontWeight: FontWeight.bold,
),
),
],
),
),
],
),
const SizedBox(height: 12),
_buildProgressBar(demande),
],
),
),
);
}
Widget _buildInfoGeneralesSection(DemandeAide demande) {
return UnifiedCard(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Informations générales',
style: AppTextStyles.titleMedium.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
_buildInfoRow('Type d\'aide', demande.typeAide.libelle, Icons.category),
_buildInfoRow('Priorité', demande.priorite.libelle, Icons.priority_high),
_buildInfoRow('Demandeur', demande.nomDemandeur, Icons.person),
if (demande.montantDemande != null)
_buildInfoRow(
'Montant demandé',
CurrencyFormatter.formatCFA(demande.montantDemande!),
Icons.attach_money,
),
if (demande.montantApprouve != null)
_buildInfoRow(
'Montant approuvé',
CurrencyFormatter.formatCFA(demande.montantApprouve!),
Icons.check_circle,
),
_buildInfoRow(
'Date de création',
DateFormatter.formatComplete(demande.dateCreation),
Icons.calendar_today,
),
if (demande.dateModification != demande.dateCreation)
_buildInfoRow(
'Dernière modification',
DateFormatter.formatComplete(demande.dateModification),
Icons.update,
),
if (demande.dateEcheance != null)
_buildInfoRow(
'Date d\'échéance',
DateFormatter.formatComplete(demande.dateEcheance!),
Icons.schedule,
),
],
),
),
);
}
Widget _buildDescriptionSection(DemandeAide demande) {
return UnifiedCard(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Description',
style: AppTextStyles.titleMedium.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
Text(
demande.description,
style: AppTextStyles.bodyMedium,
),
if (demande.justification != null) ...[
const SizedBox(height: 16),
Text(
'Justification',
style: AppTextStyles.titleSmall.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 8),
Text(
demande.justification!,
style: AppTextStyles.bodyMedium,
),
],
],
),
),
);
}
Widget _buildBeneficiaireSection(DemandeAide demande) {
if (demande.beneficiaires.isEmpty) return const SizedBox.shrink();
return UnifiedCard(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Bénéficiaires',
style: AppTextStyles.titleMedium.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
...demande.beneficiaires.map((beneficiaire) => Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
children: [
Icon(
Icons.person,
size: 20,
color: AppColors.primary,
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${beneficiaire.prenom} ${beneficiaire.nom}',
style: AppTextStyles.bodyMedium.copyWith(
fontWeight: FontWeight.w600,
),
),
if (beneficiaire.age != null)
Text(
'${beneficiaire.age} ans',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textSecondary,
),
),
],
),
),
],
),
)),
],
),
),
);
}
Widget _buildContactUrgenceSection(DemandeAide demande) {
if (demande.contactUrgence == null) return const SizedBox.shrink();
final contact = demande.contactUrgence!;
return UnifiedCard(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Contact d\'urgence',
style: AppTextStyles.titleMedium.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
_buildInfoRow('Nom', '${contact.prenom} ${contact.nom}', Icons.person),
_buildInfoRow('Téléphone', contact.telephone, Icons.phone),
if (contact.email != null)
_buildInfoRow('Email', contact.email!, Icons.email),
_buildInfoRow('Relation', contact.relation, Icons.family_restroom),
],
),
),
);
}
Widget _buildLocalisationSection(DemandeAide demande) {
if (demande.localisation == null) return const SizedBox.shrink();
final localisation = demande.localisation!;
return UnifiedCard(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
'Localisation',
style: AppTextStyles.titleMedium.copyWith(
fontWeight: FontWeight.bold,
),
),
const Spacer(),
IconButton(
onPressed: () => _ouvrirCarte(localisation),
icon: const Icon(Icons.map),
tooltip: 'Voir sur la carte',
),
],
),
const SizedBox(height: 12),
_buildInfoRow('Adresse', localisation.adresse, Icons.location_on),
if (localisation.ville != null)
_buildInfoRow('Ville', localisation.ville!, Icons.location_city),
if (localisation.codePostal != null)
_buildInfoRow('Code postal', localisation.codePostal!, Icons.markunread_mailbox),
if (localisation.pays != null)
_buildInfoRow('Pays', localisation.pays!, Icons.flag),
],
),
),
);
}
Widget _buildInfoRow(String label, String value, IconData icon) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
icon,
size: 20,
color: AppColors.primary,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textSecondary,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 2),
Text(
value,
style: AppTextStyles.bodyMedium,
),
],
),
),
],
),
);
}
Widget _buildStatutChip(StatutAide statut) {
final color = _getStatutColor(statut);
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(16),
),
child: Text(
statut.libelle,
style: AppTextStyles.labelMedium.copyWith(
color: color,
fontWeight: FontWeight.w600,
),
),
);
}
Widget _buildProgressBar(DemandeAide demande) {
final progress = demande.pourcentageAvancement;
final color = _getProgressColor(progress);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Avancement',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textSecondary,
fontWeight: FontWeight.w500,
),
),
Text(
'${progress.toInt()}%',
style: AppTextStyles.bodySmall.copyWith(
color: color,
fontWeight: FontWeight.w600,
),
),
],
),
const SizedBox(height: 8),
LinearProgressIndicator(
value: progress / 100,
backgroundColor: AppColors.outline,
valueColor: AlwaysStoppedAnimation<Color>(color),
),
],
);
}
Color _getStatutColor(StatutAide statut) {
switch (statut) {
case StatutAide.brouillon:
return AppColors.textSecondary;
case StatutAide.soumise:
return AppColors.warning;
case StatutAide.enEvaluation:
return AppColors.info;
case StatutAide.approuvee:
return AppColors.success;
case StatutAide.rejetee:
return AppColors.error;
case StatutAide.enCours:
return AppColors.primary;
case StatutAide.terminee:
return AppColors.success;
case StatutAide.versee:
return AppColors.success;
case StatutAide.livree:
return AppColors.success;
case StatutAide.annulee:
return AppColors.error;
}
}
Color _getProgressColor(double progress) {
if (progress < 25) return AppColors.error;
if (progress < 50) return AppColors.warning;
if (progress < 75) return AppColors.info;
return AppColors.success;
}
void _rechargerDemande() {
context.read<DemandesAideBloc>().add(
ObtenirDemandeAideEvent(demandeId: widget.demandeId),
);
}
void _onMenuSelected(String value, DemandeAide demande) {
switch (value) {
case 'edit':
_modifierDemande(demande);
break;
case 'submit':
_soumettredemande(demande);
break;
case 'evaluate':
_evaluerDemande(demande);
break;
case 'share':
_partagerDemande(demande);
break;
case 'export':
_exporterDemande(demande);
break;
case 'delete':
_supprimerDemande(demande);
break;
}
}
void _modifierDemande(DemandeAide demande) {
Navigator.pushNamed(
context,
'/solidarite/demandes/modifier',
arguments: demande,
);
}
void _soumettredemande(DemandeAide demande) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Soumettre la demande'),
content: const Text(
'Êtes-vous sûr de vouloir soumettre cette demande d\'aide ? '
'Une fois soumise, elle ne pourra plus être modifiée.',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
context.read<DemandesAideBloc>().add(
SoumettreDemandeAideEvent(demandeId: demande.id),
);
},
child: const Text('Soumettre'),
),
],
),
);
}
void _evaluerDemande(DemandeAide demande) {
Navigator.pushNamed(
context,
'/solidarite/demandes/evaluer',
arguments: demande,
);
}
void _partagerDemande(DemandeAide demande) {
// Implémenter le partage
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Fonctionnalité de partage à implémenter')),
);
}
void _exporterDemande(DemandeAide demande) {
// Implémenter l'export
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Fonctionnalité d\'export à implémenter')),
);
}
void _supprimerDemande(DemandeAide demande) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Supprimer la demande'),
content: const Text(
'Êtes-vous sûr de vouloir supprimer cette demande d\'aide ? '
'Cette action est irréversible.',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
Navigator.pop(context); // Retour à la liste
context.read<DemandesAideBloc>().add(
SupprimerDemandesSelectionnees(demandeIds: [demande.id]),
);
},
style: ElevatedButton.styleFrom(backgroundColor: AppColors.error),
child: const Text('Supprimer'),
),
],
),
);
}
void _ouvrirCarte(Localisation localisation) {
// Implémenter l'ouverture de la carte
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Ouverture de la carte à implémenter')),
);
}
}

View File

@@ -0,0 +1,601 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../core/widgets/unified_page_layout.dart';
import '../../../../core/widgets/unified_card.dart';
import '../../../../core/theme/app_colors.dart';
import '../../../../core/theme/app_text_styles.dart';
import '../../../../core/utils/validators.dart';
import '../../domain/entities/demande_aide.dart';
import '../bloc/demandes_aide/demandes_aide_bloc.dart';
import '../bloc/demandes_aide/demandes_aide_event.dart';
import '../bloc/demandes_aide/demandes_aide_state.dart';
import '../widgets/demande_aide_form_sections.dart';
/// Page de formulaire pour créer ou modifier une demande d'aide
///
/// Cette page utilise un formulaire multi-sections avec validation
/// pour créer ou modifier une demande d'aide.
class DemandeAideFormPage extends StatefulWidget {
final DemandeAide? demandeExistante;
final bool isModification;
const DemandeAideFormPage({
super.key,
this.demandeExistante,
this.isModification = false,
});
@override
State<DemandeAideFormPage> createState() => _DemandeAideFormPageState();
}
class _DemandeAideFormPageState extends State<DemandeAideFormPage> {
final _formKey = GlobalKey<FormState>();
final _pageController = PageController();
// Controllers pour les champs de texte
final _titreController = TextEditingController();
final _descriptionController = TextEditingController();
final _justificationController = TextEditingController();
final _montantController = TextEditingController();
// Variables d'état du formulaire
TypeAide? _typeAide;
PrioriteAide _priorite = PrioriteAide.normale;
bool _estUrgente = false;
DateTime? _dateEcheance;
List<BeneficiaireAide> _beneficiaires = [];
ContactUrgence? _contactUrgence;
Localisation? _localisation;
List<PieceJustificative> _piecesJustificatives = [];
int _currentStep = 0;
final int _totalSteps = 5;
bool _isLoading = false;
@override
void initState() {
super.initState();
_initializeForm();
}
@override
void dispose() {
_titreController.dispose();
_descriptionController.dispose();
_justificationController.dispose();
_montantController.dispose();
_pageController.dispose();
super.dispose();
}
void _initializeForm() {
if (widget.demandeExistante != null) {
final demande = widget.demandeExistante!;
_titreController.text = demande.titre;
_descriptionController.text = demande.description;
_justificationController.text = demande.justification ?? '';
_montantController.text = demande.montantDemande?.toString() ?? '';
_typeAide = demande.typeAide;
_priorite = demande.priorite;
_estUrgente = demande.estUrgente;
_dateEcheance = demande.dateEcheance;
_beneficiaires = List.from(demande.beneficiaires);
_contactUrgence = demande.contactUrgence;
_localisation = demande.localisation;
_piecesJustificatives = List.from(demande.piecesJustificatives);
}
}
@override
Widget build(BuildContext context) {
return BlocConsumer<DemandesAideBloc, DemandesAideState>(
listener: (context, state) {
if (state is DemandesAideLoading) {
setState(() {
_isLoading = true;
});
} else {
setState(() {
_isLoading = false;
});
}
if (state is DemandesAideError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.message),
backgroundColor: AppColors.error,
),
);
} else if (state is DemandesAideOperationSuccess) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.message),
backgroundColor: AppColors.success,
),
);
Navigator.pop(context, true);
} else if (state is DemandesAideValidation) {
if (!state.isValid) {
_showValidationErrors(state.erreurs);
}
}
},
builder: (context, state) {
return UnifiedPageLayout(
title: widget.isModification ? 'Modifier la demande' : 'Nouvelle demande',
actions: [
if (_currentStep > 0)
IconButton(
onPressed: _previousStep,
icon: const Icon(Icons.arrow_back),
tooltip: 'Étape précédente',
),
IconButton(
onPressed: _saveDraft,
icon: const Icon(Icons.save),
tooltip: 'Sauvegarder le brouillon',
),
],
body: Column(
children: [
_buildProgressIndicator(),
Expanded(
child: Form(
key: _formKey,
child: PageView(
controller: _pageController,
physics: const NeverScrollableScrollPhysics(),
children: [
_buildStep1InfoGenerales(),
_buildStep2Beneficiaires(),
_buildStep3Contact(),
_buildStep4Localisation(),
_buildStep5Documents(),
],
),
),
),
_buildBottomActions(),
],
),
);
},
);
}
Widget _buildProgressIndicator() {
return Container(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Row(
children: List.generate(_totalSteps, (index) {
final isActive = index == _currentStep;
final isCompleted = index < _currentStep;
return Expanded(
child: Container(
height: 4,
margin: EdgeInsets.only(right: index < _totalSteps - 1 ? 8 : 0),
decoration: BoxDecoration(
color: isCompleted || isActive
? AppColors.primary
: AppColors.outline,
borderRadius: BorderRadius.circular(2),
),
),
);
}),
),
const SizedBox(height: 8),
Text(
'Étape ${_currentStep + 1} sur $_totalSteps: ${_getStepTitle(_currentStep)}',
style: AppTextStyles.bodyMedium.copyWith(
color: AppColors.textSecondary,
),
),
],
),
);
}
Widget _buildStep1InfoGenerales() {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
UnifiedCard(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Informations générales',
style: AppTextStyles.titleMedium.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
TextFormField(
controller: _titreController,
decoration: const InputDecoration(
labelText: 'Titre de la demande *',
hintText: 'Ex: Aide pour frais médicaux',
border: OutlineInputBorder(),
),
validator: Validators.required,
maxLength: 100,
),
const SizedBox(height: 16),
DropdownButtonFormField<TypeAide>(
value: _typeAide,
decoration: const InputDecoration(
labelText: 'Type d\'aide *',
border: OutlineInputBorder(),
),
items: TypeAide.values.map((type) => DropdownMenuItem(
value: type,
child: Text(type.libelle),
)).toList(),
onChanged: (value) {
setState(() {
_typeAide = value;
});
},
validator: (value) => value == null ? 'Veuillez sélectionner un type d\'aide' : null,
),
const SizedBox(height: 16),
TextFormField(
controller: _descriptionController,
decoration: const InputDecoration(
labelText: 'Description détaillée *',
hintText: 'Décrivez votre situation et vos besoins...',
border: OutlineInputBorder(),
),
maxLines: 4,
validator: Validators.required,
maxLength: 1000,
),
const SizedBox(height: 16),
TextFormField(
controller: _justificationController,
decoration: const InputDecoration(
labelText: 'Justification',
hintText: 'Pourquoi cette aide est-elle nécessaire ?',
border: OutlineInputBorder(),
),
maxLines: 3,
maxLength: 500,
),
],
),
),
),
const SizedBox(height: 16),
UnifiedCard(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Détails de la demande',
style: AppTextStyles.titleMedium.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
TextFormField(
controller: _montantController,
decoration: const InputDecoration(
labelText: 'Montant demandé (FCFA)',
hintText: '0',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.attach_money),
),
keyboardType: TextInputType.number,
validator: (value) {
if (value != null && value.isNotEmpty) {
final montant = double.tryParse(value);
if (montant == null || montant <= 0) {
return 'Veuillez saisir un montant valide';
}
}
return null;
},
),
const SizedBox(height: 16),
DropdownButtonFormField<PrioriteAide>(
value: _priorite,
decoration: const InputDecoration(
labelText: 'Priorité',
border: OutlineInputBorder(),
),
items: PrioriteAide.values.map((priorite) => DropdownMenuItem(
value: priorite,
child: Text(priorite.libelle),
)).toList(),
onChanged: (value) {
setState(() {
_priorite = value ?? PrioriteAide.normale;
});
},
),
const SizedBox(height: 16),
SwitchListTile(
title: const Text('Demande urgente'),
subtitle: const Text('Cette demande nécessite un traitement prioritaire'),
value: _estUrgente,
onChanged: (value) {
setState(() {
_estUrgente = value;
});
},
),
const SizedBox(height: 16),
ListTile(
title: const Text('Date d\'échéance'),
subtitle: Text(_dateEcheance != null
? '${_dateEcheance!.day}/${_dateEcheance!.month}/${_dateEcheance!.year}'
: 'Aucune date limite'),
trailing: const Icon(Icons.calendar_today),
onTap: _selectDateEcheance,
),
],
),
),
),
],
),
);
}
Widget _buildStep2Beneficiaires() {
return DemandeAideFormBeneficiairesSection(
beneficiaires: _beneficiaires,
onBeneficiairesChanged: (beneficiaires) {
setState(() {
_beneficiaires = beneficiaires;
});
},
);
}
Widget _buildStep3Contact() {
return DemandeAideFormContactSection(
contactUrgence: _contactUrgence,
onContactChanged: (contact) {
setState(() {
_contactUrgence = contact;
});
},
);
}
Widget _buildStep4Localisation() {
return DemandeAideFormLocalisationSection(
localisation: _localisation,
onLocalisationChanged: (localisation) {
setState(() {
_localisation = localisation;
});
},
);
}
Widget _buildStep5Documents() {
return DemandeAideFormDocumentsSection(
piecesJustificatives: _piecesJustificatives,
onDocumentsChanged: (documents) {
setState(() {
_piecesJustificatives = documents;
});
},
);
}
Widget _buildBottomActions() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.surface,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, -2),
),
],
),
child: Row(
children: [
if (_currentStep > 0)
Expanded(
child: OutlinedButton(
onPressed: _isLoading ? null : _previousStep,
child: const Text('Précédent'),
),
),
if (_currentStep > 0) const SizedBox(width: 16),
Expanded(
child: ElevatedButton(
onPressed: _isLoading ? null : _nextStepOrSubmit,
child: _isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: Text(_currentStep < _totalSteps - 1 ? 'Suivant' : 'Créer la demande'),
),
),
],
),
);
}
String _getStepTitle(int step) {
switch (step) {
case 0:
return 'Informations générales';
case 1:
return 'Bénéficiaires';
case 2:
return 'Contact d\'urgence';
case 3:
return 'Localisation';
case 4:
return 'Documents';
default:
return '';
}
}
void _previousStep() {
if (_currentStep > 0) {
setState(() {
_currentStep--;
});
_pageController.previousPage(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}
}
void _nextStepOrSubmit() {
if (_validateCurrentStep()) {
if (_currentStep < _totalSteps - 1) {
setState(() {
_currentStep++;
});
_pageController.nextPage(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
} else {
_submitForm();
}
}
}
bool _validateCurrentStep() {
switch (_currentStep) {
case 0:
return _formKey.currentState?.validate() ?? false;
case 1:
// Validation des bénéficiaires (optionnel)
return true;
case 2:
// Validation du contact d'urgence (optionnel)
return true;
case 3:
// Validation de la localisation (optionnel)
return true;
case 4:
// Validation des documents (optionnel)
return true;
default:
return true;
}
}
void _submitForm() {
if (!_formKey.currentState!.validate()) {
return;
}
final demande = DemandeAide(
id: widget.demandeExistante?.id ?? '',
numeroReference: widget.demandeExistante?.numeroReference ?? '',
titre: _titreController.text,
description: _descriptionController.text,
justification: _justificationController.text.isEmpty ? null : _justificationController.text,
typeAide: _typeAide!,
statut: widget.demandeExistante?.statut ?? StatutAide.brouillon,
priorite: _priorite,
estUrgente: _estUrgente,
montantDemande: _montantController.text.isEmpty ? null : double.tryParse(_montantController.text),
montantApprouve: widget.demandeExistante?.montantApprouve,
dateCreation: widget.demandeExistante?.dateCreation ?? DateTime.now(),
dateModification: DateTime.now(),
dateEcheance: _dateEcheance,
organisationId: widget.demandeExistante?.organisationId ?? '',
demandeurId: widget.demandeExistante?.demandeurId ?? '',
nomDemandeur: widget.demandeExistante?.nomDemandeur ?? '',
emailDemandeur: widget.demandeExistante?.emailDemandeur ?? '',
telephoneDemandeur: widget.demandeExistante?.telephoneDemandeur ?? '',
beneficiaires: _beneficiaires,
contactUrgence: _contactUrgence,
localisation: _localisation,
piecesJustificatives: _piecesJustificatives,
evaluations: widget.demandeExistante?.evaluations ?? [],
commentairesInternes: widget.demandeExistante?.commentairesInternes ?? [],
historiqueStatuts: widget.demandeExistante?.historiqueStatuts ?? [],
tags: widget.demandeExistante?.tags ?? [],
metadonnees: widget.demandeExistante?.metadonnees ?? {},
);
if (widget.isModification) {
context.read<DemandesAideBloc>().add(
MettreAJourDemandeAideEvent(demande: demande),
);
} else {
context.read<DemandesAideBloc>().add(
CreerDemandeAideEvent(demande: demande),
);
}
}
void _saveDraft() {
// Sauvegarder le brouillon
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Brouillon sauvegardé'),
backgroundColor: AppColors.success,
),
);
}
void _selectDateEcheance() async {
final date = await showDatePicker(
context: context,
initialDate: _dateEcheance ?? DateTime.now().add(const Duration(days: 30)),
firstDate: DateTime.now(),
lastDate: DateTime.now().add(const Duration(days: 365)),
);
if (date != null) {
setState(() {
_dateEcheance = date;
});
}
}
void _showValidationErrors(Map<String, String> erreurs) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Erreurs de validation'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: erreurs.entries.map((entry) => Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text('${entry.value}'),
)).toList(),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('OK'),
),
],
),
);
}
}

View File

@@ -0,0 +1,676 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../core/widgets/unified_page_layout.dart';
import '../../../../core/widgets/unified_card.dart';
import '../../../../core/widgets/unified_list_widget.dart';
import '../../../../core/theme/app_colors.dart';
import '../../../../core/theme/app_text_styles.dart';
import '../../../../core/utils/date_formatter.dart';
import '../../../../core/utils/currency_formatter.dart';
import '../../domain/entities/demande_aide.dart';
import '../bloc/demandes_aide/demandes_aide_bloc.dart';
import '../bloc/demandes_aide/demandes_aide_event.dart';
import '../bloc/demandes_aide/demandes_aide_state.dart';
import '../widgets/demande_aide_card.dart';
import '../widgets/demandes_aide_filter_bottom_sheet.dart';
import '../widgets/demandes_aide_sort_bottom_sheet.dart';
/// Page principale pour afficher la liste des demandes d'aide
///
/// Cette page utilise le pattern BLoC pour gérer l'état et affiche
/// une liste paginée des demandes d'aide avec des fonctionnalités
/// de filtrage, tri, recherche et sélection multiple.
class DemandesAidePage extends StatefulWidget {
final String? organisationId;
final TypeAide? typeAideInitial;
final StatutAide? statutInitial;
const DemandesAidePage({
super.key,
this.organisationId,
this.typeAideInitial,
this.statutInitial,
});
@override
State<DemandesAidePage> createState() => _DemandesAidePageState();
}
class _DemandesAidePageState extends State<DemandesAidePage> {
final ScrollController _scrollController = ScrollController();
final TextEditingController _searchController = TextEditingController();
bool _isSelectionMode = false;
@override
void initState() {
super.initState();
_scrollController.addListener(_onScroll);
// Charger les demandes d'aide au démarrage
context.read<DemandesAideBloc>().add(ChargerDemandesAideEvent(
organisationId: widget.organisationId,
typeAide: widget.typeAideInitial,
statut: widget.statutInitial,
));
}
@override
void dispose() {
_scrollController.dispose();
_searchController.dispose();
super.dispose();
}
void _onScroll() {
if (_isBottom) {
context.read<DemandesAideBloc>().add(const ChargerPlusDemandesAideEvent());
}
}
bool get _isBottom {
if (!_scrollController.hasClients) return false;
final maxScroll = _scrollController.position.maxScrollExtent;
final currentScroll = _scrollController.offset;
return currentScroll >= (maxScroll * 0.9);
}
@override
Widget build(BuildContext context) {
return BlocConsumer<DemandesAideBloc, DemandesAideState>(
listener: (context, state) {
if (state is DemandesAideError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.message),
backgroundColor: AppColors.error,
action: state.canRetry
? SnackBarAction(
label: 'Réessayer',
textColor: Colors.white,
onPressed: () => _rafraichir(),
)
: null,
),
);
} else if (state is DemandesAideOperationSuccess) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.message),
backgroundColor: AppColors.success,
),
);
} else if (state is DemandesAideExported) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Fichier exporté: ${state.filePath}'),
backgroundColor: AppColors.success,
action: SnackBarAction(
label: 'Ouvrir',
textColor: Colors.white,
onPressed: () => _ouvrirFichier(state.filePath),
),
),
);
}
},
builder: (context, state) {
return UnifiedPageLayout(
title: 'Demandes d\'aide',
showBackButton: false,
actions: _buildActions(state),
floatingActionButton: _buildFloatingActionButton(),
body: Column(
children: [
_buildSearchBar(state),
_buildFilterChips(state),
Expanded(child: _buildContent(state)),
],
),
);
},
);
}
List<Widget> _buildActions(DemandesAideState state) {
final actions = <Widget>[];
if (_isSelectionMode && state is DemandesAideLoaded) {
// Actions en mode sélection
actions.addAll([
IconButton(
icon: const Icon(Icons.select_all),
onPressed: () => _toggleSelectAll(state),
tooltip: state.toutesDemandesSelectionnees
? 'Désélectionner tout'
: 'Sélectionner tout',
),
IconButton(
icon: const Icon(Icons.delete),
onPressed: state.nombreDemandesSelectionnees > 0
? () => _supprimerSelection(state)
: null,
tooltip: 'Supprimer la sélection',
),
IconButton(
icon: const Icon(Icons.file_download),
onPressed: state.nombreDemandesSelectionnees > 0
? () => _exporterSelection(state)
: null,
tooltip: 'Exporter la sélection',
),
IconButton(
icon: const Icon(Icons.close),
onPressed: _quitterModeSelection,
tooltip: 'Quitter la sélection',
),
]);
} else {
// Actions normales
actions.addAll([
IconButton(
icon: const Icon(Icons.filter_list),
onPressed: () => _afficherFiltres(state),
tooltip: 'Filtrer',
),
IconButton(
icon: const Icon(Icons.sort),
onPressed: () => _afficherTri(state),
tooltip: 'Trier',
),
PopupMenuButton<String>(
icon: const Icon(Icons.more_vert),
onSelected: (value) => _onMenuSelected(value, state),
itemBuilder: (context) => [
const PopupMenuItem(
value: 'refresh',
child: ListTile(
leading: Icon(Icons.refresh),
title: Text('Actualiser'),
dense: true,
),
),
const PopupMenuItem(
value: 'select',
child: ListTile(
leading: Icon(Icons.checklist),
title: Text('Sélection multiple'),
dense: true,
),
),
const PopupMenuItem(
value: 'export_all',
child: ListTile(
leading: Icon(Icons.file_download),
title: Text('Exporter tout'),
dense: true,
),
),
const PopupMenuItem(
value: 'urgentes',
child: ListTile(
leading: Icon(Icons.priority_high, color: AppColors.error),
title: Text('Demandes urgentes'),
dense: true,
),
),
],
),
]);
}
return actions;
}
Widget _buildFloatingActionButton() {
return FloatingActionButton.extended(
onPressed: _creerNouvelleDemande,
icon: const Icon(Icons.add),
label: const Text('Nouvelle demande'),
backgroundColor: AppColors.primary,
);
}
Widget _buildSearchBar(DemandesAideState state) {
return Container(
padding: const EdgeInsets.all(16.0),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: 'Rechercher des demandes...',
prefixIcon: const Icon(Icons.search),
suffixIcon: _searchController.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_searchController.clear();
_rechercherDemandes('');
},
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
onChanged: _rechercherDemandes,
onSubmitted: _rechercherDemandes,
),
);
}
Widget _buildFilterChips(DemandesAideState state) {
if (state is! DemandesAideLoaded || !state.hasFiltres) {
return const SizedBox.shrink();
}
return Container(
height: 50,
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: ListView(
scrollDirection: Axis.horizontal,
children: [
if (state.filtres.typeAide != null)
_buildFilterChip(
'Type: ${state.filtres.typeAide!.libelle}',
() => _supprimerFiltre('typeAide'),
),
if (state.filtres.statut != null)
_buildFilterChip(
'Statut: ${state.filtres.statut!.libelle}',
() => _supprimerFiltre('statut'),
),
if (state.filtres.priorite != null)
_buildFilterChip(
'Priorité: ${state.filtres.priorite!.libelle}',
() => _supprimerFiltre('priorite'),
),
if (state.filtres.urgente == true)
_buildFilterChip(
'Urgente',
() => _supprimerFiltre('urgente'),
),
if (state.filtres.motCle != null && state.filtres.motCle!.isNotEmpty)
_buildFilterChip(
'Recherche: "${state.filtres.motCle}"',
() => _supprimerFiltre('motCle'),
),
ActionChip(
label: const Text('Effacer tout'),
onPressed: _effacerTousFiltres,
backgroundColor: AppColors.error.withOpacity(0.1),
labelStyle: TextStyle(color: AppColors.error),
),
],
),
);
}
Widget _buildFilterChip(String label, VoidCallback onDeleted) {
return Padding(
padding: const EdgeInsets.only(right: 8.0),
child: Chip(
label: Text(label),
onDeleted: onDeleted,
backgroundColor: AppColors.primary.withOpacity(0.1),
labelStyle: TextStyle(color: AppColors.primary),
),
);
}
Widget _buildContent(DemandesAideState state) {
if (state is DemandesAideInitial) {
return const Center(
child: Text('Appuyez sur actualiser pour charger les demandes'),
);
}
if (state is DemandesAideLoading && state.isRefreshing == false) {
return const Center(child: CircularProgressIndicator());
}
if (state is DemandesAideError && !state.hasCachedData) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
state.isNetworkError ? Icons.wifi_off : Icons.error,
size: 64,
color: AppColors.error,
),
const SizedBox(height: 16),
Text(
state.message,
style: AppTextStyles.bodyLarge,
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
if (state.canRetry)
ElevatedButton(
onPressed: _rafraichir,
child: const Text('Réessayer'),
),
],
),
);
}
if (state is DemandesAideExporting) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(value: state.progress),
const SizedBox(height: 16),
Text(
state.currentStep ?? 'Export en cours...',
style: AppTextStyles.bodyLarge,
),
const SizedBox(height: 8),
Text(
'${(state.progress * 100).toInt()}%',
style: AppTextStyles.bodyMedium,
),
],
),
);
}
if (state is DemandesAideLoaded) {
return _buildDemandesList(state);
}
return const SizedBox.shrink();
}
Widget _buildDemandesList(DemandesAideLoaded state) {
if (state.demandesFiltrees.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.inbox,
size: 64,
color: AppColors.textSecondary,
),
const SizedBox(height: 16),
Text(
state.hasData
? 'Aucun résultat pour les filtres appliqués'
: 'Aucune demande d\'aide',
style: AppTextStyles.bodyLarge,
textAlign: TextAlign.center,
),
if (state.hasFiltres) ...[
const SizedBox(height: 8),
TextButton(
onPressed: _effacerTousFiltres,
child: const Text('Effacer les filtres'),
),
],
],
),
);
}
return RefreshIndicator(
onRefresh: () async => _rafraichir(),
child: UnifiedListWidget<DemandeAide>(
items: state.demandesFiltrees,
itemBuilder: (context, demande, index) => DemandeAideCard(
demande: demande,
isSelected: state.demandesSelectionnees[demande.id] == true,
isSelectionMode: _isSelectionMode,
onTap: () => _onDemandeAideTap(demande),
onLongPress: () => _onDemandeAideLongPress(demande),
onSelectionChanged: (selected) => _onDemandeAideSelectionChanged(demande.id, selected),
),
scrollController: _scrollController,
hasReachedMax: state.hasReachedMax,
isLoading: state.isLoadingMore,
emptyWidget: const SizedBox.shrink(), // Géré plus haut
),
);
}
// Méthodes d'action
void _rafraichir() {
context.read<DemandesAideBloc>().add(const RafraichirDemandesAideEvent());
}
void _rechercherDemandes(String query) {
context.read<DemandesAideBloc>().add(FiltrerDemandesAideEvent(motCle: query));
}
void _afficherFiltres(DemandesAideState state) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) => DemandesAideFilterBottomSheet(
filtresActuels: state is DemandesAideLoaded ? state.filtres : const FiltresDemandesAide(),
onFiltresChanged: (filtres) {
context.read<DemandesAideBloc>().add(FiltrerDemandesAideEvent(
typeAide: filtres.typeAide,
statut: filtres.statut,
priorite: filtres.priorite,
urgente: filtres.urgente,
motCle: filtres.motCle,
));
},
),
);
}
void _afficherTri(DemandesAideState state) {
showModalBottomSheet(
context: context,
builder: (context) => DemandesAideSortBottomSheet(
critereActuel: state is DemandesAideLoaded ? state.criterieTri : null,
croissantActuel: state is DemandesAideLoaded ? state.triCroissant : true,
onTriChanged: (critere, croissant) {
context.read<DemandesAideBloc>().add(TrierDemandesAideEvent(
critere: critere,
croissant: croissant,
));
},
),
);
}
void _onMenuSelected(String value, DemandesAideState state) {
switch (value) {
case 'refresh':
_rafraichir();
break;
case 'select':
_activerModeSelection();
break;
case 'export_all':
if (state is DemandesAideLoaded) {
_exporterTout(state);
}
break;
case 'urgentes':
_afficherDemandesUrgentes();
break;
}
}
void _creerNouvelleDemande() {
Navigator.pushNamed(context, '/solidarite/demandes/creer');
}
void _onDemandeAideTap(DemandeAide demande) {
if (_isSelectionMode) {
_onDemandeAideSelectionChanged(
demande.id,
!(context.read<DemandesAideBloc>().state as DemandesAideLoaded)
.demandesSelectionnees[demande.id] == true,
);
} else {
Navigator.pushNamed(
context,
'/solidarite/demandes/details',
arguments: demande.id,
);
}
}
void _onDemandeAideLongPress(DemandeAide demande) {
if (!_isSelectionMode) {
_activerModeSelection();
_onDemandeAideSelectionChanged(demande.id, true);
}
}
void _onDemandeAideSelectionChanged(String demandeId, bool selected) {
context.read<DemandesAideBloc>().add(SelectionnerDemandeAideEvent(
demandeId: demandeId,
selectionne: selected,
));
}
void _activerModeSelection() {
setState(() {
_isSelectionMode = true;
});
}
void _quitterModeSelection() {
setState(() {
_isSelectionMode = false;
});
context.read<DemandesAideBloc>().add(const SelectionnerToutesDemandesAideEvent(selectionne: false));
}
void _toggleSelectAll(DemandesAideLoaded state) {
context.read<DemandesAideBloc>().add(SelectionnerToutesDemandesAideEvent(
selectionne: !state.toutesDemandesSelectionnees,
));
}
void _supprimerSelection(DemandesAideLoaded state) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Confirmer la suppression'),
content: Text(
'Êtes-vous sûr de vouloir supprimer ${state.nombreDemandesSelectionnees} demande(s) d\'aide ?',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
context.read<DemandesAideBloc>().add(SupprimerDemandesSelectionnees(
demandeIds: state.demandesSelectionneesIds,
));
_quitterModeSelection();
},
style: ElevatedButton.styleFrom(backgroundColor: AppColors.error),
child: const Text('Supprimer'),
),
],
),
);
}
void _exporterSelection(DemandesAideLoaded state) {
_afficherDialogueExport(state.demandesSelectionneesIds);
}
void _exporterTout(DemandesAideLoaded state) {
_afficherDialogueExport(state.demandesFiltrees.map((d) => d.id).toList());
}
void _afficherDialogueExport(List<String> demandeIds) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Exporter les demandes'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: FormatExport.values.map((format) => ListTile(
leading: Icon(_getFormatIcon(format)),
title: Text(format.libelle),
onTap: () {
Navigator.pop(context);
context.read<DemandesAideBloc>().add(ExporterDemandesAideEvent(
demandeIds: demandeIds,
format: format,
));
},
)).toList(),
),
),
);
}
IconData _getFormatIcon(FormatExport format) {
switch (format) {
case FormatExport.pdf:
return Icons.picture_as_pdf;
case FormatExport.excel:
return Icons.table_chart;
case FormatExport.csv:
return Icons.grid_on;
case FormatExport.json:
return Icons.code;
}
}
void _afficherDemandesUrgentes() {
context.read<DemandesAideBloc>().add(ChargerDemandesUrgentesEvent(
organisationId: widget.organisationId ?? '',
));
}
void _supprimerFiltre(String filtre) {
final state = context.read<DemandesAideBloc>().state;
if (state is DemandesAideLoaded) {
var nouveauxFiltres = state.filtres;
switch (filtre) {
case 'typeAide':
nouveauxFiltres = nouveauxFiltres.copyWith(typeAide: null);
break;
case 'statut':
nouveauxFiltres = nouveauxFiltres.copyWith(statut: null);
break;
case 'priorite':
nouveauxFiltres = nouveauxFiltres.copyWith(priorite: null);
break;
case 'urgente':
nouveauxFiltres = nouveauxFiltres.copyWith(urgente: null);
break;
case 'motCle':
nouveauxFiltres = nouveauxFiltres.copyWith(motCle: '');
_searchController.clear();
break;
}
context.read<DemandesAideBloc>().add(FiltrerDemandesAideEvent(
typeAide: nouveauxFiltres.typeAide,
statut: nouveauxFiltres.statut,
priorite: nouveauxFiltres.priorite,
urgente: nouveauxFiltres.urgente,
motCle: nouveauxFiltres.motCle,
));
}
}
void _effacerTousFiltres() {
_searchController.clear();
context.read<DemandesAideBloc>().add(const FiltrerDemandesAideEvent());
}
void _ouvrirFichier(String filePath) {
// Implémenter l'ouverture du fichier avec un package comme open_file
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Ouverture du fichier: $filePath')),
);
}
}

View File

@@ -0,0 +1,407 @@
import 'package:flutter/material.dart';
import '../../../../core/widgets/unified_card.dart';
import '../../../../core/theme/app_colors.dart';
import '../../../../core/theme/app_text_styles.dart';
import '../../../../core/utils/date_formatter.dart';
import '../../../../core/utils/currency_formatter.dart';
import '../../domain/entities/demande_aide.dart';
/// Widget de carte pour afficher une demande d'aide
///
/// Cette carte affiche les informations essentielles d'une demande d'aide
/// avec un design cohérent et des interactions tactiles.
class DemandeAideCard extends StatelessWidget {
final DemandeAide demande;
final bool isSelected;
final bool isSelectionMode;
final VoidCallback? onTap;
final VoidCallback? onLongPress;
final ValueChanged<bool>? onSelectionChanged;
const DemandeAideCard({
super.key,
required this.demande,
this.isSelected = false,
this.isSelectionMode = false,
this.onTap,
this.onLongPress,
this.onSelectionChanged,
});
@override
Widget build(BuildContext context) {
return UnifiedCard(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: InkWell(
onTap: onTap,
onLongPress: onLongPress,
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: isSelected
? Border.all(color: AppColors.primary, width: 2)
: null,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
const SizedBox(height: 12),
_buildContent(),
const SizedBox(height: 12),
_buildFooter(),
],
),
),
),
);
}
Widget _buildHeader() {
return Row(
children: [
if (isSelectionMode) ...[
Checkbox(
value: isSelected,
onChanged: onSelectionChanged,
activeColor: AppColors.primary,
),
const SizedBox(width: 8),
],
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
demande.titre,
style: AppTextStyles.titleMedium.copyWith(
fontWeight: FontWeight.bold,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 8),
_buildStatutChip(),
],
),
const SizedBox(height: 4),
Row(
children: [
Icon(
Icons.person,
size: 16,
color: AppColors.textSecondary,
),
const SizedBox(width: 4),
Expanded(
child: Text(
demande.nomDemandeur,
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textSecondary,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 8),
Text(
demande.numeroReference,
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textSecondary,
fontFamily: 'monospace',
),
),
],
),
],
),
),
if (demande.estUrgente) ...[
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: AppColors.error.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.priority_high,
size: 16,
color: AppColors.error,
),
const SizedBox(width: 4),
Text(
'URGENT',
style: AppTextStyles.labelSmall.copyWith(
color: AppColors.error,
fontWeight: FontWeight.bold,
),
),
],
),
),
],
],
);
}
Widget _buildContent() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
demande.description,
style: AppTextStyles.bodyMedium,
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 8),
Row(
children: [
_buildTypeAideChip(),
const SizedBox(width: 8),
_buildPrioriteChip(),
const Spacer(),
if (demande.montantDemande != null)
Text(
CurrencyFormatter.formatCFA(demande.montantDemande!),
style: AppTextStyles.titleSmall.copyWith(
color: AppColors.primary,
fontWeight: FontWeight.bold,
),
),
],
),
],
);
}
Widget _buildFooter() {
return Row(
children: [
Icon(
Icons.access_time,
size: 16,
color: AppColors.textSecondary,
),
const SizedBox(width: 4),
Text(
'Créée ${DateFormatter.formatRelative(demande.dateCreation)}',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textSecondary,
),
),
if (demande.dateModification != demande.dateCreation) ...[
const SizedBox(width: 8),
Text(
'• Modifiée ${DateFormatter.formatRelative(demande.dateModification)}',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textSecondary,
),
),
],
const Spacer(),
_buildProgressIndicator(),
],
);
}
Widget _buildStatutChip() {
final color = _getStatutColor(demande.statut);
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Text(
demande.statut.libelle,
style: AppTextStyles.labelSmall.copyWith(
color: color,
fontWeight: FontWeight.w600,
),
),
);
}
Widget _buildTypeAideChip() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppColors.outline),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
_getTypeAideIcon(demande.typeAide),
size: 14,
color: AppColors.primary,
),
const SizedBox(width: 4),
Text(
demande.typeAide.libelle,
style: AppTextStyles.labelSmall.copyWith(
color: AppColors.textPrimary,
),
),
],
),
);
}
Widget _buildPrioriteChip() {
final color = _getPrioriteColor(demande.priorite);
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
_getPrioriteIcon(demande.priorite),
size: 14,
color: color,
),
const SizedBox(width: 4),
Text(
demande.priorite.libelle,
style: AppTextStyles.labelSmall.copyWith(
color: color,
),
),
],
),
);
}
Widget _buildProgressIndicator() {
final progress = demande.pourcentageAvancement;
final color = _getProgressColor(progress);
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 60,
height: 4,
decoration: BoxDecoration(
color: AppColors.outline,
borderRadius: BorderRadius.circular(2),
),
child: FractionallySizedBox(
alignment: Alignment.centerLeft,
widthFactor: progress / 100,
child: Container(
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(2),
),
),
),
),
const SizedBox(width: 8),
Text(
'${progress.toInt()}%',
style: AppTextStyles.labelSmall.copyWith(
color: color,
fontWeight: FontWeight.w600,
),
),
],
);
}
Color _getStatutColor(StatutAide statut) {
switch (statut) {
case StatutAide.brouillon:
return AppColors.textSecondary;
case StatutAide.soumise:
return AppColors.warning;
case StatutAide.enEvaluation:
return AppColors.info;
case StatutAide.approuvee:
return AppColors.success;
case StatutAide.rejetee:
return AppColors.error;
case StatutAide.enCours:
return AppColors.primary;
case StatutAide.terminee:
return AppColors.success;
case StatutAide.versee:
return AppColors.success;
case StatutAide.livree:
return AppColors.success;
case StatutAide.annulee:
return AppColors.error;
}
}
Color _getPrioriteColor(PrioriteAide priorite) {
switch (priorite) {
case PrioriteAide.basse:
return AppColors.success;
case PrioriteAide.normale:
return AppColors.info;
case PrioriteAide.haute:
return AppColors.warning;
case PrioriteAide.critique:
return AppColors.error;
}
}
Color _getProgressColor(double progress) {
if (progress < 25) return AppColors.error;
if (progress < 50) return AppColors.warning;
if (progress < 75) return AppColors.info;
return AppColors.success;
}
IconData _getTypeAideIcon(TypeAide typeAide) {
switch (typeAide) {
case TypeAide.aideFinanciereUrgente:
return Icons.attach_money;
case TypeAide.aideFinanciereMedicale:
return Icons.medical_services;
case TypeAide.aideFinanciereEducation:
return Icons.school;
case TypeAide.aideMaterielleVetements:
return Icons.checkroom;
case TypeAide.aideMaterielleNourriture:
return Icons.restaurant;
case TypeAide.aideProfessionnelleFormation:
return Icons.work;
case TypeAide.aideSocialeAccompagnement:
return Icons.support;
case TypeAide.autre:
return Icons.help;
}
}
IconData _getPrioriteIcon(PrioriteAide priorite) {
switch (priorite) {
case PrioriteAide.basse:
return Icons.keyboard_arrow_down;
case PrioriteAide.normale:
return Icons.remove;
case PrioriteAide.haute:
return Icons.keyboard_arrow_up;
case PrioriteAide.critique:
return Icons.priority_high;
}
}
}

View File

@@ -0,0 +1,343 @@
import 'package:flutter/material.dart';
import '../../../../core/widgets/unified_card.dart';
import '../../../../core/theme/app_colors.dart';
import '../../../../core/theme/app_text_styles.dart';
import '../../../../core/utils/file_utils.dart';
import '../../domain/entities/demande_aide.dart';
/// Widget pour afficher la section des documents d'une demande d'aide
///
/// Ce widget affiche tous les documents joints à une demande d'aide
/// avec la possibilité de les visualiser et télécharger.
class DemandeAideDocumentsSection extends StatelessWidget {
final DemandeAide demande;
const DemandeAideDocumentsSection({
super.key,
required this.demande,
});
@override
Widget build(BuildContext context) {
if (demande.piecesJustificatives.isEmpty) {
return const SizedBox.shrink();
}
return UnifiedCard(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
'Documents joints',
style: AppTextStyles.titleMedium.copyWith(
fontWeight: FontWeight.bold,
),
),
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: AppColors.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Text(
'${demande.piecesJustificatives.length}',
style: AppTextStyles.labelSmall.copyWith(
color: AppColors.primary,
fontWeight: FontWeight.bold,
),
),
),
],
),
const SizedBox(height: 16),
...demande.piecesJustificatives.asMap().entries.map((entry) {
final index = entry.key;
final document = entry.value;
final isLast = index == demande.piecesJustificatives.length - 1;
return Column(
children: [
_buildDocumentCard(context, document),
if (!isLast) const SizedBox(height: 8),
],
);
}),
],
),
),
);
}
Widget _buildDocumentCard(BuildContext context, PieceJustificative document) {
final fileExtension = _getFileExtension(document.nomFichier);
final fileIcon = _getFileIcon(fileExtension);
final fileColor = _getFileColor(fileExtension);
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppColors.outline),
),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: fileColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
fileIcon,
color: fileColor,
size: 20,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
document.nomFichier,
style: AppTextStyles.bodyMedium.copyWith(
fontWeight: FontWeight.w600,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Row(
children: [
Text(
document.typeDocument.libelle,
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textSecondary,
),
),
if (document.tailleFichier != null) ...[
Text(
'',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textSecondary,
),
),
Text(
_formatFileSize(document.tailleFichier!),
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textSecondary,
),
),
],
],
),
if (document.description != null && document.description!.isNotEmpty) ...[
const SizedBox(height: 4),
Text(
document.description!,
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textSecondary,
fontStyle: FontStyle.italic,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
],
),
),
const SizedBox(width: 8),
Column(
children: [
IconButton(
onPressed: () => _previewDocument(context, document),
icon: const Icon(Icons.visibility),
tooltip: 'Aperçu',
iconSize: 20,
),
IconButton(
onPressed: () => _downloadDocument(context, document),
icon: const Icon(Icons.download),
tooltip: 'Télécharger',
iconSize: 20,
),
],
),
],
),
);
}
String _getFileExtension(String fileName) {
final parts = fileName.split('.');
return parts.length > 1 ? parts.last.toLowerCase() : '';
}
IconData _getFileIcon(String extension) {
switch (extension) {
case 'pdf':
return Icons.picture_as_pdf;
case 'doc':
case 'docx':
return Icons.description;
case 'xls':
case 'xlsx':
return Icons.table_chart;
case 'ppt':
case 'pptx':
return Icons.slideshow;
case 'jpg':
case 'jpeg':
case 'png':
case 'gif':
case 'bmp':
return Icons.image;
case 'mp4':
case 'avi':
case 'mov':
case 'wmv':
return Icons.video_file;
case 'mp3':
case 'wav':
case 'aac':
return Icons.audio_file;
case 'zip':
case 'rar':
case '7z':
return Icons.archive;
case 'txt':
return Icons.text_snippet;
default:
return Icons.insert_drive_file;
}
}
Color _getFileColor(String extension) {
switch (extension) {
case 'pdf':
return Colors.red;
case 'doc':
case 'docx':
return Colors.blue;
case 'xls':
case 'xlsx':
return Colors.green;
case 'ppt':
case 'pptx':
return Colors.orange;
case 'jpg':
case 'jpeg':
case 'png':
case 'gif':
case 'bmp':
return Colors.purple;
case 'mp4':
case 'avi':
case 'mov':
case 'wmv':
return Colors.indigo;
case 'mp3':
case 'wav':
case 'aac':
return Colors.teal;
case 'zip':
case 'rar':
case '7z':
return Colors.brown;
case 'txt':
return Colors.grey;
default:
return AppColors.textSecondary;
}
}
String _formatFileSize(int bytes) {
if (bytes < 1024) {
return '$bytes B';
} else if (bytes < 1024 * 1024) {
return '${(bytes / 1024).toStringAsFixed(1)} KB';
} else if (bytes < 1024 * 1024 * 1024) {
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
} else {
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB';
}
}
void _previewDocument(BuildContext context, PieceJustificative document) {
// Implémenter la prévisualisation du document
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Aperçu du document'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Nom: ${document.nomFichier}'),
Text('Type: ${document.typeDocument.libelle}'),
if (document.tailleFichier != null)
Text('Taille: ${_formatFileSize(document.tailleFichier!)}'),
if (document.description != null && document.description!.isNotEmpty)
Text('Description: ${document.description}'),
const SizedBox(height: 16),
const Text('Fonctionnalité de prévisualisation à implémenter'),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Fermer'),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
_downloadDocument(context, document);
},
child: const Text('Télécharger'),
),
],
),
);
}
void _downloadDocument(BuildContext context, PieceJustificative document) {
// Implémenter le téléchargement du document
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Téléchargement de ${document.nomFichier}...'),
action: SnackBarAction(
label: 'Annuler',
onPressed: () {
// Annuler le téléchargement
},
),
),
);
// Simuler le téléchargement
Future.delayed(const Duration(seconds: 2), () {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('${document.nomFichier} téléchargé avec succès'),
backgroundColor: AppColors.success,
action: SnackBarAction(
label: 'Ouvrir',
textColor: Colors.white,
onPressed: () {
// Ouvrir le fichier téléchargé
},
),
),
);
}
});
}
}

View File

@@ -0,0 +1,412 @@
import 'package:flutter/material.dart';
import '../../../../core/widgets/unified_card.dart';
import '../../../../core/theme/app_colors.dart';
import '../../../../core/theme/app_text_styles.dart';
import '../../../../core/utils/date_formatter.dart';
import '../../../../core/utils/currency_formatter.dart';
import '../../domain/entities/demande_aide.dart';
/// Widget pour afficher la section des évaluations d'une demande d'aide
///
/// Ce widget affiche toutes les évaluations effectuées sur une demande d'aide
/// avec les détails de chaque évaluation.
class DemandeAideEvaluationSection extends StatelessWidget {
final DemandeAide demande;
const DemandeAideEvaluationSection({
super.key,
required this.demande,
});
@override
Widget build(BuildContext context) {
if (demande.evaluations.isEmpty) {
return const SizedBox.shrink();
}
return UnifiedCard(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
'Évaluations',
style: AppTextStyles.titleMedium.copyWith(
fontWeight: FontWeight.bold,
),
),
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: AppColors.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Text(
'${demande.evaluations.length}',
style: AppTextStyles.labelSmall.copyWith(
color: AppColors.primary,
fontWeight: FontWeight.bold,
),
),
),
],
),
const SizedBox(height: 16),
...demande.evaluations.asMap().entries.map((entry) {
final index = entry.key;
final evaluation = entry.value;
final isLast = index == demande.evaluations.length - 1;
return Column(
children: [
_buildEvaluationCard(evaluation),
if (!isLast) const SizedBox(height: 12),
],
);
}),
],
),
),
);
}
Widget _buildEvaluationCard(EvaluationAide evaluation) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.outline),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildEvaluationHeader(evaluation),
const SizedBox(height: 12),
_buildEvaluationContent(evaluation),
if (evaluation.commentaire != null && evaluation.commentaire!.isNotEmpty) ...[
const SizedBox(height: 12),
_buildCommentaireSection(evaluation.commentaire!),
],
if (evaluation.criteres.isNotEmpty) ...[
const SizedBox(height: 12),
_buildCriteresSection(evaluation.criteres),
],
],
),
);
}
Widget _buildEvaluationHeader(EvaluationAide evaluation) {
final color = _getDecisionColor(evaluation.decision);
return Row(
children: [
CircleAvatar(
radius: 20,
backgroundColor: color.withOpacity(0.1),
child: Icon(
_getDecisionIcon(evaluation.decision),
color: color,
size: 20,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
evaluation.nomEvaluateur,
style: AppTextStyles.bodyMedium.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 2),
Text(
evaluation.typeEvaluateur.libelle,
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textSecondary,
),
),
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
evaluation.decision.libelle,
style: AppTextStyles.labelSmall.copyWith(
color: color,
fontWeight: FontWeight.w600,
),
),
),
const SizedBox(height: 4),
Text(
DateFormatter.formatShort(evaluation.dateEvaluation),
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textSecondary,
),
),
],
),
],
);
}
Widget _buildEvaluationContent(EvaluationAide evaluation) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (evaluation.noteGlobale != null) ...[
Row(
children: [
Text(
'Note globale:',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textSecondary,
fontWeight: FontWeight.w500,
),
),
const SizedBox(width: 8),
_buildStarRating(evaluation.noteGlobale!),
const SizedBox(width: 8),
Text(
'${evaluation.noteGlobale!.toStringAsFixed(1)}/5',
style: AppTextStyles.bodySmall.copyWith(
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 8),
],
if (evaluation.montantRecommande != null) ...[
Row(
children: [
Icon(
Icons.attach_money,
size: 16,
color: AppColors.textSecondary,
),
const SizedBox(width: 4),
Text(
'Montant recommandé:',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textSecondary,
fontWeight: FontWeight.w500,
),
),
const SizedBox(width: 8),
Text(
CurrencyFormatter.formatCFA(evaluation.montantRecommande!),
style: AppTextStyles.bodyMedium.copyWith(
color: AppColors.primary,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 8),
],
if (evaluation.prioriteRecommandee != null) ...[
Row(
children: [
Icon(
Icons.priority_high,
size: 16,
color: AppColors.textSecondary,
),
const SizedBox(width: 4),
Text(
'Priorité recommandée:',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textSecondary,
fontWeight: FontWeight.w500,
),
),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: _getPrioriteColor(evaluation.prioriteRecommandee!).withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
evaluation.prioriteRecommandee!.libelle,
style: AppTextStyles.labelSmall.copyWith(
color: _getPrioriteColor(evaluation.prioriteRecommandee!),
fontWeight: FontWeight.w600,
),
),
),
],
),
],
],
);
}
Widget _buildCommentaireSection(String commentaire) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColors.background,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppColors.outline),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.comment,
size: 16,
color: AppColors.textSecondary,
),
const SizedBox(width: 4),
Text(
'Commentaire',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textSecondary,
fontWeight: FontWeight.w600,
),
),
],
),
const SizedBox(height: 8),
Text(
commentaire,
style: AppTextStyles.bodySmall,
),
],
),
);
}
Widget _buildCriteresSection(Map<String, double> criteres) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColors.background,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppColors.outline),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.checklist,
size: 16,
color: AppColors.textSecondary,
),
const SizedBox(width: 4),
Text(
'Critères d\'évaluation',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textSecondary,
fontWeight: FontWeight.w600,
),
),
],
),
const SizedBox(height: 8),
...criteres.entries.map((entry) => Padding(
padding: const EdgeInsets.only(bottom: 4),
child: Row(
children: [
Expanded(
child: Text(
entry.key,
style: AppTextStyles.bodySmall,
),
),
const SizedBox(width: 8),
_buildStarRating(entry.value),
const SizedBox(width: 8),
Text(
'${entry.value.toStringAsFixed(1)}/5',
style: AppTextStyles.bodySmall.copyWith(
fontWeight: FontWeight.w600,
),
),
],
),
)),
],
),
);
}
Widget _buildStarRating(double rating) {
return Row(
mainAxisSize: MainAxisSize.min,
children: List.generate(5, (index) {
final starValue = index + 1;
return Icon(
starValue <= rating
? Icons.star
: starValue - 0.5 <= rating
? Icons.star_half
: Icons.star_border,
size: 16,
color: AppColors.warning,
);
}),
);
}
Color _getDecisionColor(StatutAide decision) {
switch (decision) {
case StatutAide.approuvee:
return AppColors.success;
case StatutAide.rejetee:
return AppColors.error;
case StatutAide.enEvaluation:
return AppColors.info;
default:
return AppColors.textSecondary;
}
}
IconData _getDecisionIcon(StatutAide decision) {
switch (decision) {
case StatutAide.approuvee:
return Icons.check_circle;
case StatutAide.rejetee:
return Icons.cancel;
case StatutAide.enEvaluation:
return Icons.rate_review;
default:
return Icons.help;
}
}
Color _getPrioriteColor(PrioriteAide priorite) {
switch (priorite) {
case PrioriteAide.basse:
return AppColors.success;
case PrioriteAide.normale:
return AppColors.info;
case PrioriteAide.haute:
return AppColors.warning;
case PrioriteAide.critique:
return AppColors.error;
}
}
}

View File

@@ -0,0 +1,744 @@
import 'package:flutter/material.dart';
import '../../../../core/widgets/unified_card.dart';
import '../../../../core/theme/app_colors.dart';
import '../../../../core/theme/app_text_styles.dart';
import '../../../../core/utils/validators.dart';
import '../../domain/entities/demande_aide.dart';
/// Section du formulaire pour les bénéficiaires
class DemandeAideFormBeneficiairesSection extends StatefulWidget {
final List<BeneficiaireAide> beneficiaires;
final ValueChanged<List<BeneficiaireAide>> onBeneficiairesChanged;
const DemandeAideFormBeneficiairesSection({
super.key,
required this.beneficiaires,
required this.onBeneficiairesChanged,
});
@override
State<DemandeAideFormBeneficiairesSection> createState() => _DemandeAideFormBeneficiairesState();
}
class _DemandeAideFormBeneficiairesState extends State<DemandeAideFormBeneficiairesSection> {
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
UnifiedCard(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
'Bénéficiaires',
style: AppTextStyles.titleMedium.copyWith(
fontWeight: FontWeight.bold,
),
),
const Spacer(),
TextButton.icon(
onPressed: _ajouterBeneficiaire,
icon: const Icon(Icons.add),
label: const Text('Ajouter'),
),
],
),
const SizedBox(height: 8),
Text(
'Ajoutez les personnes qui bénéficieront de cette aide (optionnel)',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textSecondary,
),
),
const SizedBox(height: 16),
if (widget.beneficiaires.isEmpty)
Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppColors.outline),
),
child: Column(
children: [
Icon(
Icons.people_outline,
size: 48,
color: AppColors.textSecondary,
),
const SizedBox(height: 8),
Text(
'Aucun bénéficiaire ajouté',
style: AppTextStyles.bodyMedium.copyWith(
color: AppColors.textSecondary,
),
),
],
),
)
else
...widget.beneficiaires.asMap().entries.map((entry) {
final index = entry.key;
final beneficiaire = entry.value;
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: _buildBeneficiaireCard(beneficiaire, index),
);
}),
],
),
),
),
],
),
);
}
Widget _buildBeneficiaireCard(BeneficiaireAide beneficiaire, int index) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppColors.outline),
),
child: Row(
children: [
CircleAvatar(
backgroundColor: AppColors.primary.withOpacity(0.1),
child: Icon(
Icons.person,
color: AppColors.primary,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${beneficiaire.prenom} ${beneficiaire.nom}',
style: AppTextStyles.bodyMedium.copyWith(
fontWeight: FontWeight.w600,
),
),
if (beneficiaire.age != null)
Text(
'${beneficiaire.age} ans',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textSecondary,
),
),
],
),
),
IconButton(
onPressed: () => _modifierBeneficiaire(index),
icon: const Icon(Icons.edit),
iconSize: 20,
),
IconButton(
onPressed: () => _supprimerBeneficiaire(index),
icon: const Icon(Icons.delete),
iconSize: 20,
color: AppColors.error,
),
],
),
);
}
void _ajouterBeneficiaire() {
_showBeneficiaireDialog();
}
void _modifierBeneficiaire(int index) {
_showBeneficiaireDialog(beneficiaire: widget.beneficiaires[index], index: index);
}
void _supprimerBeneficiaire(int index) {
final nouveauxBeneficiaires = List<BeneficiaireAide>.from(widget.beneficiaires);
nouveauxBeneficiaires.removeAt(index);
widget.onBeneficiairesChanged(nouveauxBeneficiaires);
}
void _showBeneficiaireDialog({BeneficiaireAide? beneficiaire, int? index}) {
final prenomController = TextEditingController(text: beneficiaire?.prenom ?? '');
final nomController = TextEditingController(text: beneficiaire?.nom ?? '');
final ageController = TextEditingController(text: beneficiaire?.age?.toString() ?? '');
final formKey = GlobalKey<FormState>();
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(beneficiaire == null ? 'Ajouter un bénéficiaire' : 'Modifier le bénéficiaire'),
content: Form(
key: formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
controller: prenomController,
decoration: const InputDecoration(
labelText: 'Prénom *',
border: OutlineInputBorder(),
),
validator: Validators.required,
),
const SizedBox(height: 16),
TextFormField(
controller: nomController,
decoration: const InputDecoration(
labelText: 'Nom *',
border: OutlineInputBorder(),
),
validator: Validators.required,
),
const SizedBox(height: 16),
TextFormField(
controller: ageController,
decoration: const InputDecoration(
labelText: 'Âge',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
validator: (value) {
if (value != null && value.isNotEmpty) {
final age = int.tryParse(value);
if (age == null || age < 0 || age > 150) {
return 'Veuillez saisir un âge valide';
}
}
return null;
},
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () {
if (formKey.currentState!.validate()) {
final nouveauBeneficiaire = BeneficiaireAide(
prenom: prenomController.text,
nom: nomController.text,
age: ageController.text.isEmpty ? null : int.parse(ageController.text),
);
final nouveauxBeneficiaires = List<BeneficiaireAide>.from(widget.beneficiaires);
if (index != null) {
nouveauxBeneficiaires[index] = nouveauBeneficiaire;
} else {
nouveauxBeneficiaires.add(nouveauBeneficiaire);
}
widget.onBeneficiairesChanged(nouveauxBeneficiaires);
Navigator.pop(context);
}
},
child: Text(beneficiaire == null ? 'Ajouter' : 'Modifier'),
),
],
),
);
}
}
/// Section du formulaire pour le contact d'urgence
class DemandeAideFormContactSection extends StatefulWidget {
final ContactUrgence? contactUrgence;
final ValueChanged<ContactUrgence?> onContactChanged;
const DemandeAideFormContactSection({
super.key,
required this.contactUrgence,
required this.onContactChanged,
});
@override
State<DemandeAideFormContactSection> createState() => _DemandeAideFormContactSectionState();
}
class _DemandeAideFormContactSectionState extends State<DemandeAideFormContactSection> {
final _prenomController = TextEditingController();
final _nomController = TextEditingController();
final _telephoneController = TextEditingController();
final _emailController = TextEditingController();
final _relationController = TextEditingController();
final _formKey = GlobalKey<FormState>();
@override
void initState() {
super.initState();
if (widget.contactUrgence != null) {
_prenomController.text = widget.contactUrgence!.prenom;
_nomController.text = widget.contactUrgence!.nom;
_telephoneController.text = widget.contactUrgence!.telephone;
_emailController.text = widget.contactUrgence!.email ?? '';
_relationController.text = widget.contactUrgence!.relation;
}
}
@override
void dispose() {
_prenomController.dispose();
_nomController.dispose();
_telephoneController.dispose();
_emailController.dispose();
_relationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
UnifiedCard(
child: Padding(
padding: const EdgeInsets.all(16),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Contact d\'urgence',
style: AppTextStyles.titleMedium.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
'Personne à contacter en cas d\'urgence (optionnel)',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textSecondary,
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: TextFormField(
controller: _prenomController,
decoration: const InputDecoration(
labelText: 'Prénom',
border: OutlineInputBorder(),
),
onChanged: _updateContact,
),
),
const SizedBox(width: 16),
Expanded(
child: TextFormField(
controller: _nomController,
decoration: const InputDecoration(
labelText: 'Nom',
border: OutlineInputBorder(),
),
onChanged: _updateContact,
),
),
],
),
const SizedBox(height: 16),
TextFormField(
controller: _telephoneController,
decoration: const InputDecoration(
labelText: 'Téléphone',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.phone),
),
keyboardType: TextInputType.phone,
onChanged: _updateContact,
),
const SizedBox(height: 16),
TextFormField(
controller: _emailController,
decoration: const InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.email),
),
keyboardType: TextInputType.emailAddress,
onChanged: _updateContact,
),
const SizedBox(height: 16),
TextFormField(
controller: _relationController,
decoration: const InputDecoration(
labelText: 'Relation',
hintText: 'Ex: Conjoint, Parent, Ami...',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.family_restroom),
),
onChanged: _updateContact,
),
],
),
),
),
),
],
),
);
}
void _updateContact(String value) {
if (_prenomController.text.isNotEmpty ||
_nomController.text.isNotEmpty ||
_telephoneController.text.isNotEmpty ||
_emailController.text.isNotEmpty ||
_relationController.text.isNotEmpty) {
final contact = ContactUrgence(
prenom: _prenomController.text,
nom: _nomController.text,
telephone: _telephoneController.text,
email: _emailController.text.isEmpty ? null : _emailController.text,
relation: _relationController.text,
);
widget.onContactChanged(contact);
} else {
widget.onContactChanged(null);
}
}
}
/// Section du formulaire pour la localisation
class DemandeAideFormLocalisationSection extends StatefulWidget {
final Localisation? localisation;
final ValueChanged<Localisation?> onLocalisationChanged;
const DemandeAideFormLocalisationSection({
super.key,
required this.localisation,
required this.onLocalisationChanged,
});
@override
State<DemandeAideFormLocalisationSection> createState() => _DemandeAideFormLocalisationSectionState();
}
class _DemandeAideFormLocalisationSectionState extends State<DemandeAideFormLocalisationSection> {
final _adresseController = TextEditingController();
final _villeController = TextEditingController();
final _codePostalController = TextEditingController();
final _paysController = TextEditingController();
final _formKey = GlobalKey<FormState>();
@override
void initState() {
super.initState();
if (widget.localisation != null) {
_adresseController.text = widget.localisation!.adresse;
_villeController.text = widget.localisation!.ville ?? '';
_codePostalController.text = widget.localisation!.codePostal ?? '';
_paysController.text = widget.localisation!.pays ?? '';
}
}
@override
void dispose() {
_adresseController.dispose();
_villeController.dispose();
_codePostalController.dispose();
_paysController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
UnifiedCard(
child: Padding(
padding: const EdgeInsets.all(16),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Localisation',
style: AppTextStyles.titleMedium.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
'Lieu où l\'aide sera fournie (optionnel)',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textSecondary,
),
),
const SizedBox(height: 16),
TextFormField(
controller: _adresseController,
decoration: const InputDecoration(
labelText: 'Adresse',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.location_on),
),
maxLines: 2,
onChanged: _updateLocalisation,
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: TextFormField(
controller: _villeController,
decoration: const InputDecoration(
labelText: 'Ville',
border: OutlineInputBorder(),
),
onChanged: _updateLocalisation,
),
),
const SizedBox(width: 16),
Expanded(
child: TextFormField(
controller: _codePostalController,
decoration: const InputDecoration(
labelText: 'Code postal',
border: OutlineInputBorder(),
),
onChanged: _updateLocalisation,
),
),
],
),
const SizedBox(height: 16),
TextFormField(
controller: _paysController,
decoration: const InputDecoration(
labelText: 'Pays',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.flag),
),
onChanged: _updateLocalisation,
),
const SizedBox(height: 16),
OutlinedButton.icon(
onPressed: _utiliserPositionActuelle,
icon: const Icon(Icons.my_location),
label: const Text('Utiliser ma position actuelle'),
),
],
),
),
),
),
],
),
);
}
void _updateLocalisation(String value) {
if (_adresseController.text.isNotEmpty ||
_villeController.text.isNotEmpty ||
_codePostalController.text.isNotEmpty ||
_paysController.text.isNotEmpty) {
final localisation = Localisation(
adresse: _adresseController.text,
ville: _villeController.text.isEmpty ? null : _villeController.text,
codePostal: _codePostalController.text.isEmpty ? null : _codePostalController.text,
pays: _paysController.text.isEmpty ? null : _paysController.text,
latitude: widget.localisation?.latitude,
longitude: widget.localisation?.longitude,
);
widget.onLocalisationChanged(localisation);
} else {
widget.onLocalisationChanged(null);
}
}
void _utiliserPositionActuelle() {
// Implémenter la géolocalisation
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Fonctionnalité de géolocalisation à implémenter'),
),
);
}
}
/// Section du formulaire pour les documents
class DemandeAideFormDocumentsSection extends StatefulWidget {
final List<PieceJustificative> piecesJustificatives;
final ValueChanged<List<PieceJustificative>> onDocumentsChanged;
const DemandeAideFormDocumentsSection({
super.key,
required this.piecesJustificatives,
required this.onDocumentsChanged,
});
@override
State<DemandeAideFormDocumentsSection> createState() => _DemandeAideFormDocumentsSectionState();
}
class _DemandeAideFormDocumentsSectionState extends State<DemandeAideFormDocumentsSection> {
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
UnifiedCard(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
'Documents justificatifs',
style: AppTextStyles.titleMedium.copyWith(
fontWeight: FontWeight.bold,
),
),
const Spacer(),
TextButton.icon(
onPressed: _ajouterDocument,
icon: const Icon(Icons.add),
label: const Text('Ajouter'),
),
],
),
const SizedBox(height: 8),
Text(
'Ajoutez des documents pour appuyer votre demande (optionnel)',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textSecondary,
),
),
const SizedBox(height: 16),
if (widget.piecesJustificatives.isEmpty)
Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppColors.outline),
),
child: Column(
children: [
Icon(
Icons.upload_file,
size: 48,
color: AppColors.textSecondary,
),
const SizedBox(height: 8),
Text(
'Aucun document ajouté',
style: AppTextStyles.bodyMedium.copyWith(
color: AppColors.textSecondary,
),
),
const SizedBox(height: 8),
Text(
'Formats acceptés: PDF, DOC, JPG, PNG',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textSecondary,
),
),
],
),
)
else
...widget.piecesJustificatives.asMap().entries.map((entry) {
final index = entry.key;
final document = entry.value;
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: _buildDocumentCard(document, index),
);
}),
],
),
),
),
],
),
);
}
Widget _buildDocumentCard(PieceJustificative document, int index) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppColors.outline),
),
child: Row(
children: [
Icon(
Icons.insert_drive_file,
color: AppColors.primary,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
document.nomFichier,
style: AppTextStyles.bodyMedium.copyWith(
fontWeight: FontWeight.w600,
),
),
Text(
document.typeDocument.libelle,
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textSecondary,
),
),
],
),
),
IconButton(
onPressed: () => _supprimerDocument(index),
icon: const Icon(Icons.delete),
iconSize: 20,
color: AppColors.error,
),
],
),
);
}
void _ajouterDocument() {
// Implémenter la sélection de fichier
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Fonctionnalité de sélection de fichier à implémenter'),
),
);
}
void _supprimerDocument(int index) {
final nouveauxDocuments = List<PieceJustificative>.from(widget.piecesJustificatives);
nouveauxDocuments.removeAt(index);
widget.onDocumentsChanged(nouveauxDocuments);
}
}

View File

@@ -0,0 +1,308 @@
import 'package:flutter/material.dart';
import '../../../../core/widgets/unified_card.dart';
import '../../../../core/theme/app_colors.dart';
import '../../../../core/theme/app_text_styles.dart';
import '../../../../core/utils/date_formatter.dart';
import '../../domain/entities/demande_aide.dart';
/// Widget de timeline pour afficher l'historique des statuts d'une demande d'aide
///
/// Ce widget affiche une timeline verticale avec tous les changements de statut
/// de la demande d'aide, incluant les dates et les commentaires.
class DemandeAideStatusTimeline extends StatelessWidget {
final DemandeAide demande;
const DemandeAideStatusTimeline({
super.key,
required this.demande,
});
@override
Widget build(BuildContext context) {
final historique = _buildHistorique();
if (historique.isEmpty) {
return const SizedBox.shrink();
}
return UnifiedCard(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Historique des statuts',
style: AppTextStyles.titleMedium.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
...historique.asMap().entries.map((entry) {
final index = entry.key;
final item = entry.value;
final isLast = index == historique.length - 1;
return _buildTimelineItem(
item: item,
isLast: isLast,
isActive: index == 0, // Le premier élément est l'état actuel
);
}),
],
),
),
);
}
List<TimelineItem> _buildHistorique() {
final items = <TimelineItem>[];
// Ajouter l'état actuel
items.add(TimelineItem(
statut: demande.statut,
date: demande.dateModification,
commentaire: _getStatutDescription(demande.statut),
isActuel: true,
));
// Ajouter l'historique depuis les évaluations
for (final evaluation in demande.evaluations) {
items.add(TimelineItem(
statut: evaluation.decision,
date: evaluation.dateEvaluation,
commentaire: evaluation.commentaire,
evaluateur: evaluation.nomEvaluateur,
));
}
// Ajouter la création
if (demande.dateCreation != demande.dateModification) {
items.add(TimelineItem(
statut: StatutAide.brouillon,
date: demande.dateCreation,
commentaire: 'Demande créée',
));
}
return items;
}
Widget _buildTimelineItem({
required TimelineItem item,
required bool isLast,
required bool isActive,
}) {
final color = isActive ? _getStatutColor(item.statut) : AppColors.textSecondary;
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Timeline indicator
Column(
children: [
Container(
width: 24,
height: 24,
decoration: BoxDecoration(
color: isActive ? color : AppColors.surface,
border: Border.all(
color: color,
width: isActive ? 3 : 2,
),
shape: BoxShape.circle,
),
child: isActive
? Icon(
_getStatutIcon(item.statut),
size: 12,
color: Colors.white,
)
: null,
),
if (!isLast)
Container(
width: 2,
height: 40,
color: AppColors.outline,
),
],
),
const SizedBox(width: 16),
// Content
Expanded(
child: Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
item.statut.libelle,
style: AppTextStyles.bodyMedium.copyWith(
fontWeight: isActive ? FontWeight.bold : FontWeight.w600,
color: isActive ? color : AppColors.textPrimary,
),
),
),
if (item.isActuel)
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
'ACTUEL',
style: AppTextStyles.labelSmall.copyWith(
color: color,
fontWeight: FontWeight.bold,
),
),
),
],
),
const SizedBox(height: 4),
Text(
DateFormatter.formatComplete(item.date),
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textSecondary,
),
),
if (item.evaluateur != null) ...[
const SizedBox(height: 4),
Row(
children: [
Icon(
Icons.person,
size: 16,
color: AppColors.textSecondary,
),
const SizedBox(width: 4),
Text(
'Par ${item.evaluateur}',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.textSecondary,
fontStyle: FontStyle.italic,
),
),
],
),
],
if (item.commentaire != null && item.commentaire!.isNotEmpty) ...[
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppColors.outline),
),
child: Text(
item.commentaire!,
style: AppTextStyles.bodySmall,
),
),
],
],
),
),
),
],
);
}
Color _getStatutColor(StatutAide statut) {
switch (statut) {
case StatutAide.brouillon:
return AppColors.textSecondary;
case StatutAide.soumise:
return AppColors.warning;
case StatutAide.enEvaluation:
return AppColors.info;
case StatutAide.approuvee:
return AppColors.success;
case StatutAide.rejetee:
return AppColors.error;
case StatutAide.enCours:
return AppColors.primary;
case StatutAide.terminee:
return AppColors.success;
case StatutAide.versee:
return AppColors.success;
case StatutAide.livree:
return AppColors.success;
case StatutAide.annulee:
return AppColors.error;
}
}
IconData _getStatutIcon(StatutAide statut) {
switch (statut) {
case StatutAide.brouillon:
return Icons.edit;
case StatutAide.soumise:
return Icons.send;
case StatutAide.enEvaluation:
return Icons.rate_review;
case StatutAide.approuvee:
return Icons.check;
case StatutAide.rejetee:
return Icons.close;
case StatutAide.enCours:
return Icons.play_arrow;
case StatutAide.terminee:
return Icons.done_all;
case StatutAide.versee:
return Icons.payment;
case StatutAide.livree:
return Icons.local_shipping;
case StatutAide.annulee:
return Icons.cancel;
}
}
String _getStatutDescription(StatutAide statut) {
switch (statut) {
case StatutAide.brouillon:
return 'Demande en cours de rédaction';
case StatutAide.soumise:
return 'Demande soumise pour évaluation';
case StatutAide.enEvaluation:
return 'Demande en cours d\'évaluation';
case StatutAide.approuvee:
return 'Demande approuvée';
case StatutAide.rejetee:
return 'Demande rejetée';
case StatutAide.enCours:
return 'Aide en cours de traitement';
case StatutAide.terminee:
return 'Aide terminée';
case StatutAide.versee:
return 'Montant versé';
case StatutAide.livree:
return 'Aide livrée';
case StatutAide.annulee:
return 'Demande annulée';
}
}
}
/// Classe pour représenter un élément de la timeline
class TimelineItem {
final StatutAide statut;
final DateTime date;
final String? commentaire;
final String? evaluateur;
final bool isActuel;
const TimelineItem({
required this.statut,
required this.date,
this.commentaire,
this.evaluateur,
this.isActuel = false,
});
}

View File

@@ -0,0 +1,444 @@
import 'package:flutter/material.dart';
import '../../../../core/theme/app_colors.dart';
import '../../../../core/theme/app_text_styles.dart';
import '../../domain/entities/demande_aide.dart';
import '../bloc/demandes_aide/demandes_aide_state.dart';
/// Bottom sheet pour filtrer les demandes d'aide
///
/// Permet à l'utilisateur de sélectionner différents critères
/// de filtrage pour affiner la liste des demandes d'aide.
class DemandesAideFilterBottomSheet extends StatefulWidget {
final FiltresDemandesAide filtresActuels;
final ValueChanged<FiltresDemandesAide> onFiltresChanged;
const DemandesAideFilterBottomSheet({
super.key,
required this.filtresActuels,
required this.onFiltresChanged,
});
@override
State<DemandesAideFilterBottomSheet> createState() => _DemandesAideFilterBottomSheetState();
}
class _DemandesAideFilterBottomSheetState extends State<DemandesAideFilterBottomSheet> {
late FiltresDemandesAide _filtres;
final TextEditingController _motCleController = TextEditingController();
final TextEditingController _montantMinController = TextEditingController();
final TextEditingController _montantMaxController = TextEditingController();
@override
void initState() {
super.initState();
_filtres = widget.filtresActuels;
_motCleController.text = _filtres.motCle ?? '';
_montantMinController.text = _filtres.montantMin?.toInt().toString() ?? '';
_montantMaxController.text = _filtres.montantMax?.toInt().toString() ?? '';
}
@override
void dispose() {
_motCleController.dispose();
_montantMinController.dispose();
_montantMaxController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
height: MediaQuery.of(context).size.height * 0.8,
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
const SizedBox(height: 16),
Expanded(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildMotCleSection(),
const SizedBox(height: 24),
_buildTypeAideSection(),
const SizedBox(height: 24),
_buildStatutSection(),
const SizedBox(height: 24),
_buildPrioriteSection(),
const SizedBox(height: 24),
_buildUrgenteSection(),
const SizedBox(height: 24),
_buildMontantSection(),
const SizedBox(height: 24),
_buildDateSection(),
],
),
),
),
const SizedBox(height: 16),
_buildActions(),
],
),
);
}
Widget _buildHeader() {
return Row(
children: [
Text(
'Filtrer les demandes',
style: AppTextStyles.titleLarge.copyWith(
fontWeight: FontWeight.bold,
),
),
const Spacer(),
IconButton(
onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.close),
),
],
);
}
Widget _buildMotCleSection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Recherche par mot-clé',
style: AppTextStyles.titleMedium.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 8),
TextField(
controller: _motCleController,
decoration: const InputDecoration(
hintText: 'Titre, description, demandeur...',
prefixIcon: Icon(Icons.search),
border: OutlineInputBorder(),
),
onChanged: (value) {
setState(() {
_filtres = _filtres.copyWith(motCle: value.isEmpty ? null : value);
});
},
),
],
);
}
Widget _buildTypeAideSection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Type d\'aide',
style: AppTextStyles.titleMedium.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
_buildFilterChip(
label: 'Tous',
isSelected: _filtres.typeAide == null,
onSelected: () {
setState(() {
_filtres = _filtres.copyWith(typeAide: null);
});
},
),
...TypeAide.values.map((type) => _buildFilterChip(
label: type.libelle,
isSelected: _filtres.typeAide == type,
onSelected: () {
setState(() {
_filtres = _filtres.copyWith(typeAide: type);
});
},
)),
],
),
],
);
}
Widget _buildStatutSection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Statut',
style: AppTextStyles.titleMedium.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
_buildFilterChip(
label: 'Tous',
isSelected: _filtres.statut == null,
onSelected: () {
setState(() {
_filtres = _filtres.copyWith(statut: null);
});
},
),
...StatutAide.values.map((statut) => _buildFilterChip(
label: statut.libelle,
isSelected: _filtres.statut == statut,
onSelected: () {
setState(() {
_filtres = _filtres.copyWith(statut: statut);
});
},
)),
],
),
],
);
}
Widget _buildPrioriteSection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Priorité',
style: AppTextStyles.titleMedium.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
_buildFilterChip(
label: 'Toutes',
isSelected: _filtres.priorite == null,
onSelected: () {
setState(() {
_filtres = _filtres.copyWith(priorite: null);
});
},
),
...PrioriteAide.values.map((priorite) => _buildFilterChip(
label: priorite.libelle,
isSelected: _filtres.priorite == priorite,
onSelected: () {
setState(() {
_filtres = _filtres.copyWith(priorite: priorite);
});
},
)),
],
),
],
);
}
Widget _buildUrgenteSection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Urgence',
style: AppTextStyles.titleMedium.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: CheckboxListTile(
title: const Text('Demandes urgentes uniquement'),
value: _filtres.urgente == true,
onChanged: (value) {
setState(() {
_filtres = _filtres.copyWith(urgente: value == true ? true : null);
});
},
controlAffinity: ListTileControlAffinity.leading,
contentPadding: EdgeInsets.zero,
),
),
],
),
],
);
}
Widget _buildMontantSection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Montant demandé (FCFA)',
style: AppTextStyles.titleMedium.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: TextField(
controller: _montantMinController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: 'Minimum',
border: OutlineInputBorder(),
),
onChanged: (value) {
final montant = double.tryParse(value);
setState(() {
_filtres = _filtres.copyWith(montantMin: montant);
});
},
),
),
const SizedBox(width: 16),
Expanded(
child: TextField(
controller: _montantMaxController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: 'Maximum',
border: OutlineInputBorder(),
),
onChanged: (value) {
final montant = double.tryParse(value);
setState(() {
_filtres = _filtres.copyWith(montantMax: montant);
});
},
),
),
],
),
],
);
}
Widget _buildDateSection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Période de création',
style: AppTextStyles.titleMedium.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: () => _selectDate(context, true),
icon: const Icon(Icons.calendar_today),
label: Text(
_filtres.dateDebutCreation != null
? '${_filtres.dateDebutCreation!.day}/${_filtres.dateDebutCreation!.month}/${_filtres.dateDebutCreation!.year}'
: 'Date début',
),
),
),
const SizedBox(width: 16),
Expanded(
child: OutlinedButton.icon(
onPressed: () => _selectDate(context, false),
icon: const Icon(Icons.calendar_today),
label: Text(
_filtres.dateFinCreation != null
? '${_filtres.dateFinCreation!.day}/${_filtres.dateFinCreation!.month}/${_filtres.dateFinCreation!.year}'
: 'Date fin',
),
),
),
],
),
],
);
}
Widget _buildFilterChip({
required String label,
required bool isSelected,
required VoidCallback onSelected,
}) {
return FilterChip(
label: Text(label),
selected: isSelected,
onSelected: (_) => onSelected(),
selectedColor: AppColors.primary.withOpacity(0.2),
checkmarkColor: AppColors.primary,
);
}
Widget _buildActions() {
return Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: _reinitialiserFiltres,
child: const Text('Réinitialiser'),
),
),
const SizedBox(width: 16),
Expanded(
child: ElevatedButton(
onPressed: _appliquerFiltres,
child: Text('Appliquer (${_filtres.nombreFiltresActifs})'),
),
),
],
);
}
Future<void> _selectDate(BuildContext context, bool isStartDate) async {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: isStartDate
? _filtres.dateDebutCreation ?? DateTime.now()
: _filtres.dateFinCreation ?? DateTime.now(),
firstDate: DateTime(2020),
lastDate: DateTime.now(),
);
if (picked != null) {
setState(() {
if (isStartDate) {
_filtres = _filtres.copyWith(dateDebutCreation: picked);
} else {
_filtres = _filtres.copyWith(dateFinCreation: picked);
}
});
}
}
void _reinitialiserFiltres() {
setState(() {
_filtres = const FiltresDemandesAide();
_motCleController.clear();
_montantMinController.clear();
_montantMaxController.clear();
});
}
void _appliquerFiltres() {
widget.onFiltresChanged(_filtres);
Navigator.pop(context);
}
}

View File

@@ -0,0 +1,313 @@
import 'package:flutter/material.dart';
import '../../../../core/theme/app_colors.dart';
import '../../../../core/theme/app_text_styles.dart';
import '../bloc/demandes_aide/demandes_aide_event.dart';
/// Bottom sheet pour trier les demandes d'aide
///
/// Permet à l'utilisateur de sélectionner un critère de tri
/// et l'ordre (croissant/décroissant) pour la liste des demandes.
class DemandesAideSortBottomSheet extends StatefulWidget {
final TriDemandes? critereActuel;
final bool croissantActuel;
final Function(TriDemandes critere, bool croissant) onTriChanged;
const DemandesAideSortBottomSheet({
super.key,
this.critereActuel,
required this.croissantActuel,
required this.onTriChanged,
});
@override
State<DemandesAideSortBottomSheet> createState() => _DemandesAideSortBottomSheetState();
}
class _DemandesAideSortBottomSheetState extends State<DemandesAideSortBottomSheet> {
late TriDemandes? _critereSelectionne;
late bool _croissant;
@override
void initState() {
super.initState();
_critereSelectionne = widget.critereActuel;
_croissant = widget.croissantActuel;
}
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
const SizedBox(height: 16),
_buildCriteresList(),
const SizedBox(height: 16),
_buildOrdreSection(),
const SizedBox(height: 24),
_buildActions(),
],
),
);
}
Widget _buildHeader() {
return Row(
children: [
Text(
'Trier les demandes',
style: AppTextStyles.titleLarge.copyWith(
fontWeight: FontWeight.bold,
),
),
const Spacer(),
IconButton(
onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.close),
),
],
);
}
Widget _buildCriteresList() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Critère de tri',
style: AppTextStyles.titleMedium.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 8),
...TriDemandes.values.map((critere) => _buildCritereItem(critere)),
],
);
}
Widget _buildCritereItem(TriDemandes critere) {
final isSelected = _critereSelectionne == critere;
return Card(
margin: const EdgeInsets.symmetric(vertical: 4),
elevation: isSelected ? 2 : 0,
color: isSelected ? AppColors.primary.withOpacity(0.1) : null,
child: ListTile(
leading: Icon(
_getCritereIcon(critere),
color: isSelected ? AppColors.primary : AppColors.textSecondary,
),
title: Text(
critere.libelle,
style: AppTextStyles.bodyLarge.copyWith(
color: isSelected ? AppColors.primary : AppColors.textPrimary,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
),
),
subtitle: Text(
_getCritereDescription(critere),
style: AppTextStyles.bodySmall.copyWith(
color: isSelected ? AppColors.primary : AppColors.textSecondary,
),
),
trailing: isSelected
? Icon(
Icons.check_circle,
color: AppColors.primary,
)
: null,
onTap: () {
setState(() {
_critereSelectionne = critere;
});
},
),
);
}
Widget _buildOrdreSection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Ordre de tri',
style: AppTextStyles.titleMedium.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: Card(
elevation: _croissant ? 2 : 0,
color: _croissant ? AppColors.primary.withOpacity(0.1) : null,
child: ListTile(
leading: Icon(
Icons.arrow_upward,
color: _croissant ? AppColors.primary : AppColors.textSecondary,
),
title: Text(
'Croissant',
style: AppTextStyles.bodyMedium.copyWith(
color: _croissant ? AppColors.primary : AppColors.textPrimary,
fontWeight: _croissant ? FontWeight.w600 : FontWeight.normal,
),
),
subtitle: Text(
_getOrdreDescription(true),
style: AppTextStyles.bodySmall.copyWith(
color: _croissant ? AppColors.primary : AppColors.textSecondary,
),
),
trailing: _croissant
? Icon(
Icons.check_circle,
color: AppColors.primary,
)
: null,
onTap: () {
setState(() {
_croissant = true;
});
},
),
),
),
const SizedBox(width: 8),
Expanded(
child: Card(
elevation: !_croissant ? 2 : 0,
color: !_croissant ? AppColors.primary.withOpacity(0.1) : null,
child: ListTile(
leading: Icon(
Icons.arrow_downward,
color: !_croissant ? AppColors.primary : AppColors.textSecondary,
),
title: Text(
'Décroissant',
style: AppTextStyles.bodyMedium.copyWith(
color: !_croissant ? AppColors.primary : AppColors.textPrimary,
fontWeight: !_croissant ? FontWeight.w600 : FontWeight.normal,
),
),
subtitle: Text(
_getOrdreDescription(false),
style: AppTextStyles.bodySmall.copyWith(
color: !_croissant ? AppColors.primary : AppColors.textSecondary,
),
),
trailing: !_croissant
? Icon(
Icons.check_circle,
color: AppColors.primary,
)
: null,
onTap: () {
setState(() {
_croissant = false;
});
},
),
),
),
],
),
],
);
}
Widget _buildActions() {
return Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: _reinitialiserTri,
child: const Text('Réinitialiser'),
),
),
const SizedBox(width: 16),
Expanded(
child: ElevatedButton(
onPressed: _critereSelectionne != null ? _appliquerTri : null,
child: const Text('Appliquer'),
),
),
],
);
}
IconData _getCritereIcon(TriDemandes critere) {
switch (critere) {
case TriDemandes.dateCreation:
return Icons.calendar_today;
case TriDemandes.dateModification:
return Icons.update;
case TriDemandes.titre:
return Icons.title;
case TriDemandes.statut:
return Icons.flag;
case TriDemandes.priorite:
return Icons.priority_high;
case TriDemandes.montant:
return Icons.attach_money;
case TriDemandes.demandeur:
return Icons.person;
}
}
String _getCritereDescription(TriDemandes critere) {
switch (critere) {
case TriDemandes.dateCreation:
return 'Trier par date de création de la demande';
case TriDemandes.dateModification:
return 'Trier par date de dernière modification';
case TriDemandes.titre:
return 'Trier par titre de la demande (alphabétique)';
case TriDemandes.statut:
return 'Trier par statut de la demande';
case TriDemandes.priorite:
return 'Trier par niveau de priorité';
case TriDemandes.montant:
return 'Trier par montant demandé';
case TriDemandes.demandeur:
return 'Trier par nom du demandeur (alphabétique)';
}
}
String _getOrdreDescription(bool croissant) {
if (_critereSelectionne == null) return '';
switch (_critereSelectionne!) {
case TriDemandes.dateCreation:
case TriDemandes.dateModification:
return croissant ? 'Plus ancien en premier' : 'Plus récent en premier';
case TriDemandes.titre:
case TriDemandes.demandeur:
return croissant ? 'A à Z' : 'Z à A';
case TriDemandes.statut:
return croissant ? 'Brouillon à Terminée' : 'Terminée à Brouillon';
case TriDemandes.priorite:
return croissant ? 'Basse à Critique' : 'Critique à Basse';
case TriDemandes.montant:
return croissant ? 'Montant le plus faible' : 'Montant le plus élevé';
}
}
void _reinitialiserTri() {
setState(() {
_critereSelectionne = null;
_croissant = true;
});
}
void _appliquerTri() {
if (_critereSelectionne != null) {
widget.onTriChanged(_critereSelectionne!, _croissant);
Navigator.pop(context);
}
}
}