Refactoring
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()}',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>();
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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});
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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});
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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.';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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')),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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')),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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é
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user