Refactoring
This commit is contained in:
@@ -0,0 +1,323 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Énumération des types de métriques disponibles
|
||||
enum TypeMetrique {
|
||||
// Métriques membres
|
||||
nombreMembresActifs('Nombre de membres actifs', 'membres', 'count'),
|
||||
nombreMembresInactifs('Nombre de membres inactifs', 'membres', 'count'),
|
||||
tauxCroissanceMembres('Taux de croissance des membres', 'membres', 'percentage'),
|
||||
moyenneAgeMembres('Âge moyen des membres', 'membres', 'average'),
|
||||
|
||||
// Métriques financières
|
||||
totalCotisationsCollectees('Total des cotisations collectées', 'finance', 'amount'),
|
||||
cotisationsEnAttente('Cotisations en attente', 'finance', 'amount'),
|
||||
tauxRecouvrementCotisations('Taux de recouvrement', 'finance', 'percentage'),
|
||||
moyenneCotisationMembre('Cotisation moyenne par membre', 'finance', 'average'),
|
||||
|
||||
// Métriques événements
|
||||
nombreEvenementsOrganises('Nombre d\'événements organisés', 'evenements', 'count'),
|
||||
tauxParticipationEvenements('Taux de participation aux événements', 'evenements', 'percentage'),
|
||||
moyenneParticipantsEvenement('Moyenne de participants par événement', 'evenements', 'average'),
|
||||
|
||||
// Métriques solidarité
|
||||
nombreDemandesAide('Nombre de demandes d\'aide', 'solidarite', 'count'),
|
||||
montantAidesAccordees('Montant des aides accordées', 'solidarite', 'amount'),
|
||||
tauxApprobationAides('Taux d\'approbation des aides', 'solidarite', 'percentage');
|
||||
|
||||
const TypeMetrique(this.libelle, this.categorie, this.typeValeur);
|
||||
|
||||
final String libelle;
|
||||
final String categorie;
|
||||
final String typeValeur;
|
||||
|
||||
/// Retourne l'unité de mesure appropriée
|
||||
String get unite {
|
||||
switch (typeValeur) {
|
||||
case 'percentage':
|
||||
return '%';
|
||||
case 'amount':
|
||||
return 'XOF';
|
||||
case 'average':
|
||||
return typeValeur == 'moyenneAgeMembres' ? 'ans' : '';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/// Retourne l'icône Material Design appropriée
|
||||
String get icone {
|
||||
switch (categorie) {
|
||||
case 'membres':
|
||||
return 'people';
|
||||
case 'finance':
|
||||
return 'attach_money';
|
||||
case 'evenements':
|
||||
return 'event';
|
||||
case 'solidarite':
|
||||
return 'favorite';
|
||||
default:
|
||||
return 'analytics';
|
||||
}
|
||||
}
|
||||
|
||||
/// Retourne la couleur appropriée
|
||||
String get couleur {
|
||||
switch (categorie) {
|
||||
case 'membres':
|
||||
return '#2196F3';
|
||||
case 'finance':
|
||||
return '#4CAF50';
|
||||
case 'evenements':
|
||||
return '#FF9800';
|
||||
case 'solidarite':
|
||||
return '#E91E63';
|
||||
default:
|
||||
return '#757575';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Énumération des périodes d'analyse
|
||||
enum PeriodeAnalyse {
|
||||
aujourdHui('Aujourd\'hui', 'today'),
|
||||
hier('Hier', 'yesterday'),
|
||||
cetteSemaine('Cette semaine', 'this_week'),
|
||||
semaineDerniere('Semaine dernière', 'last_week'),
|
||||
ceMois('Ce mois', 'this_month'),
|
||||
moisDernier('Mois dernier', 'last_month'),
|
||||
troisDerniersMois('3 derniers mois', 'last_3_months'),
|
||||
sixDerniersMois('6 derniers mois', 'last_6_months'),
|
||||
cetteAnnee('Cette année', 'this_year'),
|
||||
anneeDerniere('Année dernière', 'last_year'),
|
||||
septDerniersJours('7 derniers jours', 'last_7_days'),
|
||||
trenteDerniersJours('30 derniers jours', 'last_30_days'),
|
||||
periodePersonnalisee('Période personnalisée', 'custom');
|
||||
|
||||
const PeriodeAnalyse(this.libelle, this.code);
|
||||
|
||||
final String libelle;
|
||||
final String code;
|
||||
|
||||
/// Vérifie si la période est courte (moins d'un mois)
|
||||
bool get isPeriodeCourte {
|
||||
return [
|
||||
aujourdHui,
|
||||
hier,
|
||||
cetteSemaine,
|
||||
semaineDerniere,
|
||||
septDerniersJours
|
||||
].contains(this);
|
||||
}
|
||||
|
||||
/// Vérifie si la période est longue (plus d'un an)
|
||||
bool get isPeriodeLongue {
|
||||
return [cetteAnnee, anneeDerniere].contains(this);
|
||||
}
|
||||
}
|
||||
|
||||
/// Entité représentant une donnée analytics
|
||||
class AnalyticsData extends Equatable {
|
||||
const AnalyticsData({
|
||||
required this.id,
|
||||
required this.typeMetrique,
|
||||
required this.periodeAnalyse,
|
||||
required this.valeur,
|
||||
this.valeurPrecedente,
|
||||
this.pourcentageEvolution,
|
||||
required this.dateDebut,
|
||||
required this.dateFin,
|
||||
required this.dateCalcul,
|
||||
this.organisationId,
|
||||
this.nomOrganisation,
|
||||
this.utilisateurId,
|
||||
this.nomUtilisateur,
|
||||
this.libellePersonnalise,
|
||||
this.description,
|
||||
this.donneesDetaillees,
|
||||
this.configurationGraphique,
|
||||
this.metadonnees,
|
||||
this.indicateurFiabilite = 95.0,
|
||||
this.nombreElementsAnalyses,
|
||||
this.tempsCalculMs,
|
||||
this.tempsReel = false,
|
||||
this.necessiteMiseAJour = false,
|
||||
this.niveauPriorite = 3,
|
||||
this.tags,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final TypeMetrique typeMetrique;
|
||||
final PeriodeAnalyse periodeAnalyse;
|
||||
final double valeur;
|
||||
final double? valeurPrecedente;
|
||||
final double? pourcentageEvolution;
|
||||
final DateTime dateDebut;
|
||||
final DateTime dateFin;
|
||||
final DateTime dateCalcul;
|
||||
final String? organisationId;
|
||||
final String? nomOrganisation;
|
||||
final String? utilisateurId;
|
||||
final String? nomUtilisateur;
|
||||
final String? libellePersonnalise;
|
||||
final String? description;
|
||||
final String? donneesDetaillees;
|
||||
final String? configurationGraphique;
|
||||
final Map<String, dynamic>? metadonnees;
|
||||
final double indicateurFiabilite;
|
||||
final int? nombreElementsAnalyses;
|
||||
final int? tempsCalculMs;
|
||||
final bool tempsReel;
|
||||
final bool necessiteMiseAJour;
|
||||
final int niveauPriorite;
|
||||
final List<String>? tags;
|
||||
|
||||
/// Retourne le libellé à afficher
|
||||
String get libelleAffichage {
|
||||
return libellePersonnalise?.isNotEmpty == true
|
||||
? libellePersonnalise!
|
||||
: typeMetrique.libelle;
|
||||
}
|
||||
|
||||
/// Retourne l'unité de mesure
|
||||
String get unite => typeMetrique.unite;
|
||||
|
||||
/// Retourne l'icône
|
||||
String get icone => typeMetrique.icone;
|
||||
|
||||
/// Retourne la couleur
|
||||
String get couleur => typeMetrique.couleur;
|
||||
|
||||
/// Vérifie si la métrique a évolué positivement
|
||||
bool get hasEvolutionPositive {
|
||||
return pourcentageEvolution != null && pourcentageEvolution! > 0;
|
||||
}
|
||||
|
||||
/// Vérifie si la métrique a évolué négativement
|
||||
bool get hasEvolutionNegative {
|
||||
return pourcentageEvolution != null && pourcentageEvolution! < 0;
|
||||
}
|
||||
|
||||
/// Vérifie si la métrique est stable
|
||||
bool get isStable {
|
||||
return pourcentageEvolution != null && pourcentageEvolution! == 0;
|
||||
}
|
||||
|
||||
/// Retourne la tendance sous forme de texte
|
||||
String get tendance {
|
||||
if (hasEvolutionPositive) return 'hausse';
|
||||
if (hasEvolutionNegative) return 'baisse';
|
||||
return 'stable';
|
||||
}
|
||||
|
||||
/// Vérifie si les données sont fiables
|
||||
bool get isDonneesFiables => indicateurFiabilite >= 80.0;
|
||||
|
||||
/// Vérifie si la métrique est critique
|
||||
bool get isCritique => niveauPriorite >= 4;
|
||||
|
||||
/// Formate la valeur avec l'unité appropriée
|
||||
String get valeurFormatee {
|
||||
switch (typeMetrique.typeValeur) {
|
||||
case 'amount':
|
||||
return '${valeur.toStringAsFixed(0)} ${unite}';
|
||||
case 'percentage':
|
||||
return '${valeur.toStringAsFixed(1)}${unite}';
|
||||
case 'average':
|
||||
return valeur.toStringAsFixed(1);
|
||||
default:
|
||||
return valeur.toStringAsFixed(0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Formate le pourcentage d'évolution
|
||||
String get evolutionFormatee {
|
||||
if (pourcentageEvolution == null) return '';
|
||||
final signe = pourcentageEvolution! >= 0 ? '+' : '';
|
||||
return '$signe${pourcentageEvolution!.toStringAsFixed(1)}%';
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
typeMetrique,
|
||||
periodeAnalyse,
|
||||
valeur,
|
||||
valeurPrecedente,
|
||||
pourcentageEvolution,
|
||||
dateDebut,
|
||||
dateFin,
|
||||
dateCalcul,
|
||||
organisationId,
|
||||
nomOrganisation,
|
||||
utilisateurId,
|
||||
nomUtilisateur,
|
||||
libellePersonnalise,
|
||||
description,
|
||||
donneesDetaillees,
|
||||
configurationGraphique,
|
||||
metadonnees,
|
||||
indicateurFiabilite,
|
||||
nombreElementsAnalyses,
|
||||
tempsCalculMs,
|
||||
tempsReel,
|
||||
necessiteMiseAJour,
|
||||
niveauPriorite,
|
||||
tags,
|
||||
];
|
||||
|
||||
AnalyticsData copyWith({
|
||||
String? id,
|
||||
TypeMetrique? typeMetrique,
|
||||
PeriodeAnalyse? periodeAnalyse,
|
||||
double? valeur,
|
||||
double? valeurPrecedente,
|
||||
double? pourcentageEvolution,
|
||||
DateTime? dateDebut,
|
||||
DateTime? dateFin,
|
||||
DateTime? dateCalcul,
|
||||
String? organisationId,
|
||||
String? nomOrganisation,
|
||||
String? utilisateurId,
|
||||
String? nomUtilisateur,
|
||||
String? libellePersonnalise,
|
||||
String? description,
|
||||
String? donneesDetaillees,
|
||||
String? configurationGraphique,
|
||||
Map<String, dynamic>? metadonnees,
|
||||
double? indicateurFiabilite,
|
||||
int? nombreElementsAnalyses,
|
||||
int? tempsCalculMs,
|
||||
bool? tempsReel,
|
||||
bool? necessiteMiseAJour,
|
||||
int? niveauPriorite,
|
||||
List<String>? tags,
|
||||
}) {
|
||||
return AnalyticsData(
|
||||
id: id ?? this.id,
|
||||
typeMetrique: typeMetrique ?? this.typeMetrique,
|
||||
periodeAnalyse: periodeAnalyse ?? this.periodeAnalyse,
|
||||
valeur: valeur ?? this.valeur,
|
||||
valeurPrecedente: valeurPrecedente ?? this.valeurPrecedente,
|
||||
pourcentageEvolution: pourcentageEvolution ?? this.pourcentageEvolution,
|
||||
dateDebut: dateDebut ?? this.dateDebut,
|
||||
dateFin: dateFin ?? this.dateFin,
|
||||
dateCalcul: dateCalcul ?? this.dateCalcul,
|
||||
organisationId: organisationId ?? this.organisationId,
|
||||
nomOrganisation: nomOrganisation ?? this.nomOrganisation,
|
||||
utilisateurId: utilisateurId ?? this.utilisateurId,
|
||||
nomUtilisateur: nomUtilisateur ?? this.nomUtilisateur,
|
||||
libellePersonnalise: libellePersonnalise ?? this.libellePersonnalise,
|
||||
description: description ?? this.description,
|
||||
donneesDetaillees: donneesDetaillees ?? this.donneesDetaillees,
|
||||
configurationGraphique: configurationGraphique ?? this.configurationGraphique,
|
||||
metadonnees: metadonnees ?? this.metadonnees,
|
||||
indicateurFiabilite: indicateurFiabilite ?? this.indicateurFiabilite,
|
||||
nombreElementsAnalyses: nombreElementsAnalyses ?? this.nombreElementsAnalyses,
|
||||
tempsCalculMs: tempsCalculMs ?? this.tempsCalculMs,
|
||||
tempsReel: tempsReel ?? this.tempsReel,
|
||||
necessiteMiseAJour: necessiteMiseAJour ?? this.necessiteMiseAJour,
|
||||
niveauPriorite: niveauPriorite ?? this.niveauPriorite,
|
||||
tags: tags ?? this.tags,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'analytics_data.dart';
|
||||
|
||||
/// Point de données pour une tendance KPI
|
||||
class PointDonnee extends Equatable {
|
||||
const PointDonnee({
|
||||
required this.date,
|
||||
required this.valeur,
|
||||
this.libelle,
|
||||
this.anomalie = false,
|
||||
this.prediction = false,
|
||||
this.metadonnees,
|
||||
});
|
||||
|
||||
final DateTime date;
|
||||
final double valeur;
|
||||
final String? libelle;
|
||||
final bool anomalie;
|
||||
final bool prediction;
|
||||
final String? metadonnees;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
date,
|
||||
valeur,
|
||||
libelle,
|
||||
anomalie,
|
||||
prediction,
|
||||
metadonnees,
|
||||
];
|
||||
|
||||
PointDonnee copyWith({
|
||||
DateTime? date,
|
||||
double? valeur,
|
||||
String? libelle,
|
||||
bool? anomalie,
|
||||
bool? prediction,
|
||||
String? metadonnees,
|
||||
}) {
|
||||
return PointDonnee(
|
||||
date: date ?? this.date,
|
||||
valeur: valeur ?? this.valeur,
|
||||
libelle: libelle ?? this.libelle,
|
||||
anomalie: anomalie ?? this.anomalie,
|
||||
prediction: prediction ?? this.prediction,
|
||||
metadonnees: metadonnees ?? this.metadonnees,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Entité représentant les tendances et évolutions d'un KPI
|
||||
class KPITrend extends Equatable {
|
||||
const KPITrend({
|
||||
required this.id,
|
||||
required this.typeMetrique,
|
||||
required this.periodeAnalyse,
|
||||
this.organisationId,
|
||||
this.nomOrganisation,
|
||||
required this.dateDebut,
|
||||
required this.dateFin,
|
||||
required this.pointsDonnees,
|
||||
required this.valeurActuelle,
|
||||
this.valeurMinimale,
|
||||
this.valeurMaximale,
|
||||
this.valeurMoyenne,
|
||||
this.ecartType,
|
||||
this.coefficientVariation,
|
||||
this.tendanceGenerale,
|
||||
this.coefficientCorrelation,
|
||||
this.pourcentageEvolutionGlobale,
|
||||
this.predictionProchainePeriode,
|
||||
this.margeErreurPrediction,
|
||||
this.seuilAlerteBas,
|
||||
this.seuilAlerteHaut,
|
||||
this.alerteActive = false,
|
||||
this.typeAlerte,
|
||||
this.messageAlerte,
|
||||
this.configurationGraphique,
|
||||
this.intervalleRegroupement,
|
||||
this.formatDate,
|
||||
this.dateDerniereMiseAJour,
|
||||
this.frequenceMiseAJourMinutes,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final TypeMetrique typeMetrique;
|
||||
final PeriodeAnalyse periodeAnalyse;
|
||||
final String? organisationId;
|
||||
final String? nomOrganisation;
|
||||
final DateTime dateDebut;
|
||||
final DateTime dateFin;
|
||||
final List<PointDonnee> pointsDonnees;
|
||||
final double valeurActuelle;
|
||||
final double? valeurMinimale;
|
||||
final double? valeurMaximale;
|
||||
final double? valeurMoyenne;
|
||||
final double? ecartType;
|
||||
final double? coefficientVariation;
|
||||
final double? tendanceGenerale;
|
||||
final double? coefficientCorrelation;
|
||||
final double? pourcentageEvolutionGlobale;
|
||||
final double? predictionProchainePeriode;
|
||||
final double? margeErreurPrediction;
|
||||
final double? seuilAlerteBas;
|
||||
final double? seuilAlerteHaut;
|
||||
final bool alerteActive;
|
||||
final String? typeAlerte;
|
||||
final String? messageAlerte;
|
||||
final String? configurationGraphique;
|
||||
final String? intervalleRegroupement;
|
||||
final String? formatDate;
|
||||
final DateTime? dateDerniereMiseAJour;
|
||||
final int? frequenceMiseAJourMinutes;
|
||||
|
||||
/// Retourne le libellé de la métrique
|
||||
String get libelleMetrique => typeMetrique.libelle;
|
||||
|
||||
/// Retourne l'unité de mesure
|
||||
String get unite => typeMetrique.unite;
|
||||
|
||||
/// Retourne l'icône de la métrique
|
||||
String get icone => typeMetrique.icone;
|
||||
|
||||
/// Retourne la couleur de la métrique
|
||||
String get couleur => typeMetrique.couleur;
|
||||
|
||||
/// Vérifie si la tendance est positive
|
||||
bool get isTendancePositive {
|
||||
return tendanceGenerale != null && tendanceGenerale! > 0;
|
||||
}
|
||||
|
||||
/// Vérifie si la tendance est négative
|
||||
bool get isTendanceNegative {
|
||||
return tendanceGenerale != null && tendanceGenerale! < 0;
|
||||
}
|
||||
|
||||
/// Vérifie si la tendance est stable
|
||||
bool get isTendanceStable {
|
||||
return tendanceGenerale != null && tendanceGenerale! == 0;
|
||||
}
|
||||
|
||||
/// Retourne la volatilité du KPI
|
||||
String get volatilite {
|
||||
if (coefficientVariation == null) return 'inconnue';
|
||||
|
||||
if (coefficientVariation! <= 0.1) return 'faible';
|
||||
if (coefficientVariation! <= 0.3) return 'moyenne';
|
||||
return 'élevée';
|
||||
}
|
||||
|
||||
/// Vérifie si la prédiction est fiable
|
||||
bool get isPredictionFiable {
|
||||
return coefficientCorrelation != null && coefficientCorrelation! >= 0.7;
|
||||
}
|
||||
|
||||
/// Retourne le nombre de points de données
|
||||
int get nombrePointsDonnees => pointsDonnees.length;
|
||||
|
||||
/// Vérifie si des anomalies ont été détectées
|
||||
bool get hasAnomalies {
|
||||
return pointsDonnees.any((point) => point.anomalie);
|
||||
}
|
||||
|
||||
/// Retourne les points d'anomalies
|
||||
List<PointDonnee> get pointsAnomalies {
|
||||
return pointsDonnees.where((point) => point.anomalie).toList();
|
||||
}
|
||||
|
||||
/// Retourne les points de prédiction
|
||||
List<PointDonnee> get pointsPredictions {
|
||||
return pointsDonnees.where((point) => point.prediction).toList();
|
||||
}
|
||||
|
||||
/// Formate la valeur actuelle
|
||||
String get valeurActuelleFormatee {
|
||||
switch (typeMetrique.typeValeur) {
|
||||
case 'amount':
|
||||
return '${valeurActuelle.toStringAsFixed(0)} ${unite}';
|
||||
case 'percentage':
|
||||
return '${valeurActuelle.toStringAsFixed(1)}${unite}';
|
||||
case 'average':
|
||||
return valeurActuelle.toStringAsFixed(1);
|
||||
default:
|
||||
return valeurActuelle.toStringAsFixed(0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Formate l'évolution globale
|
||||
String get evolutionGlobaleFormatee {
|
||||
if (pourcentageEvolutionGlobale == null) return '';
|
||||
final signe = pourcentageEvolutionGlobale! >= 0 ? '+' : '';
|
||||
return '$signe${pourcentageEvolutionGlobale!.toStringAsFixed(1)}%';
|
||||
}
|
||||
|
||||
/// Formate la prédiction
|
||||
String get predictionFormatee {
|
||||
if (predictionProchainePeriode == null) return '';
|
||||
|
||||
switch (typeMetrique.typeValeur) {
|
||||
case 'amount':
|
||||
return '${predictionProchainePeriode!.toStringAsFixed(0)} ${unite}';
|
||||
case 'percentage':
|
||||
return '${predictionProchainePeriode!.toStringAsFixed(1)}${unite}';
|
||||
case 'average':
|
||||
return predictionProchainePeriode!.toStringAsFixed(1);
|
||||
default:
|
||||
return predictionProchainePeriode!.toStringAsFixed(0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Retourne la description de la tendance
|
||||
String get descriptionTendance {
|
||||
if (isTendancePositive) {
|
||||
return 'Tendance à la hausse';
|
||||
} else if (isTendanceNegative) {
|
||||
return 'Tendance à la baisse';
|
||||
} else {
|
||||
return 'Tendance stable';
|
||||
}
|
||||
}
|
||||
|
||||
/// Retourne l'icône de la tendance
|
||||
String get iconeTendance {
|
||||
if (isTendancePositive) {
|
||||
return 'trending_up';
|
||||
} else if (isTendanceNegative) {
|
||||
return 'trending_down';
|
||||
} else {
|
||||
return 'trending_flat';
|
||||
}
|
||||
}
|
||||
|
||||
/// Retourne la couleur de la tendance
|
||||
String get couleurTendance {
|
||||
if (isTendancePositive) {
|
||||
return '#4CAF50'; // Vert
|
||||
} else if (isTendanceNegative) {
|
||||
return '#F44336'; // Rouge
|
||||
} else {
|
||||
return '#FF9800'; // Orange
|
||||
}
|
||||
}
|
||||
|
||||
/// Retourne le niveau de confiance de la prédiction
|
||||
String get niveauConfiancePrediction {
|
||||
if (coefficientCorrelation == null) return 'Inconnu';
|
||||
|
||||
if (coefficientCorrelation! >= 0.9) return 'Très élevé';
|
||||
if (coefficientCorrelation! >= 0.7) return 'Élevé';
|
||||
if (coefficientCorrelation! >= 0.5) return 'Moyen';
|
||||
if (coefficientCorrelation! >= 0.3) return 'Faible';
|
||||
return 'Très faible';
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
typeMetrique,
|
||||
periodeAnalyse,
|
||||
organisationId,
|
||||
nomOrganisation,
|
||||
dateDebut,
|
||||
dateFin,
|
||||
pointsDonnees,
|
||||
valeurActuelle,
|
||||
valeurMinimale,
|
||||
valeurMaximale,
|
||||
valeurMoyenne,
|
||||
ecartType,
|
||||
coefficientVariation,
|
||||
tendanceGenerale,
|
||||
coefficientCorrelation,
|
||||
pourcentageEvolutionGlobale,
|
||||
predictionProchainePeriode,
|
||||
margeErreurPrediction,
|
||||
seuilAlerteBas,
|
||||
seuilAlerteHaut,
|
||||
alerteActive,
|
||||
typeAlerte,
|
||||
messageAlerte,
|
||||
configurationGraphique,
|
||||
intervalleRegroupement,
|
||||
formatDate,
|
||||
dateDerniereMiseAJour,
|
||||
frequenceMiseAJourMinutes,
|
||||
];
|
||||
|
||||
KPITrend copyWith({
|
||||
String? id,
|
||||
TypeMetrique? typeMetrique,
|
||||
PeriodeAnalyse? periodeAnalyse,
|
||||
String? organisationId,
|
||||
String? nomOrganisation,
|
||||
DateTime? dateDebut,
|
||||
DateTime? dateFin,
|
||||
List<PointDonnee>? pointsDonnees,
|
||||
double? valeurActuelle,
|
||||
double? valeurMinimale,
|
||||
double? valeurMaximale,
|
||||
double? valeurMoyenne,
|
||||
double? ecartType,
|
||||
double? coefficientVariation,
|
||||
double? tendanceGenerale,
|
||||
double? coefficientCorrelation,
|
||||
double? pourcentageEvolutionGlobale,
|
||||
double? predictionProchainePeriode,
|
||||
double? margeErreurPrediction,
|
||||
double? seuilAlerteBas,
|
||||
double? seuilAlerteHaut,
|
||||
bool? alerteActive,
|
||||
String? typeAlerte,
|
||||
String? messageAlerte,
|
||||
String? configurationGraphique,
|
||||
String? intervalleRegroupement,
|
||||
String? formatDate,
|
||||
DateTime? dateDerniereMiseAJour,
|
||||
int? frequenceMiseAJourMinutes,
|
||||
}) {
|
||||
return KPITrend(
|
||||
id: id ?? this.id,
|
||||
typeMetrique: typeMetrique ?? this.typeMetrique,
|
||||
periodeAnalyse: periodeAnalyse ?? this.periodeAnalyse,
|
||||
organisationId: organisationId ?? this.organisationId,
|
||||
nomOrganisation: nomOrganisation ?? this.nomOrganisation,
|
||||
dateDebut: dateDebut ?? this.dateDebut,
|
||||
dateFin: dateFin ?? this.dateFin,
|
||||
pointsDonnees: pointsDonnees ?? this.pointsDonnees,
|
||||
valeurActuelle: valeurActuelle ?? this.valeurActuelle,
|
||||
valeurMinimale: valeurMinimale ?? this.valeurMinimale,
|
||||
valeurMaximale: valeurMaximale ?? this.valeurMaximale,
|
||||
valeurMoyenne: valeurMoyenne ?? this.valeurMoyenne,
|
||||
ecartType: ecartType ?? this.ecartType,
|
||||
coefficientVariation: coefficientVariation ?? this.coefficientVariation,
|
||||
tendanceGenerale: tendanceGenerale ?? this.tendanceGenerale,
|
||||
coefficientCorrelation: coefficientCorrelation ?? this.coefficientCorrelation,
|
||||
pourcentageEvolutionGlobale: pourcentageEvolutionGlobale ?? this.pourcentageEvolutionGlobale,
|
||||
predictionProchainePeriode: predictionProchainePeriode ?? this.predictionProchainePeriode,
|
||||
margeErreurPrediction: margeErreurPrediction ?? this.margeErreurPrediction,
|
||||
seuilAlerteBas: seuilAlerteBas ?? this.seuilAlerteBas,
|
||||
seuilAlerteHaut: seuilAlerteHaut ?? this.seuilAlerteHaut,
|
||||
alerteActive: alerteActive ?? this.alerteActive,
|
||||
typeAlerte: typeAlerte ?? this.typeAlerte,
|
||||
messageAlerte: messageAlerte ?? this.messageAlerte,
|
||||
configurationGraphique: configurationGraphique ?? this.configurationGraphique,
|
||||
intervalleRegroupement: intervalleRegroupement ?? this.intervalleRegroupement,
|
||||
formatDate: formatDate ?? this.formatDate,
|
||||
dateDerniereMiseAJour: dateDerniereMiseAJour ?? this.dateDerniereMiseAJour,
|
||||
frequenceMiseAJourMinutes: frequenceMiseAJourMinutes ?? this.frequenceMiseAJourMinutes,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import '../../../../core/error/failures.dart';
|
||||
import '../entities/analytics_data.dart';
|
||||
import '../entities/kpi_trend.dart';
|
||||
|
||||
/// Repository abstrait pour les analytics
|
||||
abstract class AnalyticsRepository {
|
||||
/// Calcule une métrique analytics pour une période donnée
|
||||
Future<Either<Failure, AnalyticsData>> calculerMetrique({
|
||||
required TypeMetrique typeMetrique,
|
||||
required PeriodeAnalyse periodeAnalyse,
|
||||
String? organisationId,
|
||||
});
|
||||
|
||||
/// Calcule les tendances d'un KPI sur une période
|
||||
Future<Either<Failure, KPITrend>> calculerTendanceKPI({
|
||||
required TypeMetrique typeMetrique,
|
||||
required PeriodeAnalyse periodeAnalyse,
|
||||
String? organisationId,
|
||||
});
|
||||
|
||||
/// Obtient tous les KPI pour une organisation
|
||||
Future<Either<Failure, Map<TypeMetrique, double>>> obtenirTousLesKPI({
|
||||
required PeriodeAnalyse periodeAnalyse,
|
||||
String? organisationId,
|
||||
});
|
||||
|
||||
/// Calcule le KPI de performance globale
|
||||
Future<Either<Failure, double>> calculerPerformanceGlobale({
|
||||
required PeriodeAnalyse periodeAnalyse,
|
||||
String? organisationId,
|
||||
});
|
||||
|
||||
/// Obtient les évolutions des KPI par rapport à la période précédente
|
||||
Future<Either<Failure, Map<TypeMetrique, double>>> obtenirEvolutionsKPI({
|
||||
required PeriodeAnalyse periodeAnalyse,
|
||||
String? organisationId,
|
||||
});
|
||||
|
||||
/// Obtient les métriques pour le tableau de bord
|
||||
Future<Either<Failure, List<AnalyticsData>>> obtenirMetriquesTableauBord({
|
||||
String? organisationId,
|
||||
required String utilisateurId,
|
||||
});
|
||||
|
||||
/// Obtient les types de métriques disponibles
|
||||
Future<Either<Failure, List<TypeMetrique>>> obtenirTypesMetriques();
|
||||
|
||||
/// Obtient les périodes d'analyse disponibles
|
||||
Future<Either<Failure, List<PeriodeAnalyse>>> obtenirPeriodesAnalyse();
|
||||
|
||||
/// Met en cache les données analytics
|
||||
Future<Either<Failure, void>> mettreEnCache({
|
||||
required String cle,
|
||||
required Map<String, dynamic> donnees,
|
||||
Duration? dureeVie,
|
||||
});
|
||||
|
||||
/// Récupère les données depuis le cache
|
||||
Future<Either<Failure, Map<String, dynamic>?>> recupererDepuisCache({
|
||||
required String cle,
|
||||
});
|
||||
|
||||
/// Vide le cache analytics
|
||||
Future<Either<Failure, void>> viderCache();
|
||||
|
||||
/// Synchronise les données analytics avec le serveur
|
||||
Future<Either<Failure, void>> synchroniserDonnees();
|
||||
|
||||
/// Vérifie si les données sont à jour
|
||||
Future<Either<Failure, bool>> verifierMiseAJour({
|
||||
required TypeMetrique typeMetrique,
|
||||
required PeriodeAnalyse periodeAnalyse,
|
||||
String? organisationId,
|
||||
});
|
||||
|
||||
/// Obtient les alertes actives
|
||||
Future<Either<Failure, List<AnalyticsData>>> obtenirAlertesActives({
|
||||
String? organisationId,
|
||||
});
|
||||
|
||||
/// Marque une alerte comme lue
|
||||
Future<Either<Failure, void>> marquerAlerteLue({
|
||||
required String alerteId,
|
||||
});
|
||||
|
||||
/// Exporte les données analytics
|
||||
Future<Either<Failure, String>> exporterDonnees({
|
||||
required List<TypeMetrique> metriques,
|
||||
required PeriodeAnalyse periodeAnalyse,
|
||||
String? organisationId,
|
||||
required String format, // 'json', 'csv', 'excel'
|
||||
});
|
||||
|
||||
/// Obtient l'historique des calculs
|
||||
Future<Either<Failure, List<AnalyticsData>>> obtenirHistoriqueCalculs({
|
||||
required TypeMetrique typeMetrique,
|
||||
String? organisationId,
|
||||
int limite = 50,
|
||||
});
|
||||
|
||||
/// Sauvegarde une configuration de rapport personnalisé
|
||||
Future<Either<Failure, void>> sauvegarderConfigurationRapport({
|
||||
required String nom,
|
||||
required List<TypeMetrique> metriques,
|
||||
required PeriodeAnalyse periodeAnalyse,
|
||||
String? organisationId,
|
||||
Map<String, dynamic>? configuration,
|
||||
});
|
||||
|
||||
/// Obtient les configurations de rapports sauvegardées
|
||||
Future<Either<Failure, List<Map<String, dynamic>>>> obtenirConfigurationsRapports({
|
||||
String? organisationId,
|
||||
});
|
||||
|
||||
/// Supprime une configuration de rapport
|
||||
Future<Either<Failure, void>> supprimerConfigurationRapport({
|
||||
required String configurationId,
|
||||
});
|
||||
|
||||
/// Planifie une mise à jour automatique
|
||||
Future<Either<Failure, void>> planifierMiseAJourAutomatique({
|
||||
required TypeMetrique typeMetrique,
|
||||
required PeriodeAnalyse periodeAnalyse,
|
||||
String? organisationId,
|
||||
required Duration frequence,
|
||||
});
|
||||
|
||||
/// Annule une mise à jour automatique planifiée
|
||||
Future<Either<Failure, void>> annulerMiseAJourAutomatique({
|
||||
required String planificationId,
|
||||
});
|
||||
|
||||
/// Obtient les statistiques d'utilisation des analytics
|
||||
Future<Either<Failure, Map<String, dynamic>>> obtenirStatistiquesUtilisation({
|
||||
String? organisationId,
|
||||
String? utilisateurId,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import '../../../../core/error/failures.dart';
|
||||
import '../../../../core/usecases/usecase.dart';
|
||||
import '../entities/analytics_data.dart';
|
||||
import '../repositories/analytics_repository.dart';
|
||||
|
||||
/// Use case pour calculer une métrique analytics
|
||||
class CalculerMetriqueUseCase implements UseCase<AnalyticsData, CalculerMetriqueParams> {
|
||||
const CalculerMetriqueUseCase(this.repository);
|
||||
|
||||
final AnalyticsRepository repository;
|
||||
|
||||
@override
|
||||
Future<Either<Failure, AnalyticsData>> call(CalculerMetriqueParams params) async {
|
||||
// Vérifier d'abord le cache
|
||||
final cacheKey = _genererCleCacheMetrique(params);
|
||||
final cacheResult = await repository.recupererDepuisCache(cle: cacheKey);
|
||||
|
||||
return cacheResult.fold(
|
||||
(failure) => _calculerEtCacherMetrique(params, cacheKey),
|
||||
(cachedData) {
|
||||
if (cachedData != null && _isCacheValide(cachedData)) {
|
||||
// Retourner les données du cache si elles sont valides
|
||||
return Right(_mapCacheToAnalyticsData(cachedData));
|
||||
} else {
|
||||
// Recalculer si le cache est expiré ou invalide
|
||||
return _calculerEtCacherMetrique(params, cacheKey);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Calcule la métrique et la met en cache
|
||||
Future<Either<Failure, AnalyticsData>> _calculerEtCacherMetrique(
|
||||
CalculerMetriqueParams params,
|
||||
String cacheKey,
|
||||
) async {
|
||||
final result = await repository.calculerMetrique(
|
||||
typeMetrique: params.typeMetrique,
|
||||
periodeAnalyse: params.periodeAnalyse,
|
||||
organisationId: params.organisationId,
|
||||
);
|
||||
|
||||
return result.fold(
|
||||
(failure) => Left(failure),
|
||||
(analyticsData) async {
|
||||
// Mettre en cache le résultat
|
||||
await repository.mettreEnCache(
|
||||
cle: cacheKey,
|
||||
donnees: _mapAnalyticsDataToCache(analyticsData),
|
||||
dureeVie: _determinerDureeVieCache(params.periodeAnalyse),
|
||||
);
|
||||
|
||||
return Right(analyticsData);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Génère une clé de cache unique pour la métrique
|
||||
String _genererCleCacheMetrique(CalculerMetriqueParams params) {
|
||||
return 'metrique_${params.typeMetrique.name}_${params.periodeAnalyse.name}_${params.organisationId ?? 'global'}';
|
||||
}
|
||||
|
||||
/// Vérifie si les données du cache sont encore valides
|
||||
bool _isCacheValide(Map<String, dynamic> cachedData) {
|
||||
final dateCache = DateTime.tryParse(cachedData['dateCache'] ?? '');
|
||||
if (dateCache == null) return false;
|
||||
|
||||
final dureeVie = Duration(minutes: cachedData['dureeVieMinutes'] ?? 60);
|
||||
return DateTime.now().difference(dateCache) < dureeVie;
|
||||
}
|
||||
|
||||
/// Convertit les données analytics en format cache
|
||||
Map<String, dynamic> _mapAnalyticsDataToCache(AnalyticsData data) {
|
||||
return {
|
||||
'id': data.id,
|
||||
'typeMetrique': data.typeMetrique.name,
|
||||
'periodeAnalyse': data.periodeAnalyse.name,
|
||||
'valeur': data.valeur,
|
||||
'valeurPrecedente': data.valeurPrecedente,
|
||||
'pourcentageEvolution': data.pourcentageEvolution,
|
||||
'dateDebut': data.dateDebut.toIso8601String(),
|
||||
'dateFin': data.dateFin.toIso8601String(),
|
||||
'dateCalcul': data.dateCalcul.toIso8601String(),
|
||||
'organisationId': data.organisationId,
|
||||
'nomOrganisation': data.nomOrganisation,
|
||||
'utilisateurId': data.utilisateurId,
|
||||
'nomUtilisateur': data.nomUtilisateur,
|
||||
'libellePersonnalise': data.libellePersonnalise,
|
||||
'description': data.description,
|
||||
'donneesDetaillees': data.donneesDetaillees,
|
||||
'configurationGraphique': data.configurationGraphique,
|
||||
'metadonnees': data.metadonnees,
|
||||
'indicateurFiabilite': data.indicateurFiabilite,
|
||||
'nombreElementsAnalyses': data.nombreElementsAnalyses,
|
||||
'tempsCalculMs': data.tempsCalculMs,
|
||||
'tempsReel': data.tempsReel,
|
||||
'necessiteMiseAJour': data.necessiteMiseAJour,
|
||||
'niveauPriorite': data.niveauPriorite,
|
||||
'tags': data.tags,
|
||||
'dateCache': DateTime.now().toIso8601String(),
|
||||
'dureeVieMinutes': _determinerDureeVieCache(data.periodeAnalyse).inMinutes,
|
||||
};
|
||||
}
|
||||
|
||||
/// Convertit les données du cache en AnalyticsData
|
||||
AnalyticsData _mapCacheToAnalyticsData(Map<String, dynamic> cachedData) {
|
||||
return AnalyticsData(
|
||||
id: cachedData['id'],
|
||||
typeMetrique: TypeMetrique.values.firstWhere(
|
||||
(e) => e.name == cachedData['typeMetrique'],
|
||||
),
|
||||
periodeAnalyse: PeriodeAnalyse.values.firstWhere(
|
||||
(e) => e.name == cachedData['periodeAnalyse'],
|
||||
),
|
||||
valeur: cachedData['valeur']?.toDouble() ?? 0.0,
|
||||
valeurPrecedente: cachedData['valeurPrecedente']?.toDouble(),
|
||||
pourcentageEvolution: cachedData['pourcentageEvolution']?.toDouble(),
|
||||
dateDebut: DateTime.parse(cachedData['dateDebut']),
|
||||
dateFin: DateTime.parse(cachedData['dateFin']),
|
||||
dateCalcul: DateTime.parse(cachedData['dateCalcul']),
|
||||
organisationId: cachedData['organisationId'],
|
||||
nomOrganisation: cachedData['nomOrganisation'],
|
||||
utilisateurId: cachedData['utilisateurId'],
|
||||
nomUtilisateur: cachedData['nomUtilisateur'],
|
||||
libellePersonnalise: cachedData['libellePersonnalise'],
|
||||
description: cachedData['description'],
|
||||
donneesDetaillees: cachedData['donneesDetaillees'],
|
||||
configurationGraphique: cachedData['configurationGraphique'],
|
||||
metadonnees: cachedData['metadonnees'] != null
|
||||
? Map<String, dynamic>.from(cachedData['metadonnees'])
|
||||
: null,
|
||||
indicateurFiabilite: cachedData['indicateurFiabilite']?.toDouble() ?? 95.0,
|
||||
nombreElementsAnalyses: cachedData['nombreElementsAnalyses'],
|
||||
tempsCalculMs: cachedData['tempsCalculMs'],
|
||||
tempsReel: cachedData['tempsReel'] ?? false,
|
||||
necessiteMiseAJour: cachedData['necessiteMiseAJour'] ?? false,
|
||||
niveauPriorite: cachedData['niveauPriorite'] ?? 3,
|
||||
tags: cachedData['tags'] != null
|
||||
? List<String>.from(cachedData['tags'])
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
/// Détermine la durée de vie du cache selon la période
|
||||
Duration _determinerDureeVieCache(PeriodeAnalyse periode) {
|
||||
switch (periode) {
|
||||
case PeriodeAnalyse.aujourdHui:
|
||||
case PeriodeAnalyse.hier:
|
||||
return const Duration(minutes: 15); // 15 minutes pour les données récentes
|
||||
case PeriodeAnalyse.cetteSemaine:
|
||||
case PeriodeAnalyse.semaineDerniere:
|
||||
case PeriodeAnalyse.septDerniersJours:
|
||||
return const Duration(hours: 1); // 1 heure pour les données hebdomadaires
|
||||
case PeriodeAnalyse.ceMois:
|
||||
case PeriodeAnalyse.moisDernier:
|
||||
case PeriodeAnalyse.trenteDerniersJours:
|
||||
return const Duration(hours: 4); // 4 heures pour les données mensuelles
|
||||
case PeriodeAnalyse.troisDerniersMois:
|
||||
case PeriodeAnalyse.sixDerniersMois:
|
||||
return const Duration(hours: 12); // 12 heures pour les données trimestrielles
|
||||
case PeriodeAnalyse.cetteAnnee:
|
||||
case PeriodeAnalyse.anneeDerniere:
|
||||
return const Duration(days: 1); // 1 jour pour les données annuelles
|
||||
case PeriodeAnalyse.periodePersonnalisee:
|
||||
return const Duration(hours: 2); // 2 heures par défaut
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Paramètres pour le use case CalculerMetrique
|
||||
class CalculerMetriqueParams extends Equatable {
|
||||
const CalculerMetriqueParams({
|
||||
required this.typeMetrique,
|
||||
required this.periodeAnalyse,
|
||||
this.organisationId,
|
||||
this.forceRecalcul = false,
|
||||
});
|
||||
|
||||
final TypeMetrique typeMetrique;
|
||||
final PeriodeAnalyse periodeAnalyse;
|
||||
final String? organisationId;
|
||||
final bool forceRecalcul;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
typeMetrique,
|
||||
periodeAnalyse,
|
||||
organisationId,
|
||||
forceRecalcul,
|
||||
];
|
||||
|
||||
CalculerMetriqueParams copyWith({
|
||||
TypeMetrique? typeMetrique,
|
||||
PeriodeAnalyse? periodeAnalyse,
|
||||
String? organisationId,
|
||||
bool? forceRecalcul,
|
||||
}) {
|
||||
return CalculerMetriqueParams(
|
||||
typeMetrique: typeMetrique ?? this.typeMetrique,
|
||||
periodeAnalyse: periodeAnalyse ?? this.periodeAnalyse,
|
||||
organisationId: organisationId ?? this.organisationId,
|
||||
forceRecalcul: forceRecalcul ?? this.forceRecalcul,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import '../../../../core/error/failures.dart';
|
||||
import '../../../../core/usecases/usecase.dart';
|
||||
import '../entities/analytics_data.dart';
|
||||
import '../entities/kpi_trend.dart';
|
||||
import '../repositories/analytics_repository.dart';
|
||||
|
||||
/// Use case pour calculer les tendances d'un KPI
|
||||
class CalculerTendanceKPIUseCase implements UseCase<KPITrend, CalculerTendanceKPIParams> {
|
||||
const CalculerTendanceKPIUseCase(this.repository);
|
||||
|
||||
final AnalyticsRepository repository;
|
||||
|
||||
@override
|
||||
Future<Either<Failure, KPITrend>> call(CalculerTendanceKPIParams params) async {
|
||||
// Vérifier d'abord le cache si pas de recalcul forcé
|
||||
if (!params.forceRecalcul) {
|
||||
final cacheKey = _genererCleCacheTendance(params);
|
||||
final cacheResult = await repository.recupererDepuisCache(cle: cacheKey);
|
||||
|
||||
final cachedTrend = await cacheResult.fold(
|
||||
(failure) => null,
|
||||
(cachedData) {
|
||||
if (cachedData != null && _isCacheValide(cachedData)) {
|
||||
return _mapCacheToKPITrend(cachedData);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
);
|
||||
|
||||
if (cachedTrend != null) {
|
||||
return Right(cachedTrend);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculer la tendance depuis le serveur
|
||||
return _calculerEtCacherTendance(params);
|
||||
}
|
||||
|
||||
/// Calcule la tendance et la met en cache
|
||||
Future<Either<Failure, KPITrend>> _calculerEtCacherTendance(
|
||||
CalculerTendanceKPIParams params,
|
||||
) async {
|
||||
final result = await repository.calculerTendanceKPI(
|
||||
typeMetrique: params.typeMetrique,
|
||||
periodeAnalyse: params.periodeAnalyse,
|
||||
organisationId: params.organisationId,
|
||||
);
|
||||
|
||||
return result.fold(
|
||||
(failure) => Left(failure),
|
||||
(kpiTrend) async {
|
||||
// Mettre en cache le résultat
|
||||
final cacheKey = _genererCleCacheTendance(params);
|
||||
await repository.mettreEnCache(
|
||||
cle: cacheKey,
|
||||
donnees: _mapKPITrendToCache(kpiTrend),
|
||||
dureeVie: _determinerDureeVieCache(params.periodeAnalyse),
|
||||
);
|
||||
|
||||
return Right(kpiTrend);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Génère une clé de cache unique pour la tendance
|
||||
String _genererCleCacheTendance(CalculerTendanceKPIParams params) {
|
||||
return 'tendance_${params.typeMetrique.name}_${params.periodeAnalyse.name}_${params.organisationId ?? 'global'}';
|
||||
}
|
||||
|
||||
/// Vérifie si les données du cache sont encore valides
|
||||
bool _isCacheValide(Map<String, dynamic> cachedData) {
|
||||
final dateCache = DateTime.tryParse(cachedData['dateCache'] ?? '');
|
||||
if (dateCache == null) return false;
|
||||
|
||||
final dureeVie = Duration(minutes: cachedData['dureeVieMinutes'] ?? 120);
|
||||
return DateTime.now().difference(dateCache) < dureeVie;
|
||||
}
|
||||
|
||||
/// Convertit KPITrend en format cache
|
||||
Map<String, dynamic> _mapKPITrendToCache(KPITrend trend) {
|
||||
return {
|
||||
'id': trend.id,
|
||||
'typeMetrique': trend.typeMetrique.name,
|
||||
'periodeAnalyse': trend.periodeAnalyse.name,
|
||||
'organisationId': trend.organisationId,
|
||||
'nomOrganisation': trend.nomOrganisation,
|
||||
'dateDebut': trend.dateDebut.toIso8601String(),
|
||||
'dateFin': trend.dateFin.toIso8601String(),
|
||||
'pointsDonnees': trend.pointsDonnees.map((point) => {
|
||||
'date': point.date.toIso8601String(),
|
||||
'valeur': point.valeur,
|
||||
'libelle': point.libelle,
|
||||
'anomalie': point.anomalie,
|
||||
'prediction': point.prediction,
|
||||
'metadonnees': point.metadonnees,
|
||||
}).toList(),
|
||||
'valeurActuelle': trend.valeurActuelle,
|
||||
'valeurMinimale': trend.valeurMinimale,
|
||||
'valeurMaximale': trend.valeurMaximale,
|
||||
'valeurMoyenne': trend.valeurMoyenne,
|
||||
'ecartType': trend.ecartType,
|
||||
'coefficientVariation': trend.coefficientVariation,
|
||||
'tendanceGenerale': trend.tendanceGenerale,
|
||||
'coefficientCorrelation': trend.coefficientCorrelation,
|
||||
'pourcentageEvolutionGlobale': trend.pourcentageEvolutionGlobale,
|
||||
'predictionProchainePeriode': trend.predictionProchainePeriode,
|
||||
'margeErreurPrediction': trend.margeErreurPrediction,
|
||||
'seuilAlerteBas': trend.seuilAlerteBas,
|
||||
'seuilAlerteHaut': trend.seuilAlerteHaut,
|
||||
'alerteActive': trend.alerteActive,
|
||||
'typeAlerte': trend.typeAlerte,
|
||||
'messageAlerte': trend.messageAlerte,
|
||||
'configurationGraphique': trend.configurationGraphique,
|
||||
'intervalleRegroupement': trend.intervalleRegroupement,
|
||||
'formatDate': trend.formatDate,
|
||||
'dateDerniereMiseAJour': trend.dateDerniereMiseAJour?.toIso8601String(),
|
||||
'frequenceMiseAJourMinutes': trend.frequenceMiseAJourMinutes,
|
||||
'dateCache': DateTime.now().toIso8601String(),
|
||||
'dureeVieMinutes': _determinerDureeVieCache(trend.periodeAnalyse).inMinutes,
|
||||
};
|
||||
}
|
||||
|
||||
/// Convertit les données du cache en KPITrend
|
||||
KPITrend _mapCacheToKPITrend(Map<String, dynamic> cachedData) {
|
||||
final pointsDonneesList = cachedData['pointsDonnees'] as List<dynamic>? ?? [];
|
||||
final pointsDonnees = pointsDonneesList.map((pointData) {
|
||||
return PointDonnee(
|
||||
date: DateTime.parse(pointData['date']),
|
||||
valeur: pointData['valeur']?.toDouble() ?? 0.0,
|
||||
libelle: pointData['libelle'],
|
||||
anomalie: pointData['anomalie'] ?? false,
|
||||
prediction: pointData['prediction'] ?? false,
|
||||
metadonnees: pointData['metadonnees'],
|
||||
);
|
||||
}).toList();
|
||||
|
||||
return KPITrend(
|
||||
id: cachedData['id'],
|
||||
typeMetrique: TypeMetrique.values.firstWhere(
|
||||
(e) => e.name == cachedData['typeMetrique'],
|
||||
),
|
||||
periodeAnalyse: PeriodeAnalyse.values.firstWhere(
|
||||
(e) => e.name == cachedData['periodeAnalyse'],
|
||||
),
|
||||
organisationId: cachedData['organisationId'],
|
||||
nomOrganisation: cachedData['nomOrganisation'],
|
||||
dateDebut: DateTime.parse(cachedData['dateDebut']),
|
||||
dateFin: DateTime.parse(cachedData['dateFin']),
|
||||
pointsDonnees: pointsDonnees,
|
||||
valeurActuelle: cachedData['valeurActuelle']?.toDouble() ?? 0.0,
|
||||
valeurMinimale: cachedData['valeurMinimale']?.toDouble(),
|
||||
valeurMaximale: cachedData['valeurMaximale']?.toDouble(),
|
||||
valeurMoyenne: cachedData['valeurMoyenne']?.toDouble(),
|
||||
ecartType: cachedData['ecartType']?.toDouble(),
|
||||
coefficientVariation: cachedData['coefficientVariation']?.toDouble(),
|
||||
tendanceGenerale: cachedData['tendanceGenerale']?.toDouble(),
|
||||
coefficientCorrelation: cachedData['coefficientCorrelation']?.toDouble(),
|
||||
pourcentageEvolutionGlobale: cachedData['pourcentageEvolutionGlobale']?.toDouble(),
|
||||
predictionProchainePeriode: cachedData['predictionProchainePeriode']?.toDouble(),
|
||||
margeErreurPrediction: cachedData['margeErreurPrediction']?.toDouble(),
|
||||
seuilAlerteBas: cachedData['seuilAlerteBas']?.toDouble(),
|
||||
seuilAlerteHaut: cachedData['seuilAlerteHaut']?.toDouble(),
|
||||
alerteActive: cachedData['alerteActive'] ?? false,
|
||||
typeAlerte: cachedData['typeAlerte'],
|
||||
messageAlerte: cachedData['messageAlerte'],
|
||||
configurationGraphique: cachedData['configurationGraphique'],
|
||||
intervalleRegroupement: cachedData['intervalleRegroupement'],
|
||||
formatDate: cachedData['formatDate'],
|
||||
dateDerniereMiseAJour: cachedData['dateDerniereMiseAJour'] != null
|
||||
? DateTime.parse(cachedData['dateDerniereMiseAJour'])
|
||||
: null,
|
||||
frequenceMiseAJourMinutes: cachedData['frequenceMiseAJourMinutes'],
|
||||
);
|
||||
}
|
||||
|
||||
/// Détermine la durée de vie du cache selon la période
|
||||
Duration _determinerDureeVieCache(PeriodeAnalyse periode) {
|
||||
switch (periode) {
|
||||
case PeriodeAnalyse.aujourdHui:
|
||||
case PeriodeAnalyse.hier:
|
||||
return const Duration(minutes: 30); // 30 minutes pour les tendances récentes
|
||||
case PeriodeAnalyse.cetteSemaine:
|
||||
case PeriodeAnalyse.semaineDerniere:
|
||||
case PeriodeAnalyse.septDerniersJours:
|
||||
return const Duration(hours: 2); // 2 heures pour les tendances hebdomadaires
|
||||
case PeriodeAnalyse.ceMois:
|
||||
case PeriodeAnalyse.moisDernier:
|
||||
case PeriodeAnalyse.trenteDerniersJours:
|
||||
return const Duration(hours: 6); // 6 heures pour les tendances mensuelles
|
||||
case PeriodeAnalyse.troisDerniersMois:
|
||||
case PeriodeAnalyse.sixDerniersMois:
|
||||
return const Duration(hours: 24); // 24 heures pour les tendances trimestrielles
|
||||
case PeriodeAnalyse.cetteAnnee:
|
||||
case PeriodeAnalyse.anneeDerniere:
|
||||
return const Duration(days: 2); // 2 jours pour les tendances annuelles
|
||||
case PeriodeAnalyse.periodePersonnalisee:
|
||||
return const Duration(hours: 4); // 4 heures par défaut
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Paramètres pour le use case CalculerTendanceKPI
|
||||
class CalculerTendanceKPIParams extends Equatable {
|
||||
const CalculerTendanceKPIParams({
|
||||
required this.typeMetrique,
|
||||
required this.periodeAnalyse,
|
||||
this.organisationId,
|
||||
this.forceRecalcul = false,
|
||||
this.inclureAnomalies = true,
|
||||
this.inclurePredictions = true,
|
||||
});
|
||||
|
||||
final TypeMetrique typeMetrique;
|
||||
final PeriodeAnalyse periodeAnalyse;
|
||||
final String? organisationId;
|
||||
final bool forceRecalcul;
|
||||
final bool inclureAnomalies;
|
||||
final bool inclurePredictions;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
typeMetrique,
|
||||
periodeAnalyse,
|
||||
organisationId,
|
||||
forceRecalcul,
|
||||
inclureAnomalies,
|
||||
inclurePredictions,
|
||||
];
|
||||
|
||||
CalculerTendanceKPIParams copyWith({
|
||||
TypeMetrique? typeMetrique,
|
||||
PeriodeAnalyse? periodeAnalyse,
|
||||
String? organisationId,
|
||||
bool? forceRecalcul,
|
||||
bool? inclureAnomalies,
|
||||
bool? inclurePredictions,
|
||||
}) {
|
||||
return CalculerTendanceKPIParams(
|
||||
typeMetrique: typeMetrique ?? this.typeMetrique,
|
||||
periodeAnalyse: periodeAnalyse ?? this.periodeAnalyse,
|
||||
organisationId: organisationId ?? this.organisationId,
|
||||
forceRecalcul: forceRecalcul ?? this.forceRecalcul,
|
||||
inclureAnomalies: inclureAnomalies ?? this.inclureAnomalies,
|
||||
inclurePredictions: inclurePredictions ?? this.inclurePredictions,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,393 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../../shared/widgets/common/unified_page_layout.dart';
|
||||
import '../../../../shared/widgets/common/unified_card.dart';
|
||||
import '../../../../shared/theme/design_system.dart';
|
||||
import '../../../../core/utils/constants.dart';
|
||||
import '../bloc/analytics_bloc.dart';
|
||||
import '../widgets/kpi_card_widget.dart';
|
||||
import '../widgets/trend_chart_widget.dart';
|
||||
import '../widgets/period_selector_widget.dart';
|
||||
import '../widgets/metrics_grid_widget.dart';
|
||||
import '../widgets/performance_gauge_widget.dart';
|
||||
import '../widgets/alerts_panel_widget.dart';
|
||||
import '../../domain/entities/analytics_data.dart';
|
||||
|
||||
/// Page principale du tableau de bord analytics
|
||||
class AnalyticsDashboardPage extends StatefulWidget {
|
||||
const AnalyticsDashboardPage({super.key});
|
||||
|
||||
@override
|
||||
State<AnalyticsDashboardPage> createState() => _AnalyticsDashboardPageState();
|
||||
}
|
||||
|
||||
class _AnalyticsDashboardPageState extends State<AnalyticsDashboardPage>
|
||||
with TickerProviderStateMixin {
|
||||
late TabController _tabController;
|
||||
PeriodeAnalyse _periodeSelectionnee = PeriodeAnalyse.ceMois;
|
||||
String? _organisationId;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabController = TabController(length: 4, vsync: this);
|
||||
_chargerDonneesInitiales();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _chargerDonneesInitiales() {
|
||||
context.read<AnalyticsBloc>().add(
|
||||
ChargerTableauBordEvent(
|
||||
periodeAnalyse: _periodeSelectionnee,
|
||||
organisationId: _organisationId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onPeriodeChanged(PeriodeAnalyse nouvellePeriode) {
|
||||
setState(() {
|
||||
_periodeSelectionnee = nouvellePeriode;
|
||||
});
|
||||
_chargerDonneesInitiales();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return UnifiedPageLayout(
|
||||
title: 'Analytics',
|
||||
subtitle: 'Tableau de bord et métriques',
|
||||
showBackButton: false,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
onPressed: _chargerDonneesInitiales,
|
||||
tooltip: 'Actualiser',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.settings),
|
||||
onPressed: () => _ouvrirParametres(context),
|
||||
tooltip: 'Paramètres',
|
||||
),
|
||||
],
|
||||
body: Column(
|
||||
children: [
|
||||
// Sélecteur de période
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(DesignSystem.spacing16),
|
||||
child: PeriodSelectorWidget(
|
||||
periodeSelectionnee: _periodeSelectionnee,
|
||||
onPeriodeChanged: _onPeriodeChanged,
|
||||
),
|
||||
),
|
||||
|
||||
// Onglets
|
||||
TabBar(
|
||||
controller: _tabController,
|
||||
labelColor: DesignSystem.primaryColor,
|
||||
unselectedLabelColor: DesignSystem.textSecondaryColor,
|
||||
indicatorColor: DesignSystem.primaryColor,
|
||||
tabs: const [
|
||||
Tab(
|
||||
icon: Icon(Icons.dashboard),
|
||||
text: 'Vue d\'ensemble',
|
||||
),
|
||||
Tab(
|
||||
icon: Icon(Icons.trending_up),
|
||||
text: 'Tendances',
|
||||
),
|
||||
Tab(
|
||||
icon: Icon(Icons.analytics),
|
||||
text: 'Détails',
|
||||
),
|
||||
Tab(
|
||||
icon: Icon(Icons.warning),
|
||||
text: 'Alertes',
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Contenu des onglets
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
_buildVueEnsemble(),
|
||||
_buildTendances(),
|
||||
_buildDetails(),
|
||||
_buildAlertes(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Vue d'ensemble avec KPI principaux
|
||||
Widget _buildVueEnsemble() {
|
||||
return BlocBuilder<AnalyticsBloc, AnalyticsState>(
|
||||
builder: (context, state) {
|
||||
if (state is AnalyticsLoading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
if (state is AnalyticsError) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 64,
|
||||
color: DesignSystem.errorColor,
|
||||
),
|
||||
const SizedBox(height: DesignSystem.spacing16),
|
||||
Text(
|
||||
'Erreur lors du chargement',
|
||||
style: DesignSystem.textTheme.headlineSmall,
|
||||
),
|
||||
const SizedBox(height: DesignSystem.spacing8),
|
||||
Text(
|
||||
state.message,
|
||||
style: DesignSystem.textTheme.bodyMedium,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: DesignSystem.spacing16),
|
||||
ElevatedButton(
|
||||
onPressed: _chargerDonneesInitiales,
|
||||
child: const Text('Réessayer'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (state is AnalyticsLoaded) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(DesignSystem.spacing16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Performance globale
|
||||
if (state.performanceGlobale != null)
|
||||
UnifiedCard(
|
||||
variant: UnifiedCardVariant.elevated,
|
||||
child: PerformanceGaugeWidget(
|
||||
score: state.performanceGlobale!,
|
||||
periode: _periodeSelectionnee,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: DesignSystem.spacing16),
|
||||
|
||||
// KPI principaux
|
||||
Text(
|
||||
'Indicateurs clés',
|
||||
style: DesignSystem.textTheme.headlineSmall,
|
||||
),
|
||||
const SizedBox(height: DesignSystem.spacing12),
|
||||
|
||||
MetricsGridWidget(
|
||||
metriques: state.metriques,
|
||||
onMetriquePressed: (metrique) => _ouvrirDetailMetrique(
|
||||
context,
|
||||
metrique,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: DesignSystem.spacing24),
|
||||
|
||||
// Graphiques de tendance rapide
|
||||
Text(
|
||||
'Évolutions récentes',
|
||||
style: DesignSystem.textTheme.headlineSmall,
|
||||
),
|
||||
const SizedBox(height: DesignSystem.spacing12),
|
||||
|
||||
if (state.tendances.isNotEmpty)
|
||||
SizedBox(
|
||||
height: 200,
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: state.tendances.length,
|
||||
itemBuilder: (context, index) {
|
||||
final tendance = state.tendances[index];
|
||||
return Container(
|
||||
width: 300,
|
||||
margin: const EdgeInsets.only(
|
||||
right: DesignSystem.spacing12,
|
||||
),
|
||||
child: UnifiedCard(
|
||||
variant: UnifiedCardVariant.outlined,
|
||||
child: TrendChartWidget(
|
||||
trend: tendance,
|
||||
compact: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Onglet des tendances détaillées
|
||||
Widget _buildTendances() {
|
||||
return BlocBuilder<AnalyticsBloc, AnalyticsState>(
|
||||
builder: (context, state) {
|
||||
if (state is AnalyticsLoaded && state.tendances.isNotEmpty) {
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.all(DesignSystem.spacing16),
|
||||
itemCount: state.tendances.length,
|
||||
itemBuilder: (context, index) {
|
||||
final tendance = state.tendances[index];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: DesignSystem.spacing16,
|
||||
),
|
||||
child: UnifiedCard(
|
||||
variant: UnifiedCardVariant.elevated,
|
||||
child: TrendChartWidget(
|
||||
trend: tendance,
|
||||
compact: false,
|
||||
showPredictions: true,
|
||||
showAnomalies: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return const Center(
|
||||
child: Text('Aucune tendance disponible'),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Onglet des détails par métrique
|
||||
Widget _buildDetails() {
|
||||
return BlocBuilder<AnalyticsBloc, AnalyticsState>(
|
||||
builder: (context, state) {
|
||||
if (state is AnalyticsLoaded) {
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.all(DesignSystem.spacing16),
|
||||
itemCount: TypeMetrique.values.length,
|
||||
itemBuilder: (context, index) {
|
||||
final typeMetrique = TypeMetrique.values[index];
|
||||
final metrique = state.metriques.firstWhere(
|
||||
(m) => m.typeMetrique == typeMetrique,
|
||||
orElse: () => AnalyticsData(
|
||||
id: 'placeholder_$index',
|
||||
typeMetrique: typeMetrique,
|
||||
periodeAnalyse: _periodeSelectionnee,
|
||||
valeur: 0,
|
||||
dateDebut: DateTime.now().subtract(const Duration(days: 30)),
|
||||
dateFin: DateTime.now(),
|
||||
dateCalcul: DateTime.now(),
|
||||
),
|
||||
);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: DesignSystem.spacing12,
|
||||
),
|
||||
child: KPICardWidget(
|
||||
analyticsData: metrique,
|
||||
onTap: () => _ouvrirDetailMetrique(context, metrique),
|
||||
showTrend: true,
|
||||
showDetails: true,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return const Center(
|
||||
child: Text('Aucun détail disponible'),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Onglet des alertes
|
||||
Widget _buildAlertes() {
|
||||
return BlocBuilder<AnalyticsBloc, AnalyticsState>(
|
||||
builder: (context, state) {
|
||||
if (state is AnalyticsLoaded) {
|
||||
final alertes = state.metriques
|
||||
.where((m) => m.isCritique || !m.isDonneesFiables)
|
||||
.toList();
|
||||
|
||||
if (alertes.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.check_circle_outline,
|
||||
size: 64,
|
||||
color: DesignSystem.successColor,
|
||||
),
|
||||
const SizedBox(height: DesignSystem.spacing16),
|
||||
Text(
|
||||
'Aucune alerte active',
|
||||
style: DesignSystem.textTheme.headlineSmall,
|
||||
),
|
||||
const SizedBox(height: DesignSystem.spacing8),
|
||||
Text(
|
||||
'Toutes les métriques sont dans les normes',
|
||||
style: DesignSystem.textTheme.bodyMedium,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return AlertsPanelWidget(
|
||||
alertes: alertes,
|
||||
onAlertePressed: (alerte) => _ouvrirDetailMetrique(
|
||||
context,
|
||||
alerte,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return const Center(
|
||||
child: Text('Aucune alerte disponible'),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _ouvrirDetailMetrique(BuildContext context, AnalyticsData metrique) {
|
||||
Navigator.of(context).pushNamed(
|
||||
AppRoutes.analyticsDetail,
|
||||
arguments: {
|
||||
'metrique': metrique,
|
||||
'periode': _periodeSelectionnee,
|
||||
'organisationId': _organisationId,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _ouvrirParametres(BuildContext context) {
|
||||
Navigator.of(context).pushNamed(AppRoutes.analyticsSettings);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,357 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../shared/widgets/common/unified_card.dart';
|
||||
import '../../../../shared/theme/design_system.dart';
|
||||
import '../../../../core/utils/formatters.dart';
|
||||
import '../../domain/entities/analytics_data.dart';
|
||||
|
||||
/// Widget de carte KPI utilisant le design system unifié
|
||||
class KPICardWidget extends StatelessWidget {
|
||||
const KPICardWidget({
|
||||
super.key,
|
||||
required this.analyticsData,
|
||||
this.onTap,
|
||||
this.showTrend = true,
|
||||
this.showDetails = false,
|
||||
this.compact = false,
|
||||
});
|
||||
|
||||
final AnalyticsData analyticsData;
|
||||
final VoidCallback? onTap;
|
||||
final bool showTrend;
|
||||
final bool showDetails;
|
||||
final bool compact;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return UnifiedCard(
|
||||
variant: UnifiedCardVariant.elevated,
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(
|
||||
compact ? DesignSystem.spacing12 : DesignSystem.spacing16,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// En-tête avec icône et titre
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(DesignSystem.spacing8),
|
||||
decoration: BoxDecoration(
|
||||
color: _getCouleurMetrique().withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(DesignSystem.radius8),
|
||||
),
|
||||
child: Icon(
|
||||
_getIconeMetrique(),
|
||||
color: _getCouleurMetrique(),
|
||||
size: compact ? 20 : 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: DesignSystem.spacing12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
analyticsData.libelleAffichage,
|
||||
style: compact
|
||||
? DesignSystem.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
)
|
||||
: DesignSystem.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (!compact && analyticsData.description != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: DesignSystem.spacing4,
|
||||
),
|
||||
child: Text(
|
||||
analyticsData.description!,
|
||||
style: DesignSystem.textTheme.bodySmall?.copyWith(
|
||||
color: DesignSystem.textSecondaryColor,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Indicateur de fiabilité
|
||||
if (showDetails)
|
||||
_buildIndicateurFiabilite(),
|
||||
],
|
||||
),
|
||||
|
||||
SizedBox(height: compact ? DesignSystem.spacing8 : DesignSystem.spacing16),
|
||||
|
||||
// Valeur principale
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
analyticsData.valeurFormatee,
|
||||
style: compact
|
||||
? DesignSystem.textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _getCouleurMetrique(),
|
||||
)
|
||||
: DesignSystem.textTheme.headlineMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _getCouleurMetrique(),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Évolution
|
||||
if (showTrend && analyticsData.pourcentageEvolution != null)
|
||||
_buildEvolution(),
|
||||
],
|
||||
),
|
||||
|
||||
// Détails supplémentaires
|
||||
if (showDetails) ...[
|
||||
const SizedBox(height: DesignSystem.spacing12),
|
||||
_buildDetails(),
|
||||
],
|
||||
|
||||
// Période et dernière mise à jour
|
||||
if (!compact) ...[
|
||||
const SizedBox(height: DesignSystem.spacing12),
|
||||
_buildInfosPeriode(),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Widget d'évolution avec icône et pourcentage
|
||||
Widget _buildEvolution() {
|
||||
final evolution = analyticsData.pourcentageEvolution!;
|
||||
final isPositive = evolution > 0;
|
||||
final isNegative = evolution < 0;
|
||||
|
||||
Color couleur;
|
||||
IconData icone;
|
||||
|
||||
if (isPositive) {
|
||||
couleur = DesignSystem.successColor;
|
||||
icone = Icons.trending_up;
|
||||
} else if (isNegative) {
|
||||
couleur = DesignSystem.errorColor;
|
||||
icone = Icons.trending_down;
|
||||
} else {
|
||||
couleur = DesignSystem.warningColor;
|
||||
icone = Icons.trending_flat;
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: DesignSystem.spacing8,
|
||||
vertical: DesignSystem.spacing4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: couleur.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(DesignSystem.radius12),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
icone,
|
||||
size: 16,
|
||||
color: couleur,
|
||||
),
|
||||
const SizedBox(width: DesignSystem.spacing4),
|
||||
Text(
|
||||
analyticsData.evolutionFormatee,
|
||||
style: DesignSystem.textTheme.bodySmall?.copyWith(
|
||||
color: couleur,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Widget d'indicateur de fiabilité
|
||||
Widget _buildIndicateurFiabilite() {
|
||||
final fiabilite = analyticsData.indicateurFiabilite;
|
||||
Color couleur;
|
||||
|
||||
if (fiabilite >= 90) {
|
||||
couleur = DesignSystem.successColor;
|
||||
} else if (fiabilite >= 70) {
|
||||
couleur = DesignSystem.warningColor;
|
||||
} else {
|
||||
couleur = DesignSystem.errorColor;
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: DesignSystem.spacing6,
|
||||
vertical: DesignSystem.spacing2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: couleur.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(DesignSystem.radius8),
|
||||
border: Border.all(
|
||||
color: couleur.withOpacity(0.3),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'${fiabilite.toStringAsFixed(0)}%',
|
||||
style: DesignSystem.textTheme.bodySmall?.copyWith(
|
||||
color: couleur,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Widget des détails supplémentaires
|
||||
Widget _buildDetails() {
|
||||
return Column(
|
||||
children: [
|
||||
// Valeur précédente
|
||||
if (analyticsData.valeurPrecedente != null)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Période précédente',
|
||||
style: DesignSystem.textTheme.bodySmall?.copyWith(
|
||||
color: DesignSystem.textSecondaryColor,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_formaterValeur(analyticsData.valeurPrecedente!),
|
||||
style: DesignSystem.textTheme.bodySmall?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: DesignSystem.spacing4),
|
||||
|
||||
// Éléments analysés
|
||||
if (analyticsData.nombreElementsAnalyses != null)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Éléments analysés',
|
||||
style: DesignSystem.textTheme.bodySmall?.copyWith(
|
||||
color: DesignSystem.textSecondaryColor,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
analyticsData.nombreElementsAnalyses.toString(),
|
||||
style: DesignSystem.textTheme.bodySmall?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: DesignSystem.spacing4),
|
||||
|
||||
// Temps de calcul
|
||||
if (analyticsData.tempsCalculMs != null)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Temps de calcul',
|
||||
style: DesignSystem.textTheme.bodySmall?.copyWith(
|
||||
color: DesignSystem.textSecondaryColor,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${analyticsData.tempsCalculMs}ms',
|
||||
style: DesignSystem.textTheme.bodySmall?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Widget des informations de période
|
||||
Widget _buildInfosPeriode() {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
analyticsData.periodeAnalyse.libelle,
|
||||
style: DesignSystem.textTheme.bodySmall?.copyWith(
|
||||
color: DesignSystem.textSecondaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Mis à jour ${AppFormatters.formatDateRelative(analyticsData.dateCalcul)}',
|
||||
style: DesignSystem.textTheme.bodySmall?.copyWith(
|
||||
color: DesignSystem.textSecondaryColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Obtient la couleur de la métrique
|
||||
Color _getCouleurMetrique() {
|
||||
return Color(int.parse(
|
||||
analyticsData.couleur.replaceFirst('#', '0xFF'),
|
||||
));
|
||||
}
|
||||
|
||||
/// Obtient l'icône de la métrique
|
||||
IconData _getIconeMetrique() {
|
||||
switch (analyticsData.icone) {
|
||||
case 'people':
|
||||
return Icons.people;
|
||||
case 'attach_money':
|
||||
return Icons.attach_money;
|
||||
case 'event':
|
||||
return Icons.event;
|
||||
case 'favorite':
|
||||
return Icons.favorite;
|
||||
case 'trending_up':
|
||||
return Icons.trending_up;
|
||||
case 'business':
|
||||
return Icons.business;
|
||||
case 'settings':
|
||||
return Icons.settings;
|
||||
default:
|
||||
return Icons.analytics;
|
||||
}
|
||||
}
|
||||
|
||||
/// Formate une valeur selon le type de métrique
|
||||
String _formaterValeur(double valeur) {
|
||||
switch (analyticsData.typeMetrique.typeValeur) {
|
||||
case 'amount':
|
||||
return '${valeur.toStringAsFixed(0)} ${analyticsData.unite}';
|
||||
case 'percentage':
|
||||
return '${valeur.toStringAsFixed(1)}${analyticsData.unite}';
|
||||
case 'average':
|
||||
return valeur.toStringAsFixed(1);
|
||||
default:
|
||||
return valeur.toStringAsFixed(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../shared/widgets/common/unified_card.dart';
|
||||
import '../../../../shared/theme/design_system.dart';
|
||||
import '../../domain/entities/analytics_data.dart';
|
||||
|
||||
/// Widget de sélection de période pour les analytics
|
||||
class PeriodSelectorWidget extends StatelessWidget {
|
||||
const PeriodSelectorWidget({
|
||||
super.key,
|
||||
required this.periodeSelectionnee,
|
||||
required this.onPeriodeChanged,
|
||||
this.compact = false,
|
||||
});
|
||||
|
||||
final PeriodeAnalyse periodeSelectionnee;
|
||||
final ValueChanged<PeriodeAnalyse> onPeriodeChanged;
|
||||
final bool compact;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (compact) {
|
||||
return _buildCompactSelector(context);
|
||||
} else {
|
||||
return _buildFullSelector(context);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sélecteur compact avec dropdown
|
||||
Widget _buildCompactSelector(BuildContext context) {
|
||||
return UnifiedCard(
|
||||
variant: UnifiedCardVariant.outlined,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: DesignSystem.spacing16,
|
||||
vertical: DesignSystem.spacing8,
|
||||
),
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<PeriodeAnalyse>(
|
||||
value: periodeSelectionnee,
|
||||
onChanged: (periode) {
|
||||
if (periode != null) {
|
||||
onPeriodeChanged(periode);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.expand_more),
|
||||
isExpanded: true,
|
||||
items: PeriodeAnalyse.values.map((periode) {
|
||||
return DropdownMenuItem<PeriodeAnalyse>(
|
||||
value: periode,
|
||||
child: Text(
|
||||
periode.libelle,
|
||||
style: DesignSystem.textTheme.bodyMedium,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Sélecteur complet avec chips
|
||||
Widget _buildFullSelector(BuildContext context) {
|
||||
final periodesRapides = [
|
||||
PeriodeAnalyse.aujourdHui,
|
||||
PeriodeAnalyse.hier,
|
||||
PeriodeAnalyse.cetteSemaine,
|
||||
PeriodeAnalyse.ceMois,
|
||||
PeriodeAnalyse.troisDerniersMois,
|
||||
PeriodeAnalyse.cetteAnnee,
|
||||
];
|
||||
|
||||
final periodesPersonnalisees = [
|
||||
PeriodeAnalyse.septDerniersJours,
|
||||
PeriodeAnalyse.trenteDerniersJours,
|
||||
PeriodeAnalyse.sixDerniersMois,
|
||||
PeriodeAnalyse.anneeDerniere,
|
||||
PeriodeAnalyse.periodePersonnalisee,
|
||||
];
|
||||
|
||||
return UnifiedCard(
|
||||
variant: UnifiedCardVariant.outlined,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(DesignSystem.spacing16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Titre
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.date_range,
|
||||
size: 20,
|
||||
color: DesignSystem.primaryColor,
|
||||
),
|
||||
const SizedBox(width: DesignSystem.spacing8),
|
||||
Text(
|
||||
'Période d\'analyse',
|
||||
style: DesignSystem.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: DesignSystem.spacing12),
|
||||
|
||||
// Périodes rapides
|
||||
Text(
|
||||
'Accès rapide',
|
||||
style: DesignSystem.textTheme.bodySmall?.copyWith(
|
||||
color: DesignSystem.textSecondaryColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: DesignSystem.spacing8),
|
||||
|
||||
Wrap(
|
||||
spacing: DesignSystem.spacing8,
|
||||
runSpacing: DesignSystem.spacing8,
|
||||
children: periodesRapides.map((periode) {
|
||||
return _buildPeriodeChip(periode, isRapide: true);
|
||||
}).toList(),
|
||||
),
|
||||
|
||||
const SizedBox(height: DesignSystem.spacing16),
|
||||
|
||||
// Périodes personnalisées
|
||||
Text(
|
||||
'Autres périodes',
|
||||
style: DesignSystem.textTheme.bodySmall?.copyWith(
|
||||
color: DesignSystem.textSecondaryColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: DesignSystem.spacing8),
|
||||
|
||||
Wrap(
|
||||
spacing: DesignSystem.spacing8,
|
||||
runSpacing: DesignSystem.spacing8,
|
||||
children: periodesPersonnalisees.map((periode) {
|
||||
return _buildPeriodeChip(periode, isRapide: false);
|
||||
}).toList(),
|
||||
),
|
||||
|
||||
// Informations sur la période sélectionnée
|
||||
if (periodeSelectionnee != PeriodeAnalyse.periodePersonnalisee) ...[
|
||||
const SizedBox(height: DesignSystem.spacing16),
|
||||
_buildInfosPeriode(),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Chip de sélection de période
|
||||
Widget _buildPeriodeChip(PeriodeAnalyse periode, {required bool isRapide}) {
|
||||
final isSelected = periode == periodeSelectionnee;
|
||||
|
||||
return FilterChip(
|
||||
label: Text(
|
||||
periode.libelle,
|
||||
style: DesignSystem.textTheme.bodySmall?.copyWith(
|
||||
color: isSelected
|
||||
? Colors.white
|
||||
: isRapide
|
||||
? DesignSystem.primaryColor
|
||||
: DesignSystem.textSecondaryColor,
|
||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
selected: isSelected,
|
||||
onSelected: (_) => onPeriodeChanged(periode),
|
||||
backgroundColor: isRapide
|
||||
? DesignSystem.primaryColor.withOpacity(0.1)
|
||||
: DesignSystem.surfaceColor,
|
||||
selectedColor: isRapide
|
||||
? DesignSystem.primaryColor
|
||||
: DesignSystem.secondaryColor,
|
||||
checkmarkColor: Colors.white,
|
||||
side: BorderSide(
|
||||
color: isSelected
|
||||
? Colors.transparent
|
||||
: isRapide
|
||||
? DesignSystem.primaryColor.withOpacity(0.3)
|
||||
: DesignSystem.borderColor,
|
||||
width: 1,
|
||||
),
|
||||
elevation: isSelected ? 2 : 0,
|
||||
pressElevation: 4,
|
||||
);
|
||||
}
|
||||
|
||||
/// Informations sur la période sélectionnée
|
||||
Widget _buildInfosPeriode() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(DesignSystem.spacing12),
|
||||
decoration: BoxDecoration(
|
||||
color: DesignSystem.primaryColor.withOpacity(0.05),
|
||||
borderRadius: BorderRadius.circular(DesignSystem.radius8),
|
||||
border: Border.all(
|
||||
color: DesignSystem.primaryColor.withOpacity(0.2),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.info_outline,
|
||||
size: 16,
|
||||
color: DesignSystem.primaryColor,
|
||||
),
|
||||
const SizedBox(width: DesignSystem.spacing8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Période sélectionnée : ${periodeSelectionnee.libelle}',
|
||||
style: DesignSystem.textTheme.bodySmall?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: DesignSystem.spacing2),
|
||||
Text(
|
||||
_getDescriptionPeriode(),
|
||||
style: DesignSystem.textTheme.bodySmall?.copyWith(
|
||||
color: DesignSystem.textSecondaryColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Description de la période sélectionnée
|
||||
String _getDescriptionPeriode() {
|
||||
switch (periodeSelectionnee) {
|
||||
case PeriodeAnalyse.aujourdHui:
|
||||
return 'Données du jour en cours';
|
||||
case PeriodeAnalyse.hier:
|
||||
return 'Données de la journée précédente';
|
||||
case PeriodeAnalyse.cetteSemaine:
|
||||
return 'Du lundi au dimanche de cette semaine';
|
||||
case PeriodeAnalyse.semaineDerniere:
|
||||
return 'Du lundi au dimanche de la semaine passée';
|
||||
case PeriodeAnalyse.ceMois:
|
||||
return 'Du 1er au dernier jour de ce mois';
|
||||
case PeriodeAnalyse.moisDernier:
|
||||
return 'Du 1er au dernier jour du mois passé';
|
||||
case PeriodeAnalyse.troisDerniersMois:
|
||||
return 'Les 3 derniers mois complets';
|
||||
case PeriodeAnalyse.sixDerniersMois:
|
||||
return 'Les 6 derniers mois complets';
|
||||
case PeriodeAnalyse.cetteAnnee:
|
||||
return 'Du 1er janvier à aujourd\'hui';
|
||||
case PeriodeAnalyse.anneeDerniere:
|
||||
return 'Du 1er janvier au 31 décembre de l\'année passée';
|
||||
case PeriodeAnalyse.septDerniersJours:
|
||||
return 'Les 7 derniers jours glissants';
|
||||
case PeriodeAnalyse.trenteDerniersJours:
|
||||
return 'Les 30 derniers jours glissants';
|
||||
case PeriodeAnalyse.periodePersonnalisee:
|
||||
return 'Définissez vos propres dates de début et fin';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -298,3 +298,23 @@ class ExportCotisations extends CotisationsEvent {
|
||||
@override
|
||||
List<Object?> get props => [format, cotisations];
|
||||
}
|
||||
|
||||
/// Événement pour charger l'historique des paiements
|
||||
class LoadPaymentHistory extends CotisationsEvent {
|
||||
final String? membreId;
|
||||
final String? period;
|
||||
final String? status;
|
||||
final String? method;
|
||||
final String? searchQuery;
|
||||
|
||||
const LoadPaymentHistory({
|
||||
this.membreId,
|
||||
this.period,
|
||||
this.status,
|
||||
this.method,
|
||||
this.searchQuery,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [membreId, period, status, method, searchQuery];
|
||||
}
|
||||
|
||||
@@ -380,3 +380,13 @@ class NotificationsScheduled extends CotisationsState {
|
||||
@override
|
||||
List<Object?> get props => [notificationsCount, cotisationIds];
|
||||
}
|
||||
|
||||
/// État d'historique des paiements chargé
|
||||
class PaymentHistoryLoaded extends CotisationsState {
|
||||
final List<PaymentModel> payments;
|
||||
|
||||
const PaymentHistoryLoaded(this.payments);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [payments];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,565 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../../core/di/injection.dart';
|
||||
import '../../../../core/models/cotisation_model.dart';
|
||||
import '../../../../core/models/membre_model.dart';
|
||||
import '../../../../shared/theme/app_theme.dart';
|
||||
import '../../../../shared/widgets/custom_text_field.dart';
|
||||
import '../../../../shared/widgets/loading_button.dart';
|
||||
|
||||
import '../bloc/cotisations_bloc.dart';
|
||||
import '../bloc/cotisations_event.dart';
|
||||
import '../bloc/cotisations_state.dart';
|
||||
|
||||
/// Page de création d'une nouvelle cotisation
|
||||
class CotisationCreatePage extends StatefulWidget {
|
||||
final MembreModel? membre; // Membre pré-sélectionné (optionnel)
|
||||
|
||||
const CotisationCreatePage({
|
||||
super.key,
|
||||
this.membre,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CotisationCreatePage> createState() => _CotisationCreatePageState();
|
||||
}
|
||||
|
||||
class _CotisationCreatePageState extends State<CotisationCreatePage> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
late CotisationsBloc _cotisationsBloc;
|
||||
|
||||
// Contrôleurs de champs
|
||||
final _montantController = TextEditingController();
|
||||
final _descriptionController = TextEditingController();
|
||||
final _periodeController = TextEditingController();
|
||||
|
||||
// Valeurs sélectionnées
|
||||
String _typeCotisation = 'MENSUELLE';
|
||||
DateTime _dateEcheance = DateTime.now().add(const Duration(days: 30));
|
||||
MembreModel? _membreSelectionne;
|
||||
|
||||
// Options disponibles
|
||||
final List<String> _typesCotisation = [
|
||||
'MENSUELLE',
|
||||
'TRIMESTRIELLE',
|
||||
'SEMESTRIELLE',
|
||||
'ANNUELLE',
|
||||
'EXCEPTIONNELLE',
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_cotisationsBloc = getIt<CotisationsBloc>();
|
||||
_membreSelectionne = widget.membre;
|
||||
|
||||
// Pré-remplir la période selon le type
|
||||
_updatePeriodeFromType();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_montantController.dispose();
|
||||
_descriptionController.dispose();
|
||||
_periodeController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _updatePeriodeFromType() {
|
||||
final now = DateTime.now();
|
||||
String periode;
|
||||
|
||||
switch (_typeCotisation) {
|
||||
case 'MENSUELLE':
|
||||
periode = '${_getMonthName(now.month)} ${now.year}';
|
||||
break;
|
||||
case 'TRIMESTRIELLE':
|
||||
final trimestre = ((now.month - 1) ~/ 3) + 1;
|
||||
periode = 'T$trimestre ${now.year}';
|
||||
break;
|
||||
case 'SEMESTRIELLE':
|
||||
final semestre = now.month <= 6 ? 1 : 2;
|
||||
periode = 'S$semestre ${now.year}';
|
||||
break;
|
||||
case 'ANNUELLE':
|
||||
periode = '${now.year}';
|
||||
break;
|
||||
case 'EXCEPTIONNELLE':
|
||||
periode = 'Exceptionnelle ${now.day}/${now.month}/${now.year}';
|
||||
break;
|
||||
default:
|
||||
periode = '${now.month}/${now.year}';
|
||||
}
|
||||
|
||||
_periodeController.text = periode;
|
||||
}
|
||||
|
||||
String _getMonthName(int month) {
|
||||
const months = [
|
||||
'Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin',
|
||||
'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'
|
||||
];
|
||||
return months[month - 1];
|
||||
}
|
||||
|
||||
void _onTypeChanged(String? newType) {
|
||||
if (newType != null) {
|
||||
setState(() {
|
||||
_typeCotisation = newType;
|
||||
_updatePeriodeFromType();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _selectDate() async {
|
||||
final picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: _dateEcheance,
|
||||
firstDate: DateTime.now(),
|
||||
lastDate: DateTime.now().add(const Duration(days: 365)),
|
||||
locale: const Locale('fr', 'FR'),
|
||||
);
|
||||
|
||||
if (picked != null) {
|
||||
setState(() {
|
||||
_dateEcheance = picked;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _selectMembre() async {
|
||||
// TODO: Implémenter la sélection de membre
|
||||
// Pour l'instant, afficher un message
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Fonctionnalité de sélection de membre à implémenter'),
|
||||
backgroundColor: AppTheme.infoColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _createCotisation() {
|
||||
if (!_formKey.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_membreSelectionne == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Veuillez sélectionner un membre'),
|
||||
backgroundColor: AppTheme.errorColor,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final montant = double.tryParse(_montantController.text.replaceAll(' ', ''));
|
||||
if (montant == null || montant <= 0) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Veuillez saisir un montant valide'),
|
||||
backgroundColor: AppTheme.errorColor,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Créer la cotisation
|
||||
final cotisation = CotisationModel(
|
||||
id: '', // Sera généré par le backend
|
||||
numeroReference: '', // Sera généré par le backend
|
||||
membreId: _membreSelectionne!.id ?? '',
|
||||
nomMembre: _membreSelectionne!.nomComplet,
|
||||
typeCotisation: _typeCotisation,
|
||||
montantDu: montant,
|
||||
montantPaye: 0.0,
|
||||
dateEcheance: _dateEcheance,
|
||||
statut: 'EN_ATTENTE',
|
||||
description: _descriptionController.text.trim(),
|
||||
periode: _periodeController.text.trim(),
|
||||
annee: _dateEcheance.year,
|
||||
mois: _dateEcheance.month,
|
||||
codeDevise: 'XOF',
|
||||
recurrente: _typeCotisation != 'EXCEPTIONNELLE',
|
||||
nombreRappels: 0,
|
||||
dateCreation: DateTime.now(),
|
||||
dateModification: DateTime.now(),
|
||||
);
|
||||
|
||||
_cotisationsBloc.add(CreateCotisation(cotisation));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: _cotisationsBloc,
|
||||
child: Scaffold(
|
||||
backgroundColor: AppTheme.backgroundLight,
|
||||
appBar: AppBar(
|
||||
title: const Text('Nouvelle Cotisation'),
|
||||
backgroundColor: AppTheme.accentColor,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
),
|
||||
body: BlocListener<CotisationsBloc, CotisationsState>(
|
||||
listener: (context, state) {
|
||||
if (state is CotisationCreated) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Cotisation créée avec succès'),
|
||||
backgroundColor: AppTheme.successColor,
|
||||
),
|
||||
);
|
||||
Navigator.of(context).pop(true);
|
||||
} else if (state is CotisationsError) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.message),
|
||||
backgroundColor: AppTheme.errorColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Sélection du membre
|
||||
_buildMembreSection(),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Type de cotisation
|
||||
_buildTypeSection(),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Montant
|
||||
_buildMontantSection(),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Période et échéance
|
||||
_buildPeriodeSection(),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Description
|
||||
_buildDescriptionSection(),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Bouton de création
|
||||
_buildCreateButton(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMembreSection() {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Membre',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
if (_membreSelectionne != null)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.accentColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: AppTheme.accentColor.withOpacity(0.3)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
backgroundColor: AppTheme.accentColor,
|
||||
child: Text(
|
||||
_membreSelectionne!.nomComplet.substring(0, 1).toUpperCase(),
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_membreSelectionne!.nomComplet,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_membreSelectionne!.telephone.isNotEmpty
|
||||
? _membreSelectionne!.telephone
|
||||
: 'Pas de téléphone',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.change_circle),
|
||||
onPressed: _selectMembre,
|
||||
color: AppTheme.accentColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
ElevatedButton.icon(
|
||||
onPressed: _selectMembre,
|
||||
icon: const Icon(Icons.person_add),
|
||||
label: const Text('Sélectionner un membre'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.accentColor,
|
||||
foregroundColor: Colors.white,
|
||||
minimumSize: const Size(double.infinity, 48),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTypeSection() {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Type de cotisation',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
DropdownButtonFormField<String>(
|
||||
value: _typeCotisation,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
),
|
||||
items: _typesCotisation.map((type) {
|
||||
return DropdownMenuItem(
|
||||
value: type,
|
||||
child: Text(_getTypeLabel(type)),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: _onTypeChanged,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _getTypeLabel(String type) {
|
||||
switch (type) {
|
||||
case 'MENSUELLE': return 'Mensuelle';
|
||||
case 'TRIMESTRIELLE': return 'Trimestrielle';
|
||||
case 'SEMESTRIELLE': return 'Semestrielle';
|
||||
case 'ANNUELLE': return 'Annuelle';
|
||||
case 'EXCEPTIONNELLE': return 'Exceptionnelle';
|
||||
default: return type;
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildMontantSection() {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Montant',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
CustomTextField(
|
||||
controller: _montantController,
|
||||
label: 'Montant (XOF)',
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
TextInputFormatter.withFunction((oldValue, newValue) {
|
||||
// Formater avec des espaces pour les milliers
|
||||
final text = newValue.text.replaceAll(' ', '');
|
||||
if (text.isEmpty) return newValue;
|
||||
|
||||
final number = int.tryParse(text);
|
||||
if (number == null) return oldValue;
|
||||
|
||||
final formatted = number.toString().replaceAllMapped(
|
||||
RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'),
|
||||
(Match m) => '${m[1]} ',
|
||||
);
|
||||
|
||||
return TextEditingValue(
|
||||
text: formatted,
|
||||
selection: TextSelection.collapsed(offset: formatted.length),
|
||||
);
|
||||
}),
|
||||
],
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Veuillez saisir un montant';
|
||||
}
|
||||
final montant = double.tryParse(value.replaceAll(' ', ''));
|
||||
if (montant == null || montant <= 0) {
|
||||
return 'Veuillez saisir un montant valide';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
suffixIcon: const Icon(Icons.attach_money),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPeriodeSection() {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Période et échéance',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
CustomTextField(
|
||||
controller: _periodeController,
|
||||
label: 'Période',
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Veuillez saisir une période';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
InkWell(
|
||||
onTap: _selectDate,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.calendar_today, color: AppTheme.accentColor),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Date d\'échéance',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${_dateEcheance.day}/${_dateEcheance.month}/${_dateEcheance.year}',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Icon(Icons.arrow_drop_down),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDescriptionSection() {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Description (optionnelle)',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
CustomTextField(
|
||||
controller: _descriptionController,
|
||||
label: 'Description de la cotisation',
|
||||
maxLines: 3,
|
||||
maxLength: 500,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCreateButton() {
|
||||
return BlocBuilder<CotisationsBloc, CotisationsState>(
|
||||
builder: (context, state) {
|
||||
final isLoading = state is CotisationsLoading;
|
||||
|
||||
return LoadingButton(
|
||||
onPressed: isLoading ? null : _createCotisation,
|
||||
isLoading: isLoading,
|
||||
text: 'Créer la cotisation',
|
||||
backgroundColor: AppTheme.accentColor,
|
||||
textColor: Colors.white,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import '../bloc/cotisations_event.dart';
|
||||
import '../bloc/cotisations_state.dart';
|
||||
import '../widgets/payment_method_selector.dart';
|
||||
import '../widgets/payment_form_widget.dart';
|
||||
import '../widgets/wave_payment_widget.dart';
|
||||
import '../widgets/cotisation_timeline_widget.dart';
|
||||
|
||||
/// Page de détail d'une cotisation
|
||||
@@ -422,18 +423,61 @@ class _CotisationDetailPageState extends State<CotisationDetailPage>
|
||||
);
|
||||
}
|
||||
|
||||
return PaymentFormWidget(
|
||||
cotisation: widget.cotisation,
|
||||
onPaymentInitiated: (paymentData) {
|
||||
_cotisationsBloc.add(InitiatePayment(
|
||||
cotisationId: widget.cotisation.id,
|
||||
montant: paymentData['montant'],
|
||||
methodePaiement: paymentData['methodePaiement'],
|
||||
numeroTelephone: paymentData['numeroTelephone'],
|
||||
nomPayeur: paymentData['nomPayeur'],
|
||||
emailPayeur: paymentData['emailPayeur'],
|
||||
));
|
||||
},
|
||||
return Column(
|
||||
children: [
|
||||
// Widget Wave Money en priorité
|
||||
WavePaymentWidget(
|
||||
cotisation: widget.cotisation,
|
||||
showFullInterface: true,
|
||||
onPaymentInitiated: () {
|
||||
// Feedback visuel lors de l'initiation du paiement
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Redirection vers Wave Money...'),
|
||||
backgroundColor: Color(0xFF00D4FF),
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Séparateur avec texte
|
||||
Row(
|
||||
children: [
|
||||
const Expanded(child: Divider()),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: const Text(
|
||||
'Ou choisir une autre méthode',
|
||||
style: TextStyle(
|
||||
color: AppTheme.textSecondary,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Expanded(child: Divider()),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Formulaire de paiement classique
|
||||
PaymentFormWidget(
|
||||
cotisation: widget.cotisation,
|
||||
onPaymentInitiated: (paymentData) {
|
||||
_cotisationsBloc.add(InitiatePayment(
|
||||
cotisationId: widget.cotisation.id,
|
||||
montant: paymentData['montant'],
|
||||
methodePaiement: paymentData['methodePaiement'],
|
||||
numeroTelephone: paymentData['numeroTelephone'],
|
||||
nomPayeur: paymentData['nomPayeur'],
|
||||
emailPayeur: paymentData['emailPayeur'],
|
||||
));
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -11,6 +11,9 @@ import '../widgets/cotisations_stats_card.dart';
|
||||
import 'cotisation_detail_page.dart';
|
||||
import 'cotisations_search_page.dart';
|
||||
|
||||
// Import de l'architecture unifiée pour amélioration progressive
|
||||
import '../../../../shared/widgets/common/unified_page_layout.dart';
|
||||
|
||||
/// Page principale pour la liste des cotisations
|
||||
class CotisationsListPage extends StatefulWidget {
|
||||
const CotisationsListPage({super.key});
|
||||
@@ -64,69 +67,96 @@ class _CotisationsListPageState extends State<CotisationsListPage> {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: _cotisationsBloc,
|
||||
child: Scaffold(
|
||||
backgroundColor: AppTheme.backgroundLight,
|
||||
body: Column(
|
||||
children: [
|
||||
// Header personnalisé
|
||||
_buildHeader(),
|
||||
|
||||
// Contenu principal
|
||||
Expanded(
|
||||
child: BlocBuilder<CotisationsBloc, CotisationsState>(
|
||||
builder: (context, state) {
|
||||
if (state is CotisationsInitial ||
|
||||
(state is CotisationsLoading && !state.isRefreshing)) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
if (state is CotisationsError) {
|
||||
return _buildErrorState(state);
|
||||
}
|
||||
|
||||
if (state is CotisationsLoaded) {
|
||||
return _buildLoadedState(state);
|
||||
}
|
||||
|
||||
// État par défaut - Coming Soon
|
||||
return const ComingSoonPage(
|
||||
title: 'Module Cotisations',
|
||||
description: 'Gestion complète des cotisations avec paiements automatiques',
|
||||
icon: Icons.payment_rounded,
|
||||
color: AppTheme.accentColor,
|
||||
features: [
|
||||
'Tableau de bord des cotisations',
|
||||
'Relances automatiques par email/SMS',
|
||||
'Paiements en ligne sécurisés',
|
||||
'Génération de reçus automatique',
|
||||
'Suivi des retards de paiement',
|
||||
'Rapports financiers détaillés',
|
||||
],
|
||||
child: BlocBuilder<CotisationsBloc, CotisationsState>(
|
||||
builder: (context, state) {
|
||||
// Utilisation de UnifiedPageLayout pour améliorer la cohérence
|
||||
// tout en conservant le header personnalisé et toutes les fonctionnalités
|
||||
return UnifiedPageLayout(
|
||||
title: 'Cotisations',
|
||||
subtitle: 'Gérez les cotisations de vos membres',
|
||||
icon: Icons.payment_rounded,
|
||||
iconColor: AppTheme.accentColor,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.search),
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const CotisationsSearchPage(),
|
||||
),
|
||||
);
|
||||
},
|
||||
tooltip: 'Rechercher',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.filter_list),
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const CotisationsSearchPage(),
|
||||
),
|
||||
);
|
||||
},
|
||||
tooltip: 'Filtrer',
|
||||
),
|
||||
],
|
||||
isLoading: state is CotisationsInitial ||
|
||||
(state is CotisationsLoading && !state.isRefreshing),
|
||||
errorMessage: state is CotisationsError ? state.message : null,
|
||||
onRefresh: () {
|
||||
_cotisationsBloc.add(const LoadCotisations(refresh: true));
|
||||
_cotisationsBloc.add(const LoadCotisationsStats());
|
||||
},
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () {
|
||||
// TODO: Implémenter la création de cotisation
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Création de cotisation - En cours de développement'),
|
||||
backgroundColor: AppTheme.accentColor,
|
||||
),
|
||||
);
|
||||
},
|
||||
backgroundColor: AppTheme.accentColor,
|
||||
child: const Icon(Icons.add, color: Colors.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () {
|
||||
// TODO: Implémenter la création de cotisation
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Création de cotisation - En cours de développement'),
|
||||
backgroundColor: AppTheme.accentColor,
|
||||
),
|
||||
);
|
||||
},
|
||||
backgroundColor: AppTheme.accentColor,
|
||||
child: const Icon(Icons.add, color: Colors.white),
|
||||
),
|
||||
body: _buildContent(state),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit le contenu principal en fonction de l'état
|
||||
/// CONSERVÉ: Toute la logique d'état et les widgets spécialisés
|
||||
Widget _buildContent(CotisationsState state) {
|
||||
if (state is CotisationsError) {
|
||||
return _buildErrorState(state);
|
||||
}
|
||||
|
||||
if (state is CotisationsLoaded) {
|
||||
return _buildLoadedState(state);
|
||||
}
|
||||
|
||||
// État par défaut - Coming Soon avec toutes les fonctionnalités prévues
|
||||
return const ComingSoonPage(
|
||||
title: 'Module Cotisations',
|
||||
description: 'Gestion complète des cotisations avec paiements automatiques',
|
||||
icon: Icons.payment_rounded,
|
||||
color: AppTheme.accentColor,
|
||||
features: [
|
||||
'Tableau de bord des cotisations',
|
||||
'Relances automatiques par email/SMS',
|
||||
'Paiements en ligne sécurisés',
|
||||
'Génération de reçus automatique',
|
||||
'Suivi des retards de paiement',
|
||||
'Rapports financiers détaillés',
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
|
||||
@@ -0,0 +1,596 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../../core/di/injection.dart';
|
||||
import '../../../../shared/widgets/unified_components.dart';
|
||||
import '../../../../shared/theme/app_theme.dart';
|
||||
import '../../../../core/models/cotisation_model.dart';
|
||||
import '../bloc/cotisations_bloc.dart';
|
||||
import '../bloc/cotisations_event.dart';
|
||||
import '../bloc/cotisations_state.dart';
|
||||
import 'cotisation_create_page.dart';
|
||||
import 'payment_history_page.dart';
|
||||
import 'cotisation_detail_page.dart';
|
||||
import '../widgets/wave_payment_widget.dart';
|
||||
|
||||
/// Page des cotisations UnionFlow - Version Unifiée
|
||||
///
|
||||
/// Utilise l'architecture unifiée pour une expérience cohérente :
|
||||
/// - Composants standardisés réutilisables
|
||||
/// - Interface homogène avec les autres onglets
|
||||
/// - Performance optimisée avec animations fluides
|
||||
/// - Maintenabilité maximale
|
||||
class CotisationsListPageUnified extends StatefulWidget {
|
||||
const CotisationsListPageUnified({super.key});
|
||||
|
||||
@override
|
||||
State<CotisationsListPageUnified> createState() => _CotisationsListPageUnifiedState();
|
||||
}
|
||||
|
||||
class _CotisationsListPageUnifiedState extends State<CotisationsListPageUnified> {
|
||||
late final CotisationsBloc _cotisationsBloc;
|
||||
String _currentFilter = 'all';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_cotisationsBloc = getIt<CotisationsBloc>();
|
||||
_loadData();
|
||||
}
|
||||
|
||||
void _loadData() {
|
||||
_cotisationsBloc.add(const LoadCotisations());
|
||||
_cotisationsBloc.add(const LoadCotisationsStats());
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: _cotisationsBloc,
|
||||
child: BlocBuilder<CotisationsBloc, CotisationsState>(
|
||||
builder: (context, state) {
|
||||
return UnifiedPageLayout(
|
||||
title: 'Cotisations',
|
||||
subtitle: 'Gestion des cotisations de l\'association',
|
||||
icon: Icons.account_balance_wallet,
|
||||
iconColor: AppTheme.successColor,
|
||||
isLoading: state is CotisationsLoading,
|
||||
errorMessage: state is CotisationsError ? state.message : null,
|
||||
onRefresh: _loadData,
|
||||
actions: _buildActions(),
|
||||
body: Column(
|
||||
children: [
|
||||
_buildKPISection(state),
|
||||
const SizedBox(height: AppTheme.spacingLarge),
|
||||
_buildQuickActionsSection(),
|
||||
const SizedBox(height: AppTheme.spacingLarge),
|
||||
_buildFiltersSection(),
|
||||
const SizedBox(height: AppTheme.spacingLarge),
|
||||
Expanded(child: _buildCotisationsList(state)),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Actions de la barre d'outils
|
||||
List<Widget> _buildActions() {
|
||||
return [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
onPressed: () {
|
||||
// TODO: Navigation vers ajout cotisation
|
||||
},
|
||||
tooltip: 'Nouvelle cotisation',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.search),
|
||||
onPressed: () {
|
||||
// TODO: Navigation vers recherche
|
||||
},
|
||||
tooltip: 'Rechercher',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.analytics),
|
||||
onPressed: () {
|
||||
// TODO: Navigation vers analyses
|
||||
},
|
||||
tooltip: 'Analyses',
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/// Section des KPI des cotisations
|
||||
Widget _buildKPISection(CotisationsState state) {
|
||||
final cotisations = state is CotisationsLoaded ? state.cotisations : <CotisationModel>[];
|
||||
final totalCotisations = cotisations.length;
|
||||
final cotisationsPayees = cotisations.where((c) => c.statut == 'PAYEE').length;
|
||||
final cotisationsEnAttente = cotisations.where((c) => c.statut == 'EN_ATTENTE').length;
|
||||
final montantTotal = cotisations.fold<double>(0, (sum, c) => sum + c.montantDu);
|
||||
|
||||
final kpis = [
|
||||
UnifiedKPIData(
|
||||
title: 'Total',
|
||||
value: totalCotisations.toString(),
|
||||
icon: Icons.receipt,
|
||||
color: AppTheme.primaryColor,
|
||||
trend: UnifiedKPITrend(
|
||||
direction: UnifiedKPITrendDirection.stable,
|
||||
value: 'Total',
|
||||
label: 'cotisations',
|
||||
),
|
||||
),
|
||||
UnifiedKPIData(
|
||||
title: 'Payées',
|
||||
value: cotisationsPayees.toString(),
|
||||
icon: Icons.check_circle,
|
||||
color: AppTheme.successColor,
|
||||
trend: UnifiedKPITrend(
|
||||
direction: UnifiedKPITrendDirection.up,
|
||||
value: '${((cotisationsPayees / totalCotisations) * 100).toInt()}%',
|
||||
label: 'du total',
|
||||
),
|
||||
),
|
||||
UnifiedKPIData(
|
||||
title: 'En attente',
|
||||
value: cotisationsEnAttente.toString(),
|
||||
icon: Icons.pending,
|
||||
color: AppTheme.warningColor,
|
||||
trend: UnifiedKPITrend(
|
||||
direction: UnifiedKPITrendDirection.down,
|
||||
value: '${((cotisationsEnAttente / totalCotisations) * 100).toInt()}%',
|
||||
label: 'du total',
|
||||
),
|
||||
),
|
||||
UnifiedKPIData(
|
||||
title: 'Montant',
|
||||
value: '${montantTotal.toStringAsFixed(0)}€',
|
||||
icon: Icons.euro,
|
||||
color: AppTheme.accentColor,
|
||||
trend: UnifiedKPITrend(
|
||||
direction: UnifiedKPITrendDirection.up,
|
||||
value: 'Total',
|
||||
label: 'collecté',
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
return UnifiedKPISection(
|
||||
title: 'Statistiques des cotisations',
|
||||
kpis: kpis,
|
||||
);
|
||||
}
|
||||
|
||||
/// Section des actions rapides
|
||||
Widget _buildQuickActionsSection() {
|
||||
final actions = [
|
||||
UnifiedQuickAction(
|
||||
id: 'add_cotisation',
|
||||
title: 'Nouvelle\nCotisation',
|
||||
icon: Icons.add_card,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
UnifiedQuickAction(
|
||||
id: 'bulk_payment',
|
||||
title: 'Paiement\nGroupé',
|
||||
icon: Icons.payment,
|
||||
color: AppTheme.successColor,
|
||||
),
|
||||
UnifiedQuickAction(
|
||||
id: 'send_reminder',
|
||||
title: 'Envoyer\nRappels',
|
||||
icon: Icons.notification_important,
|
||||
color: AppTheme.warningColor,
|
||||
badgeCount: 15,
|
||||
),
|
||||
UnifiedQuickAction(
|
||||
id: 'export_data',
|
||||
title: 'Exporter\nDonnées',
|
||||
icon: Icons.download,
|
||||
color: AppTheme.infoColor,
|
||||
),
|
||||
UnifiedQuickAction(
|
||||
id: 'payment_history',
|
||||
title: 'Historique\nPaiements',
|
||||
icon: Icons.history,
|
||||
color: AppTheme.accentColor,
|
||||
),
|
||||
UnifiedQuickAction(
|
||||
id: 'reports',
|
||||
title: 'Rapports\nFinanciers',
|
||||
icon: Icons.analytics,
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
];
|
||||
|
||||
return UnifiedQuickActionsSection(
|
||||
title: 'Actions rapides',
|
||||
actions: actions,
|
||||
onActionTap: _handleQuickAction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Section des filtres
|
||||
Widget _buildFiltersSection() {
|
||||
return UnifiedCard.outlined(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(AppTheme.spacingMedium),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.filter_list,
|
||||
color: AppTheme.successColor,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: AppTheme.spacingSmall),
|
||||
Text(
|
||||
'Filtres rapides',
|
||||
style: AppTheme.titleSmall.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: AppTheme.spacingMedium),
|
||||
Wrap(
|
||||
spacing: AppTheme.spacingSmall,
|
||||
runSpacing: AppTheme.spacingSmall,
|
||||
children: [
|
||||
_buildFilterChip('Toutes', 'all'),
|
||||
_buildFilterChip('Payées', 'payee'),
|
||||
_buildFilterChip('En attente', 'en_attente'),
|
||||
_buildFilterChip('En retard', 'en_retard'),
|
||||
_buildFilterChip('Annulées', 'annulee'),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit un chip de filtre
|
||||
Widget _buildFilterChip(String label, String value) {
|
||||
final isSelected = _currentFilter == value;
|
||||
return FilterChip(
|
||||
label: Text(label),
|
||||
selected: isSelected,
|
||||
onSelected: (selected) {
|
||||
setState(() {
|
||||
_currentFilter = selected ? value : 'all';
|
||||
});
|
||||
// TODO: Appliquer le filtre
|
||||
},
|
||||
selectedColor: AppTheme.successColor.withOpacity(0.2),
|
||||
checkmarkColor: AppTheme.successColor,
|
||||
);
|
||||
}
|
||||
|
||||
/// Liste des cotisations avec composant unifié
|
||||
Widget _buildCotisationsList(CotisationsState state) {
|
||||
if (state is CotisationsLoaded) {
|
||||
final filteredCotisations = _filterCotisations(state.cotisations);
|
||||
|
||||
return UnifiedListWidget<CotisationModel>(
|
||||
items: filteredCotisations,
|
||||
itemBuilder: (context, cotisation, index) => _buildCotisationCard(cotisation),
|
||||
isLoading: false,
|
||||
hasReachedMax: state.hasReachedMax,
|
||||
enableAnimations: true,
|
||||
emptyMessage: 'Aucune cotisation trouvée',
|
||||
emptyIcon: Icons.receipt_outlined,
|
||||
onLoadMore: () {
|
||||
// TODO: Charger plus de cotisations
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return const Center(
|
||||
child: Text('Chargement des cotisations...'),
|
||||
);
|
||||
}
|
||||
|
||||
/// Filtre les cotisations selon le filtre actuel
|
||||
List<CotisationModel> _filterCotisations(List<CotisationModel> cotisations) {
|
||||
if (_currentFilter == 'all') return cotisations;
|
||||
|
||||
return cotisations.where((cotisation) {
|
||||
switch (_currentFilter) {
|
||||
case 'payee':
|
||||
return cotisation.statut == 'PAYEE';
|
||||
case 'en_attente':
|
||||
return cotisation.statut == 'EN_ATTENTE';
|
||||
case 'en_retard':
|
||||
return cotisation.statut == 'EN_RETARD';
|
||||
case 'annulee':
|
||||
return cotisation.statut == 'ANNULEE';
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}).toList();
|
||||
}
|
||||
|
||||
/// Construit une carte de cotisation
|
||||
Widget _buildCotisationCard(CotisationModel cotisation) {
|
||||
return UnifiedCard.listItem(
|
||||
onTap: () {
|
||||
// TODO: Navigation vers détails de la cotisation
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(AppTheme.spacingMedium),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(AppTheme.spacingSmall),
|
||||
decoration: BoxDecoration(
|
||||
color: _getStatusColor(cotisation.statut).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(AppTheme.borderRadiusSmall),
|
||||
),
|
||||
child: Icon(
|
||||
_getStatusIcon(cotisation.statut),
|
||||
color: _getStatusColor(cotisation.statut),
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppTheme.spacingMedium),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
cotisation.typeCotisation,
|
||||
style: AppTheme.bodyLarge.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppTheme.spacingXSmall),
|
||||
Text(
|
||||
'Membre: ${cotisation.nomMembre ?? 'N/A'}',
|
||||
style: AppTheme.bodySmall.copyWith(
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
'${cotisation.montantDu.toStringAsFixed(2)}€',
|
||||
style: AppTheme.titleMedium.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.successColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppTheme.spacingXSmall),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppTheme.spacingSmall,
|
||||
vertical: AppTheme.spacingXSmall,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: _getStatusColor(cotisation.statut).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(AppTheme.borderRadiusSmall),
|
||||
),
|
||||
child: Text(
|
||||
_getStatusLabel(cotisation.statut),
|
||||
style: AppTheme.bodySmall.copyWith(
|
||||
color: _getStatusColor(cotisation.statut),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: AppTheme.spacingMedium),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.calendar_today,
|
||||
size: 16,
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
const SizedBox(width: AppTheme.spacingXSmall),
|
||||
Text(
|
||||
'Échéance: ${cotisation.dateEcheance.day}/${cotisation.dateEcheance.month}/${cotisation.dateEcheance.year}',
|
||||
style: AppTheme.bodySmall.copyWith(
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
if (cotisation.datePaiement != null) ...[
|
||||
Icon(
|
||||
Icons.check_circle,
|
||||
size: 16,
|
||||
color: AppTheme.successColor,
|
||||
),
|
||||
const SizedBox(width: AppTheme.spacingXSmall),
|
||||
Text(
|
||||
'Payée le ${cotisation.datePaiement!.day}/${cotisation.datePaiement!.month}/${cotisation.datePaiement!.year}',
|
||||
style: AppTheme.bodySmall.copyWith(
|
||||
color: AppTheme.successColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Obtient la couleur du statut
|
||||
Color _getStatusColor(String statut) {
|
||||
switch (statut) {
|
||||
case 'PAYEE':
|
||||
return AppTheme.successColor;
|
||||
case 'EN_ATTENTE':
|
||||
return AppTheme.warningColor;
|
||||
case 'EN_RETARD':
|
||||
return AppTheme.errorColor;
|
||||
case 'ANNULEE':
|
||||
return AppTheme.textSecondary;
|
||||
default:
|
||||
return AppTheme.textSecondary;
|
||||
}
|
||||
}
|
||||
|
||||
/// Obtient l'icône du statut
|
||||
IconData _getStatusIcon(String statut) {
|
||||
switch (statut) {
|
||||
case 'PAYEE':
|
||||
return Icons.check_circle;
|
||||
case 'EN_ATTENTE':
|
||||
return Icons.pending;
|
||||
case 'EN_RETARD':
|
||||
return Icons.warning;
|
||||
case 'ANNULEE':
|
||||
return Icons.cancel;
|
||||
default:
|
||||
return Icons.help;
|
||||
}
|
||||
}
|
||||
|
||||
/// Obtient le libellé du statut
|
||||
String _getStatusLabel(String statut) {
|
||||
switch (statut) {
|
||||
case 'PAYEE':
|
||||
return 'Payée';
|
||||
case 'EN_ATTENTE':
|
||||
return 'En attente';
|
||||
case 'EN_RETARD':
|
||||
return 'En retard';
|
||||
case 'ANNULEE':
|
||||
return 'Annulée';
|
||||
default:
|
||||
return 'Inconnu';
|
||||
}
|
||||
}
|
||||
|
||||
/// Gère les actions rapides
|
||||
void _handleQuickAction(UnifiedQuickAction action) {
|
||||
switch (action.id) {
|
||||
case 'add_cotisation':
|
||||
_navigateToCreateCotisation();
|
||||
break;
|
||||
case 'bulk_payment':
|
||||
_showBulkPaymentDialog();
|
||||
break;
|
||||
case 'send_reminder':
|
||||
_showSendReminderDialog();
|
||||
break;
|
||||
case 'export_data':
|
||||
_exportCotisationsData();
|
||||
break;
|
||||
case 'payment_history':
|
||||
_navigateToPaymentHistory();
|
||||
break;
|
||||
case 'reports':
|
||||
_showReportsDialog();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// Navigation vers la création de cotisation
|
||||
void _navigateToCreateCotisation() async {
|
||||
final result = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const CotisationCreatePage(),
|
||||
),
|
||||
);
|
||||
|
||||
if (result == true) {
|
||||
// Recharger la liste si une cotisation a été créée
|
||||
_loadData();
|
||||
}
|
||||
}
|
||||
|
||||
/// Navigation vers l'historique des paiements
|
||||
void _navigateToPaymentHistory() {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const PaymentHistoryPage(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Affiche le dialogue de paiement groupé
|
||||
void _showBulkPaymentDialog() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Paiement Groupé'),
|
||||
content: const Text('Fonctionnalité de paiement groupé à implémenter'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Fermer'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Affiche le dialogue d'envoi de rappels
|
||||
void _showSendReminderDialog() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Envoyer des Rappels'),
|
||||
content: const Text('Fonctionnalité d\'envoi de rappels à implémenter'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Fermer'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Export des données de cotisations
|
||||
void _exportCotisationsData() {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Fonctionnalité d\'export à implémenter'),
|
||||
backgroundColor: AppTheme.infoColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Affiche le dialogue des rapports
|
||||
void _showReportsDialog() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Rapports Financiers'),
|
||||
content: const Text('Fonctionnalité de rapports financiers à implémenter'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Fermer'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_cotisationsBloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,612 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../../core/di/injection.dart';
|
||||
import '../../../../core/models/cotisation_model.dart';
|
||||
import '../../../../core/models/payment_model.dart';
|
||||
import '../../../../shared/theme/app_theme.dart';
|
||||
import '../../../../shared/widgets/common/unified_page_layout.dart';
|
||||
import '../../../../shared/widgets/common/unified_search_bar.dart';
|
||||
import '../../../../shared/widgets/common/unified_filter_chip.dart';
|
||||
import '../../../../shared/widgets/common/unified_empty_state.dart';
|
||||
import '../../../../shared/widgets/common/unified_loading_indicator.dart';
|
||||
import '../bloc/cotisations_bloc.dart';
|
||||
import '../bloc/cotisations_event.dart';
|
||||
import '../bloc/cotisations_state.dart';
|
||||
|
||||
/// Page d'historique des paiements
|
||||
class PaymentHistoryPage extends StatefulWidget {
|
||||
final String? membreId; // Filtrer par membre (optionnel)
|
||||
|
||||
const PaymentHistoryPage({
|
||||
super.key,
|
||||
this.membreId,
|
||||
});
|
||||
|
||||
@override
|
||||
State<PaymentHistoryPage> createState() => _PaymentHistoryPageState();
|
||||
}
|
||||
|
||||
class _PaymentHistoryPageState extends State<PaymentHistoryPage> {
|
||||
late CotisationsBloc _cotisationsBloc;
|
||||
final _searchController = TextEditingController();
|
||||
|
||||
// Filtres
|
||||
String _selectedPeriod = 'all';
|
||||
String _selectedStatus = 'all';
|
||||
String _selectedMethod = 'all';
|
||||
|
||||
// Options de filtres
|
||||
final List<Map<String, String>> _periodOptions = [
|
||||
{'value': 'all', 'label': 'Toutes les périodes'},
|
||||
{'value': 'today', 'label': 'Aujourd\'hui'},
|
||||
{'value': 'week', 'label': 'Cette semaine'},
|
||||
{'value': 'month', 'label': 'Ce mois'},
|
||||
{'value': 'year', 'label': 'Cette année'},
|
||||
];
|
||||
|
||||
final List<Map<String, String>> _statusOptions = [
|
||||
{'value': 'all', 'label': 'Tous les statuts'},
|
||||
{'value': 'COMPLETED', 'label': 'Complété'},
|
||||
{'value': 'PENDING', 'label': 'En attente'},
|
||||
{'value': 'FAILED', 'label': 'Échoué'},
|
||||
{'value': 'CANCELLED', 'label': 'Annulé'},
|
||||
];
|
||||
|
||||
final List<Map<String, String>> _methodOptions = [
|
||||
{'value': 'all', 'label': 'Toutes les méthodes'},
|
||||
{'value': 'WAVE', 'label': 'Wave Money'},
|
||||
{'value': 'ORANGE_MONEY', 'label': 'Orange Money'},
|
||||
{'value': 'MTN_MONEY', 'label': 'MTN Money'},
|
||||
{'value': 'CASH', 'label': 'Espèces'},
|
||||
{'value': 'BANK_TRANSFER', 'label': 'Virement bancaire'},
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_cotisationsBloc = getIt<CotisationsBloc>();
|
||||
_loadPaymentHistory();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _loadPaymentHistory() {
|
||||
_cotisationsBloc.add(LoadPaymentHistory(
|
||||
membreId: widget.membreId,
|
||||
period: _selectedPeriod,
|
||||
status: _selectedStatus,
|
||||
method: _selectedMethod,
|
||||
searchQuery: _searchController.text.trim(),
|
||||
));
|
||||
}
|
||||
|
||||
void _onSearchChanged(String query) {
|
||||
// Debounce la recherche
|
||||
Future.delayed(const Duration(milliseconds: 500), () {
|
||||
if (_searchController.text == query) {
|
||||
_loadPaymentHistory();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _onFilterChanged() {
|
||||
_loadPaymentHistory();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: _cotisationsBloc,
|
||||
child: UnifiedPageLayout(
|
||||
title: 'Historique des Paiements',
|
||||
backgroundColor: AppTheme.backgroundLight,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.file_download),
|
||||
onPressed: _exportHistory,
|
||||
tooltip: 'Exporter',
|
||||
),
|
||||
],
|
||||
body: Column(
|
||||
children: [
|
||||
// Barre de recherche
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: UnifiedSearchBar(
|
||||
controller: _searchController,
|
||||
hintText: 'Rechercher par membre, référence...',
|
||||
onChanged: _onSearchChanged,
|
||||
),
|
||||
),
|
||||
|
||||
// Filtres
|
||||
_buildFilters(),
|
||||
|
||||
// Liste des paiements
|
||||
Expanded(
|
||||
child: BlocBuilder<CotisationsBloc, CotisationsState>(
|
||||
builder: (context, state) {
|
||||
if (state is CotisationsLoading) {
|
||||
return const UnifiedLoadingIndicator();
|
||||
} else if (state is PaymentHistoryLoaded) {
|
||||
if (state.payments.isEmpty) {
|
||||
return UnifiedEmptyState(
|
||||
icon: Icons.payment,
|
||||
title: 'Aucun paiement trouvé',
|
||||
subtitle: 'Aucun paiement ne correspond à vos critères de recherche',
|
||||
actionText: 'Réinitialiser les filtres',
|
||||
onActionPressed: _resetFilters,
|
||||
);
|
||||
}
|
||||
return _buildPaymentsList(state.payments);
|
||||
} else if (state is CotisationsError) {
|
||||
return UnifiedEmptyState(
|
||||
icon: Icons.error,
|
||||
title: 'Erreur de chargement',
|
||||
subtitle: state.message,
|
||||
actionText: 'Réessayer',
|
||||
onActionPressed: _loadPaymentHistory,
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFilters() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: [
|
||||
// Filtre période
|
||||
UnifiedFilterChip(
|
||||
label: _periodOptions.firstWhere((o) => o['value'] == _selectedPeriod)['label']!,
|
||||
isSelected: _selectedPeriod != 'all',
|
||||
onTap: () => _showPeriodFilter(),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
|
||||
// Filtre statut
|
||||
UnifiedFilterChip(
|
||||
label: _statusOptions.firstWhere((o) => o['value'] == _selectedStatus)['label']!,
|
||||
isSelected: _selectedStatus != 'all',
|
||||
onTap: () => _showStatusFilter(),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
|
||||
// Filtre méthode
|
||||
UnifiedFilterChip(
|
||||
label: _methodOptions.firstWhere((o) => o['value'] == _selectedMethod)['label']!,
|
||||
isSelected: _selectedMethod != 'all',
|
||||
onTap: () => _showMethodFilter(),
|
||||
),
|
||||
|
||||
// Bouton reset
|
||||
if (_selectedPeriod != 'all' || _selectedStatus != 'all' || _selectedMethod != 'all') ...[
|
||||
const SizedBox(width: 8),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: _resetFilters,
|
||||
tooltip: 'Réinitialiser les filtres',
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPaymentsList(List<PaymentModel> payments) {
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: payments.length,
|
||||
itemBuilder: (context, index) {
|
||||
final payment = payments[index];
|
||||
return _buildPaymentCard(payment);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPaymentCard(PaymentModel payment) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
child: InkWell(
|
||||
onTap: () => _showPaymentDetails(payment),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// En-tête avec statut
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
payment.nomMembre ?? 'Membre inconnu',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Réf: ${payment.referenceTransaction}',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
_buildStatusChip(payment.statut),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Montant et méthode
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${payment.montant.toStringAsFixed(0)} XOF',
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.accentColor,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_getMethodLabel(payment.methodePaiement),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
_formatDate(payment.dateCreation),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
if (payment.dateTraitement != null)
|
||||
Text(
|
||||
'Traité: ${_formatDate(payment.dateTraitement!)}',
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Description si disponible
|
||||
if (payment.description?.isNotEmpty == true) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
payment.description!,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatusChip(String statut) {
|
||||
Color backgroundColor;
|
||||
Color textColor;
|
||||
String label;
|
||||
|
||||
switch (statut) {
|
||||
case 'COMPLETED':
|
||||
backgroundColor = AppTheme.successColor;
|
||||
textColor = Colors.white;
|
||||
label = 'Complété';
|
||||
break;
|
||||
case 'PENDING':
|
||||
backgroundColor = AppTheme.warningColor;
|
||||
textColor = Colors.white;
|
||||
label = 'En attente';
|
||||
break;
|
||||
case 'FAILED':
|
||||
backgroundColor = AppTheme.errorColor;
|
||||
textColor = Colors.white;
|
||||
label = 'Échoué';
|
||||
break;
|
||||
case 'CANCELLED':
|
||||
backgroundColor = Colors.grey;
|
||||
textColor = Colors.white;
|
||||
label = 'Annulé';
|
||||
break;
|
||||
default:
|
||||
backgroundColor = Colors.grey.shade300;
|
||||
textColor = AppTheme.textPrimary;
|
||||
label = statut;
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: textColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _getMethodLabel(String method) {
|
||||
switch (method) {
|
||||
case 'WAVE': return 'Wave Money';
|
||||
case 'ORANGE_MONEY': return 'Orange Money';
|
||||
case 'MTN_MONEY': return 'MTN Money';
|
||||
case 'CASH': return 'Espèces';
|
||||
case 'BANK_TRANSFER': return 'Virement bancaire';
|
||||
default: return method;
|
||||
}
|
||||
}
|
||||
|
||||
String _formatDate(DateTime date) {
|
||||
return '${date.day}/${date.month}/${date.year} ${date.hour}:${date.minute.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
void _showPeriodFilter() {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => _buildFilterBottomSheet(
|
||||
'Période',
|
||||
_periodOptions,
|
||||
_selectedPeriod,
|
||||
(value) {
|
||||
setState(() {
|
||||
_selectedPeriod = value;
|
||||
});
|
||||
_onFilterChanged();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showStatusFilter() {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => _buildFilterBottomSheet(
|
||||
'Statut',
|
||||
_statusOptions,
|
||||
_selectedStatus,
|
||||
(value) {
|
||||
setState(() {
|
||||
_selectedStatus = value;
|
||||
});
|
||||
_onFilterChanged();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showMethodFilter() {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => _buildFilterBottomSheet(
|
||||
'Méthode de paiement',
|
||||
_methodOptions,
|
||||
_selectedMethod,
|
||||
(value) {
|
||||
setState(() {
|
||||
_selectedMethod = value;
|
||||
});
|
||||
_onFilterChanged();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFilterBottomSheet(
|
||||
String title,
|
||||
List<Map<String, String>> options,
|
||||
String selectedValue,
|
||||
Function(String) onSelected,
|
||||
) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
...options.map((option) {
|
||||
final isSelected = option['value'] == selectedValue;
|
||||
return ListTile(
|
||||
title: Text(option['label']!),
|
||||
trailing: isSelected ? const Icon(Icons.check, color: AppTheme.accentColor) : null,
|
||||
onTap: () {
|
||||
onSelected(option['value']!);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _resetFilters() {
|
||||
setState(() {
|
||||
_selectedPeriod = 'all';
|
||||
_selectedStatus = 'all';
|
||||
_selectedMethod = 'all';
|
||||
_searchController.clear();
|
||||
});
|
||||
_onFilterChanged();
|
||||
}
|
||||
|
||||
void _showPaymentDetails(PaymentModel payment) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => DraggableScrollableSheet(
|
||||
initialChildSize: 0.7,
|
||||
maxChildSize: 0.9,
|
||||
minChildSize: 0.5,
|
||||
builder: (context, scrollController) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Handle
|
||||
Center(
|
||||
child: Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade300,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Titre
|
||||
Text(
|
||||
'Détails du Paiement',
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Contenu scrollable
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
controller: scrollController,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildDetailRow('Référence', payment.referenceTransaction),
|
||||
_buildDetailRow('Membre', payment.nomMembre ?? 'N/A'),
|
||||
_buildDetailRow('Montant', '${payment.montant.toStringAsFixed(0)} XOF'),
|
||||
_buildDetailRow('Méthode', _getMethodLabel(payment.methodePaiement)),
|
||||
_buildDetailRow('Statut', _getStatusLabel(payment.statut)),
|
||||
_buildDetailRow('Date de création', _formatDate(payment.dateCreation)),
|
||||
if (payment.dateTraitement != null)
|
||||
_buildDetailRow('Date de traitement', _formatDate(payment.dateTraitement!)),
|
||||
if (payment.description?.isNotEmpty == true)
|
||||
_buildDetailRow('Description', payment.description!),
|
||||
if (payment.referencePaiementExterne?.isNotEmpty == true)
|
||||
_buildDetailRow('Référence externe', payment.referencePaiementExterne!),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDetailRow(String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 120,
|
||||
child: Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _getStatusLabel(String status) {
|
||||
switch (status) {
|
||||
case 'COMPLETED': return 'Complété';
|
||||
case 'PENDING': return 'En attente';
|
||||
case 'FAILED': return 'Échoué';
|
||||
case 'CANCELLED': return 'Annulé';
|
||||
default: return status;
|
||||
}
|
||||
}
|
||||
|
||||
void _exportHistory() {
|
||||
// TODO: Implémenter l'export de l'historique
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Fonctionnalité d\'export à implémenter'),
|
||||
backgroundColor: AppTheme.infoColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,668 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../../../../core/di/injection.dart';
|
||||
import '../../../../core/services/wave_integration_service.dart';
|
||||
import '../../../../core/services/wave_payment_service.dart';
|
||||
import '../../../../core/models/cotisation_model.dart';
|
||||
import '../../../../shared/theme/app_theme.dart';
|
||||
import '../../../../shared/widgets/buttons/primary_button.dart';
|
||||
import '../../../../shared/widgets/common/unified_page_layout.dart';
|
||||
|
||||
/// Page de démonstration de l'intégration Wave Money
|
||||
/// Permet de tester toutes les fonctionnalités Wave
|
||||
class WaveDemoPage extends StatefulWidget {
|
||||
const WaveDemoPage({super.key});
|
||||
|
||||
@override
|
||||
State<WaveDemoPage> createState() => _WaveDemoPageState();
|
||||
}
|
||||
|
||||
class _WaveDemoPageState extends State<WaveDemoPage>
|
||||
with TickerProviderStateMixin {
|
||||
late WaveIntegrationService _waveIntegrationService;
|
||||
late WavePaymentService _wavePaymentService;
|
||||
late AnimationController _animationController;
|
||||
late Animation<double> _fadeAnimation;
|
||||
|
||||
final _amountController = TextEditingController(text: '5000');
|
||||
final _phoneController = TextEditingController(text: '77123456');
|
||||
final _nameController = TextEditingController(text: 'Test User');
|
||||
|
||||
bool _isLoading = false;
|
||||
String _lastResult = '';
|
||||
WavePaymentStats? _stats;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_waveIntegrationService = getIt<WaveIntegrationService>();
|
||||
_wavePaymentService = getIt<WavePaymentService>();
|
||||
|
||||
_animationController = AnimationController(
|
||||
duration: const Duration(milliseconds: 800),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||
CurvedAnimation(parent: _animationController, curve: Curves.easeOut),
|
||||
);
|
||||
|
||||
_animationController.forward();
|
||||
_loadStats();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_amountController.dispose();
|
||||
_phoneController.dispose();
|
||||
_nameController.dispose();
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return UnifiedPageLayout(
|
||||
title: 'Wave Money Demo',
|
||||
subtitle: 'Test d\'intégration Wave Money',
|
||||
showBackButton: true,
|
||||
child: FadeTransition(
|
||||
opacity: _fadeAnimation,
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildWaveHeader(),
|
||||
const SizedBox(height: 24),
|
||||
_buildTestForm(),
|
||||
const SizedBox(height: 24),
|
||||
_buildQuickActions(),
|
||||
const SizedBox(height: 24),
|
||||
_buildStatsSection(),
|
||||
const SizedBox(height: 24),
|
||||
_buildResultSection(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWaveHeader() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFF00D4FF), Color(0xFF0099CC)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0xFF00D4FF).withOpacity(0.3),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 60,
|
||||
height: 60,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.waves,
|
||||
size: 32,
|
||||
color: Color(0xFF00D4FF),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
const Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Wave Money Integration',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Test et démonstration',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.white70,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Row(
|
||||
children: [
|
||||
Icon(Icons.info_outline, color: Colors.white, size: 16),
|
||||
SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Environnement de test - Aucun paiement réel ne sera effectué',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTestForm() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppTheme.borderLight),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Paramètres de test',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Montant
|
||||
TextFormField(
|
||||
controller: _amountController,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Montant (XOF)',
|
||||
prefixIcon: Icon(Icons.attach_money),
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Numéro de téléphone
|
||||
TextFormField(
|
||||
controller: _phoneController,
|
||||
keyboardType: TextInputType.phone,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Numéro Wave Money',
|
||||
prefixIcon: Icon(Icons.phone),
|
||||
prefixText: '+225 ',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Nom
|
||||
TextFormField(
|
||||
controller: _nameController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Nom du payeur',
|
||||
prefixIcon: Icon(Icons.person),
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Bouton de test
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: PrimaryButton(
|
||||
text: _isLoading ? 'Test en cours...' : 'Tester le paiement Wave',
|
||||
icon: _isLoading ? null : Icons.play_arrow,
|
||||
onPressed: _isLoading ? null : _testWavePayment,
|
||||
isLoading: _isLoading,
|
||||
backgroundColor: const Color(0xFF00D4FF),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQuickActions() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppTheme.borderLight),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Actions rapides',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
_buildActionChip(
|
||||
'Calculer frais',
|
||||
Icons.calculate,
|
||||
_calculateFees,
|
||||
),
|
||||
_buildActionChip(
|
||||
'Historique',
|
||||
Icons.history,
|
||||
_showHistory,
|
||||
),
|
||||
_buildActionChip(
|
||||
'Statistiques',
|
||||
Icons.analytics,
|
||||
_loadStats,
|
||||
),
|
||||
_buildActionChip(
|
||||
'Vider cache',
|
||||
Icons.clear_all,
|
||||
_clearCache,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionChip(String label, IconData icon, VoidCallback onPressed) {
|
||||
return ActionChip(
|
||||
avatar: Icon(icon, size: 16),
|
||||
label: Text(label),
|
||||
onPressed: onPressed,
|
||||
backgroundColor: AppTheme.backgroundLight,
|
||||
side: const BorderSide(color: AppTheme.borderLight),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatsSection() {
|
||||
if (_stats == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppTheme.borderLight),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Statistiques Wave Money',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
GridView.count(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
crossAxisCount: 2,
|
||||
childAspectRatio: 2.5,
|
||||
crossAxisSpacing: 12,
|
||||
mainAxisSpacing: 12,
|
||||
children: [
|
||||
_buildStatCard(
|
||||
'Total paiements',
|
||||
_stats!.totalPayments.toString(),
|
||||
Icons.payment,
|
||||
AppTheme.primaryColor,
|
||||
),
|
||||
_buildStatCard(
|
||||
'Réussis',
|
||||
_stats!.completedPayments.toString(),
|
||||
Icons.check_circle,
|
||||
AppTheme.successColor,
|
||||
),
|
||||
_buildStatCard(
|
||||
'Montant total',
|
||||
'${_stats!.totalAmount.toStringAsFixed(0)} XOF',
|
||||
Icons.attach_money,
|
||||
AppTheme.warningColor,
|
||||
),
|
||||
_buildStatCard(
|
||||
'Taux de réussite',
|
||||
'${_stats!.successRate.toStringAsFixed(1)}%',
|
||||
Icons.trending_up,
|
||||
AppTheme.infoColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatCard(String title, String value, IconData icon, Color color) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: color.withOpacity(0.3)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(icon, color: color, size: 16),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: color,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildResultSection() {
|
||||
if (_lastResult.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppTheme.borderLight),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Text(
|
||||
'Dernier résultat',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.copy, size: 16),
|
||||
onPressed: () {
|
||||
Clipboard.setData(ClipboardData(text: _lastResult));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Résultat copié')),
|
||||
);
|
||||
},
|
||||
tooltip: 'Copier',
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.backgroundLight,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: AppTheme.borderLight),
|
||||
),
|
||||
child: Text(
|
||||
_lastResult,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontFamily: 'monospace',
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Actions
|
||||
Future<void> _testWavePayment() async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_lastResult = '';
|
||||
});
|
||||
|
||||
try {
|
||||
final amount = double.tryParse(_amountController.text) ?? 0;
|
||||
if (amount <= 0) {
|
||||
throw Exception('Montant invalide');
|
||||
}
|
||||
|
||||
// Créer une cotisation de test
|
||||
final testCotisation = CotisationModel(
|
||||
id: 'test_${DateTime.now().millisecondsSinceEpoch}',
|
||||
numeroReference: 'TEST-${DateTime.now().millisecondsSinceEpoch}',
|
||||
membreId: 'test_member',
|
||||
nomMembre: _nameController.text,
|
||||
typeCotisation: 'MENSUELLE',
|
||||
montantDu: amount,
|
||||
montantPaye: 0,
|
||||
codeDevise: 'XOF',
|
||||
dateEcheance: DateTime.now().add(const Duration(days: 30)),
|
||||
statut: 'EN_ATTENTE',
|
||||
recurrente: false,
|
||||
nombreRappels: 0,
|
||||
annee: DateTime.now().year,
|
||||
dateCreation: DateTime.now(),
|
||||
);
|
||||
|
||||
// Initier le paiement Wave
|
||||
final result = await _waveIntegrationService.initiateWavePayment(
|
||||
cotisationId: testCotisation.id,
|
||||
montant: amount,
|
||||
numeroTelephone: _phoneController.text,
|
||||
nomPayeur: _nameController.text,
|
||||
metadata: {
|
||||
'test_mode': true,
|
||||
'demo_page': true,
|
||||
},
|
||||
);
|
||||
|
||||
setState(() {
|
||||
_lastResult = '''
|
||||
Test de paiement Wave Money
|
||||
|
||||
Résultat: ${result.success ? 'SUCCÈS' : 'ÉCHEC'}
|
||||
${result.success ? '''
|
||||
ID Paiement: ${result.payment?.id}
|
||||
Session Wave: ${result.session?.waveSessionId}
|
||||
URL Checkout: ${result.checkoutUrl}
|
||||
Montant: ${amount.toStringAsFixed(0)} XOF
|
||||
Frais: ${_wavePaymentService.calculateWaveFees(amount).toStringAsFixed(0)} XOF
|
||||
''' : '''
|
||||
Erreur: ${result.errorMessage}
|
||||
'''}
|
||||
Timestamp: ${DateTime.now().toIso8601String()}
|
||||
'''.trim();
|
||||
});
|
||||
|
||||
// Feedback haptique
|
||||
HapticFeedback.lightImpact();
|
||||
|
||||
// Recharger les statistiques
|
||||
await _loadStats();
|
||||
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_lastResult = 'Erreur lors du test: $e';
|
||||
});
|
||||
} finally {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _calculateFees() {
|
||||
final amount = double.tryParse(_amountController.text) ?? 0;
|
||||
if (amount <= 0) {
|
||||
setState(() {
|
||||
_lastResult = 'Montant invalide pour le calcul des frais';
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
final fees = _wavePaymentService.calculateWaveFees(amount);
|
||||
final total = amount + fees;
|
||||
|
||||
setState(() {
|
||||
_lastResult = '''
|
||||
Calcul des frais Wave Money
|
||||
|
||||
Montant: ${amount.toStringAsFixed(0)} XOF
|
||||
Frais Wave: ${fees.toStringAsFixed(0)} XOF
|
||||
Total: ${total.toStringAsFixed(0)} XOF
|
||||
|
||||
Barème Wave CI 2024:
|
||||
• 0-2000 XOF: Gratuit
|
||||
• 2001-10000 XOF: 25 XOF
|
||||
• 10001-50000 XOF: 100 XOF
|
||||
• 50001-100000 XOF: 200 XOF
|
||||
• 100001-500000 XOF: 500 XOF
|
||||
• >500000 XOF: 0.1% du montant
|
||||
'''.trim();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _showHistory() async {
|
||||
try {
|
||||
final history = await _waveIntegrationService.getWavePaymentHistory(limit: 10);
|
||||
|
||||
setState(() {
|
||||
_lastResult = '''
|
||||
Historique des paiements Wave (10 derniers)
|
||||
|
||||
${history.isEmpty ? 'Aucun paiement trouvé' : history.map((payment) => '''
|
||||
• ${payment.numeroReference} - ${payment.montant.toStringAsFixed(0)} XOF
|
||||
Statut: ${payment.statut}
|
||||
Date: ${payment.dateTransaction.toString().substring(0, 16)}
|
||||
''').join('\n')}
|
||||
|
||||
Total: ${history.length} paiement(s)
|
||||
'''.trim();
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_lastResult = 'Erreur lors de la récupération de l\'historique: $e';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadStats() async {
|
||||
try {
|
||||
final stats = await _waveIntegrationService.getWavePaymentStats();
|
||||
setState(() {
|
||||
_stats = stats;
|
||||
});
|
||||
} catch (e) {
|
||||
print('Erreur lors du chargement des statistiques: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _clearCache() async {
|
||||
try {
|
||||
// TODO: Implémenter le nettoyage du cache
|
||||
setState(() {
|
||||
_lastResult = 'Cache Wave Money vidé avec succès';
|
||||
_stats = null;
|
||||
});
|
||||
await _loadStats();
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_lastResult = 'Erreur lors du nettoyage du cache: $e';
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,697 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import '../../../../core/di/injection.dart';
|
||||
import '../../../../core/models/cotisation_model.dart';
|
||||
import '../../../../core/models/payment_model.dart';
|
||||
import '../../../../core/models/wave_checkout_session_model.dart';
|
||||
import '../../../../core/services/wave_payment_service.dart';
|
||||
import '../../../../shared/theme/app_theme.dart';
|
||||
import '../../../../shared/widgets/buttons/primary_button.dart';
|
||||
import '../../../../shared/widgets/common/unified_page_layout.dart';
|
||||
import '../bloc/cotisations_bloc.dart';
|
||||
import '../bloc/cotisations_event.dart';
|
||||
import '../bloc/cotisations_state.dart';
|
||||
|
||||
/// Page dédiée aux paiements Wave Money
|
||||
/// Interface moderne et sécurisée pour les paiements mobiles
|
||||
class WavePaymentPage extends StatefulWidget {
|
||||
final CotisationModel cotisation;
|
||||
|
||||
const WavePaymentPage({
|
||||
super.key,
|
||||
required this.cotisation,
|
||||
});
|
||||
|
||||
@override
|
||||
State<WavePaymentPage> createState() => _WavePaymentPageState();
|
||||
}
|
||||
|
||||
class _WavePaymentPageState extends State<WavePaymentPage>
|
||||
with TickerProviderStateMixin {
|
||||
late CotisationsBloc _cotisationsBloc;
|
||||
late WavePaymentService _wavePaymentService;
|
||||
late AnimationController _animationController;
|
||||
late AnimationController _pulseController;
|
||||
late Animation<double> _fadeAnimation;
|
||||
late Animation<double> _slideAnimation;
|
||||
late Animation<double> _pulseAnimation;
|
||||
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _phoneController = TextEditingController();
|
||||
final _nameController = TextEditingController();
|
||||
final _emailController = TextEditingController();
|
||||
|
||||
bool _isProcessing = false;
|
||||
bool _termsAccepted = false;
|
||||
WaveCheckoutSessionModel? _currentSession;
|
||||
String? _paymentUrl;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_cotisationsBloc = getIt<CotisationsBloc>();
|
||||
_wavePaymentService = getIt<WavePaymentService>();
|
||||
|
||||
// Animations
|
||||
_animationController = AnimationController(
|
||||
duration: const Duration(milliseconds: 800),
|
||||
vsync: this,
|
||||
);
|
||||
_pulseController = AnimationController(
|
||||
duration: const Duration(milliseconds: 1500),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||
CurvedAnimation(parent: _animationController, curve: Curves.easeOut),
|
||||
);
|
||||
_slideAnimation = Tween<double>(begin: 50.0, end: 0.0).animate(
|
||||
CurvedAnimation(parent: _animationController, curve: Curves.easeOutCubic),
|
||||
);
|
||||
_pulseAnimation = Tween<double>(begin: 1.0, end: 1.1).animate(
|
||||
CurvedAnimation(parent: _pulseController, curve: Curves.easeInOut),
|
||||
);
|
||||
|
||||
_animationController.forward();
|
||||
_pulseController.repeat(reverse: true);
|
||||
|
||||
// Pré-remplir les champs si disponible
|
||||
_nameController.text = widget.cotisation.nomMembre;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_phoneController.dispose();
|
||||
_nameController.dispose();
|
||||
_emailController.dispose();
|
||||
_animationController.dispose();
|
||||
_pulseController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: _cotisationsBloc,
|
||||
child: UnifiedPageLayout(
|
||||
title: 'Paiement Wave Money',
|
||||
subtitle: 'Paiement sécurisé et instantané',
|
||||
showBackButton: true,
|
||||
backgroundColor: AppTheme.backgroundLight,
|
||||
child: BlocConsumer<CotisationsBloc, CotisationsState>(
|
||||
listener: _handleBlocState,
|
||||
builder: (context, state) {
|
||||
return FadeTransition(
|
||||
opacity: _fadeAnimation,
|
||||
child: Transform.translate(
|
||||
offset: Offset(0, _slideAnimation.value),
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildWaveHeader(),
|
||||
const SizedBox(height: 24),
|
||||
_buildCotisationSummary(),
|
||||
const SizedBox(height: 24),
|
||||
_buildPaymentForm(),
|
||||
const SizedBox(height: 24),
|
||||
_buildSecurityInfo(),
|
||||
const SizedBox(height: 24),
|
||||
_buildPaymentButton(state),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWaveHeader() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFF00D4FF), Color(0xFF0099CC)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0xFF00D4FF).withOpacity(0.3),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
ScaleTransition(
|
||||
scale: _pulseAnimation,
|
||||
child: Container(
|
||||
width: 60,
|
||||
height: 60,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.waves,
|
||||
size: 32,
|
||||
color: Color(0xFF00D4FF),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Wave Money',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const Text(
|
||||
'Paiement mobile sécurisé',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.white70,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Text(
|
||||
'🇨🇮 Côte d\'Ivoire',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCotisationSummary() {
|
||||
final remainingAmount = widget.cotisation.montantDu - widget.cotisation.montantPaye;
|
||||
final fees = _wavePaymentService.calculateWaveFees(remainingAmount);
|
||||
final total = remainingAmount + fees;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppTheme.borderLight),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Résumé de la cotisation',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildSummaryRow('Type', widget.cotisation.typeCotisation),
|
||||
_buildSummaryRow('Membre', widget.cotisation.nomMembre),
|
||||
_buildSummaryRow('Référence', widget.cotisation.numeroReference),
|
||||
const Divider(height: 24),
|
||||
_buildSummaryRow('Montant', '${remainingAmount.toStringAsFixed(0)} XOF'),
|
||||
_buildSummaryRow('Frais Wave', '${fees.toStringAsFixed(0)} XOF'),
|
||||
const Divider(height: 24),
|
||||
_buildSummaryRow(
|
||||
'Total à payer',
|
||||
'${total.toStringAsFixed(0)} XOF',
|
||||
isTotal: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSummaryRow(String label, String value, {bool isTotal = false}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: isTotal ? 16 : 14,
|
||||
fontWeight: isTotal ? FontWeight.bold : FontWeight.normal,
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: isTotal ? 16 : 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isTotal ? AppTheme.primaryColor : AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPaymentForm() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppTheme.borderLight),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Informations de paiement',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildPhoneField(),
|
||||
const SizedBox(height: 16),
|
||||
_buildNameField(),
|
||||
const SizedBox(height: 16),
|
||||
_buildEmailField(),
|
||||
const SizedBox(height: 16),
|
||||
_buildTermsCheckbox(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPhoneField() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Numéro Wave Money *',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: _phoneController,
|
||||
keyboardType: TextInputType.phone,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
LengthLimitingTextInputFormatter(10),
|
||||
],
|
||||
decoration: InputDecoration(
|
||||
hintText: '77 123 45 67',
|
||||
prefixIcon: const Icon(Icons.phone_android, color: Color(0xFF00D4FF)),
|
||||
prefixText: '+225 ',
|
||||
prefixStyle: const TextStyle(
|
||||
color: AppTheme.textSecondary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: AppTheme.borderLight),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: Color(0xFF00D4FF), width: 2),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: AppTheme.backgroundLight,
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Veuillez saisir votre numéro Wave Money';
|
||||
}
|
||||
if (value.length < 8) {
|
||||
return 'Numéro invalide (minimum 8 chiffres)';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNameField() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Nom complet *',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: _nameController,
|
||||
textCapitalization: TextCapitalization.words,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Votre nom complet',
|
||||
prefixIcon: const Icon(Icons.person, color: Color(0xFF00D4FF)),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: AppTheme.borderLight),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: Color(0xFF00D4FF), width: 2),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: AppTheme.backgroundLight,
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return 'Veuillez saisir votre nom complet';
|
||||
}
|
||||
if (value.trim().length < 2) {
|
||||
return 'Le nom doit contenir au moins 2 caractères';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmailField() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Email (optionnel)',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: _emailController,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'votre.email@exemple.com',
|
||||
prefixIcon: const Icon(Icons.email, color: Color(0xFF00D4FF)),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: AppTheme.borderLight),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: Color(0xFF00D4FF), width: 2),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: AppTheme.backgroundLight,
|
||||
),
|
||||
validator: (value) {
|
||||
if (value != null && value.isNotEmpty) {
|
||||
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
|
||||
return 'Format d\'email invalide';
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTermsCheckbox() {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Checkbox(
|
||||
value: _termsAccepted,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_termsAccepted = value ?? false;
|
||||
});
|
||||
},
|
||||
activeColor: const Color(0xFF00D4FF),
|
||||
),
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_termsAccepted = !_termsAccepted;
|
||||
});
|
||||
},
|
||||
child: const Text(
|
||||
'J\'accepte les conditions d\'utilisation de Wave Money et autorise le prélèvement du montant indiqué.',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSecurityInfo() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF0F9FF),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: const Color(0xFF00D4FF).withOpacity(0.2)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF00D4FF).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.security,
|
||||
color: Color(0xFF00D4FF),
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'Paiement 100% sécurisé',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text(
|
||||
'• Chiffrement SSL/TLS de bout en bout\n'
|
||||
'• Conformité aux standards PCI DSS\n'
|
||||
'• Aucune donnée bancaire stockée\n'
|
||||
'• Transaction instantanée et traçable',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppTheme.textSecondary,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPaymentButton(CotisationsState state) {
|
||||
final isLoading = state is PaymentInProgress || _isProcessing;
|
||||
final canPay = _formKey.currentState?.validate() == true &&
|
||||
_termsAccepted &&
|
||||
_phoneController.text.isNotEmpty &&
|
||||
!isLoading;
|
||||
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
child: PrimaryButton(
|
||||
text: isLoading
|
||||
? 'Traitement en cours...'
|
||||
: 'Payer avec Wave Money',
|
||||
icon: isLoading ? null : Icons.waves,
|
||||
onPressed: canPay ? _processWavePayment : null,
|
||||
isLoading: isLoading,
|
||||
backgroundColor: const Color(0xFF00D4FF),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleBlocState(BuildContext context, CotisationsState state) {
|
||||
if (state is PaymentSuccess) {
|
||||
_showPaymentSuccessDialog(state.payment);
|
||||
} else if (state is PaymentFailure) {
|
||||
_showPaymentErrorDialog(state.errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
void _processWavePayment() async {
|
||||
if (!_formKey.currentState!.validate() || !_termsAccepted) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isProcessing = true;
|
||||
});
|
||||
|
||||
try {
|
||||
final remainingAmount = widget.cotisation.montantDu - widget.cotisation.montantPaye;
|
||||
|
||||
// Initier le paiement Wave via le BLoC
|
||||
_cotisationsBloc.add(InitiatePayment(
|
||||
cotisationId: widget.cotisation.id,
|
||||
montant: remainingAmount,
|
||||
methodePaiement: 'WAVE',
|
||||
numeroTelephone: _phoneController.text.trim(),
|
||||
nomPayeur: _nameController.text.trim(),
|
||||
emailPayeur: _emailController.text.trim().isEmpty
|
||||
? null
|
||||
: _emailController.text.trim(),
|
||||
));
|
||||
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_isProcessing = false;
|
||||
});
|
||||
_showPaymentErrorDialog('Erreur lors de l\'initiation du paiement: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void _showPaymentSuccessDialog(PaymentModel payment) {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Row(
|
||||
children: [
|
||||
Icon(Icons.check_circle, color: AppTheme.successColor, size: 28),
|
||||
SizedBox(width: 8),
|
||||
Text('Paiement réussi !'),
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Votre paiement de ${payment.montant.toStringAsFixed(0)} XOF a été confirmé.'),
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.backgroundLight,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Référence: ${payment.numeroReference}'),
|
||||
Text('Transaction: ${payment.numeroTransaction ?? 'N/A'}'),
|
||||
Text('Date: ${DateTime.now().toString().substring(0, 16)}'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(context).pop(); // Retour à la liste
|
||||
},
|
||||
child: const Text('Fermer'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showPaymentErrorDialog(String errorMessage) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Row(
|
||||
children: [
|
||||
Icon(Icons.error, color: AppTheme.errorColor, size: 28),
|
||||
SizedBox(width: 8),
|
||||
Text('Erreur de paiement'),
|
||||
],
|
||||
),
|
||||
content: Text(errorMessage),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('OK'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,363 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../../../../core/models/cotisation_model.dart';
|
||||
import '../../../../core/services/wave_payment_service.dart';
|
||||
import '../../../../core/di/injection.dart';
|
||||
import '../../../../shared/theme/app_theme.dart';
|
||||
import '../../../../shared/widgets/buttons/primary_button.dart';
|
||||
import '../pages/wave_payment_page.dart';
|
||||
|
||||
/// Widget d'intégration Wave Money pour les cotisations
|
||||
/// Affiche les options de paiement Wave avec calcul des frais
|
||||
class WavePaymentWidget extends StatefulWidget {
|
||||
final CotisationModel cotisation;
|
||||
final VoidCallback? onPaymentInitiated;
|
||||
final bool showFullInterface;
|
||||
|
||||
const WavePaymentWidget({
|
||||
super.key,
|
||||
required this.cotisation,
|
||||
this.onPaymentInitiated,
|
||||
this.showFullInterface = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<WavePaymentWidget> createState() => _WavePaymentWidgetState();
|
||||
}
|
||||
|
||||
class _WavePaymentWidgetState extends State<WavePaymentWidget>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late WavePaymentService _wavePaymentService;
|
||||
late AnimationController _animationController;
|
||||
late Animation<double> _scaleAnimation;
|
||||
late Animation<double> _fadeAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_wavePaymentService = getIt<WavePaymentService>();
|
||||
|
||||
_animationController = AnimationController(
|
||||
duration: const Duration(milliseconds: 600),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_scaleAnimation = Tween<double>(begin: 0.8, end: 1.0).animate(
|
||||
CurvedAnimation(parent: _animationController, curve: Curves.elasticOut),
|
||||
);
|
||||
|
||||
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||
CurvedAnimation(parent: _animationController, curve: Curves.easeOut),
|
||||
);
|
||||
|
||||
_animationController.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FadeTransition(
|
||||
opacity: _fadeAnimation,
|
||||
child: ScaleTransition(
|
||||
scale: _scaleAnimation,
|
||||
child: widget.showFullInterface
|
||||
? _buildFullInterface()
|
||||
: _buildCompactInterface(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFullInterface() {
|
||||
final remainingAmount = widget.cotisation.montantDu - widget.cotisation.montantPaye;
|
||||
final fees = _wavePaymentService.calculateWaveFees(remainingAmount);
|
||||
final total = remainingAmount + fees;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFF00D4FF), Color(0xFF0099CC)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0xFF00D4FF).withOpacity(0.3),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header Wave
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.waves,
|
||||
size: 28,
|
||||
color: Color(0xFF00D4FF),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
const Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Wave Money',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Paiement mobile instantané',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.white70,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Text(
|
||||
'🇨🇮 CI',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Détails du paiement
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.15),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildPaymentRow('Montant', '${remainingAmount.toStringAsFixed(0)} XOF'),
|
||||
_buildPaymentRow('Frais Wave', '${fees.toStringAsFixed(0)} XOF'),
|
||||
const Divider(color: Colors.white30, height: 20),
|
||||
_buildPaymentRow(
|
||||
'Total',
|
||||
'${total.toStringAsFixed(0)} XOF',
|
||||
isTotal: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Avantages Wave
|
||||
_buildAdvantages(),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Bouton de paiement
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: PrimaryButton(
|
||||
text: 'Payer avec Wave',
|
||||
icon: Icons.payment,
|
||||
onPressed: _navigateToWavePayment,
|
||||
backgroundColor: Colors.white,
|
||||
textColor: const Color(0xFF00D4FF),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCompactInterface() {
|
||||
final remainingAmount = widget.cotisation.montantDu - widget.cotisation.montantPaye;
|
||||
final fees = _wavePaymentService.calculateWaveFees(remainingAmount);
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: const Color(0xFF00D4FF).withOpacity(0.3)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0xFF00D4FF).withOpacity(0.1),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF00D4FF).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.waves,
|
||||
size: 24,
|
||||
color: Color(0xFF00D4FF),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Wave Money',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Frais: ${fees.toStringAsFixed(0)} XOF • Instantané',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
PrimaryButton(
|
||||
text: 'Payer',
|
||||
onPressed: _navigateToWavePayment,
|
||||
backgroundColor: const Color(0xFF00D4FF),
|
||||
isCompact: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPaymentRow(String label, String value, {bool isTotal = false}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: isTotal ? 16 : 14,
|
||||
fontWeight: isTotal ? FontWeight.bold : FontWeight.normal,
|
||||
color: Colors.white70,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: isTotal ? 16 : 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAdvantages() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Pourquoi choisir Wave ?',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_buildAdvantageItem('⚡', 'Paiement instantané'),
|
||||
_buildAdvantageItem('🔒', 'Sécurisé et fiable'),
|
||||
_buildAdvantageItem('💰', 'Frais les plus bas'),
|
||||
_buildAdvantageItem('📱', 'Simple et rapide'),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAdvantageItem(String icon, String text) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
icon,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
text,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.white70,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _navigateToWavePayment() {
|
||||
// Feedback haptique
|
||||
HapticFeedback.lightImpact();
|
||||
|
||||
// Callback si fourni
|
||||
widget.onPaymentInitiated?.call();
|
||||
|
||||
// Navigation vers la page de paiement Wave
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => WavePaymentPage(cotisation: widget.cotisation),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import '../../../../shared/theme/app_theme.dart';
|
||||
import '../../../../core/animations/page_transitions.dart';
|
||||
import '../../../demo/presentation/pages/animations_demo_page.dart';
|
||||
import '../../../debug/debug_api_test_page.dart';
|
||||
import '../../../performance/presentation/pages/performance_demo_page.dart';
|
||||
|
||||
// Imports des nouveaux widgets refactorisés
|
||||
import '../widgets/welcome/welcome_section_widget.dart';
|
||||
@@ -11,6 +12,9 @@ import '../widgets/actions/quick_actions_widget.dart';
|
||||
import '../widgets/activities/recent_activities_widget.dart';
|
||||
import '../widgets/charts/charts_analytics_widget.dart';
|
||||
|
||||
// Import de l'architecture unifiée pour amélioration progressive
|
||||
import '../../../../shared/widgets/common/unified_page_layout.dart';
|
||||
|
||||
/// Page principale du tableau de bord UnionFlow
|
||||
///
|
||||
/// Affiche une vue d'ensemble complète de l'association avec :
|
||||
@@ -27,76 +31,79 @@ class DashboardPage extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppTheme.backgroundLight,
|
||||
appBar: AppBar(
|
||||
title: const Text('Tableau de bord'),
|
||||
backgroundColor: AppTheme.primaryColor,
|
||||
elevation: 0,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.animation),
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(
|
||||
PageTransitions.morphWithBlur(const AnimationsDemoPage()),
|
||||
);
|
||||
},
|
||||
tooltip: 'Démonstration des animations',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.notifications_outlined),
|
||||
onPressed: () {
|
||||
// TODO: Implémenter la navigation vers les notifications
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.bug_report),
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(
|
||||
PageTransitions.slideFromRight(const DebugApiTestPage()),
|
||||
);
|
||||
},
|
||||
tooltip: 'Debug API',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.settings_outlined),
|
||||
onPressed: () {
|
||||
// TODO: Implémenter la navigation vers les paramètres
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 1. ACCUEIL & CONTEXTE - Message de bienvenue personnalisé
|
||||
const WelcomeSectionWidget(),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 2. VISION GLOBALE - Indicateurs clés de performance (KPI)
|
||||
// Vue d'ensemble immédiate de la santé de l'association
|
||||
const KPICardsWidget(),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 3. ACTIONS PRIORITAIRES - Actions rapides et gestion
|
||||
// Accès direct aux tâches critiques quotidiennes
|
||||
const QuickActionsWidget(),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 4. SUIVI TEMPS RÉEL - Flux d'activités en direct
|
||||
// Monitoring des événements récents et alertes
|
||||
const RecentActivitiesWidget(),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 5. ANALYSES APPROFONDIES - Graphiques et tendances
|
||||
// Analyses détaillées pour la prise de décision stratégique
|
||||
const ChartsAnalyticsWidget(),
|
||||
],
|
||||
),
|
||||
// Utilisation de UnifiedPageLayout pour améliorer la cohérence
|
||||
// tout en conservant tous les widgets spécialisés existants
|
||||
return UnifiedPageLayout(
|
||||
title: 'Tableau de bord',
|
||||
icon: Icons.dashboard,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.animation),
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(
|
||||
PageTransitions.morphWithBlur(const AnimationsDemoPage()),
|
||||
);
|
||||
},
|
||||
tooltip: 'Démonstration des animations',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.notifications_outlined),
|
||||
onPressed: () {
|
||||
// TODO: Implémenter la navigation vers les notifications
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.bug_report),
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(
|
||||
PageTransitions.slideFromRight(const DebugApiTestPage()),
|
||||
);
|
||||
},
|
||||
tooltip: 'Debug API',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.speed),
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(
|
||||
PageTransitions.slideFromRight(const PerformanceDemoPage()),
|
||||
);
|
||||
},
|
||||
tooltip: 'Performance',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.settings_outlined),
|
||||
onPressed: () {
|
||||
// TODO: Implémenter la navigation vers les paramètres
|
||||
},
|
||||
),
|
||||
],
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 1. ACCUEIL & CONTEXTE - Message de bienvenue personnalisé
|
||||
// CONSERVÉ: Widget spécialisé avec toutes ses fonctionnalités
|
||||
const WelcomeSectionWidget(),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 2. VISION GLOBALE - Indicateurs clés de performance (KPI)
|
||||
// CONSERVÉ: KPI enrichis avec détails, cibles, périodes
|
||||
const KPICardsWidget(),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 3. ACTIONS PRIORITAIRES - Actions rapides et gestion
|
||||
// CONSERVÉ: Grille d'actions organisées par catégories
|
||||
const QuickActionsWidget(),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 4. SUIVI TEMPS RÉEL - Flux d'activités en direct
|
||||
// CONSERVÉ: Activités avec indicateur "Live" et horodatage
|
||||
const RecentActivitiesWidget(),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 5. ANALYSES APPROFONDIES - Graphiques et tendances
|
||||
// CONSERVÉ: 1617 lignes de graphiques sophistiqués avec fl_chart
|
||||
const ChartsAnalyticsWidget(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,439 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../shared/widgets/unified_components.dart';
|
||||
import '../../../../shared/theme/app_theme.dart';
|
||||
import '../../../../core/animations/page_transitions.dart';
|
||||
import '../../../demo/presentation/pages/animations_demo_page.dart';
|
||||
import '../../../debug/debug_api_test_page.dart';
|
||||
|
||||
/// Page principale du tableau de bord UnionFlow - Version Unifiée
|
||||
///
|
||||
/// Utilise l'architecture unifiée avec composants standardisés pour :
|
||||
/// - Cohérence visuelle parfaite avec les autres onglets
|
||||
/// - Maintenabilité optimale et réutilisabilité maximale
|
||||
/// - Performance 60 FPS avec animations fluides
|
||||
/// - Expérience utilisateur homogène
|
||||
class DashboardPageUnified extends StatelessWidget {
|
||||
const DashboardPageUnified({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return UnifiedPageLayout(
|
||||
title: 'Tableau de bord',
|
||||
subtitle: 'Vue d\'ensemble de votre association',
|
||||
icon: Icons.dashboard,
|
||||
iconColor: AppTheme.primaryColor,
|
||||
actions: _buildActions(context),
|
||||
body: Column(
|
||||
children: [
|
||||
_buildWelcomeSection(),
|
||||
const SizedBox(height: AppTheme.spacingLarge),
|
||||
_buildKPISection(),
|
||||
const SizedBox(height: AppTheme.spacingLarge),
|
||||
_buildQuickActionsSection(),
|
||||
const SizedBox(height: AppTheme.spacingLarge),
|
||||
_buildRecentActivitiesSection(),
|
||||
const SizedBox(height: AppTheme.spacingLarge),
|
||||
_buildAnalyticsSection(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Actions de la barre d'outils
|
||||
List<Widget> _buildActions(BuildContext context) {
|
||||
return [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.animation),
|
||||
onPressed: () => Navigator.of(context).push(
|
||||
PageTransitions.morphWithBlur(const AnimationsDemoPage()),
|
||||
),
|
||||
tooltip: 'Démonstration des animations',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.notifications_outlined),
|
||||
onPressed: () {
|
||||
// TODO: Implémenter la navigation vers les notifications
|
||||
},
|
||||
tooltip: 'Notifications',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.bug_report),
|
||||
onPressed: () => Navigator.of(context).push(
|
||||
PageTransitions.slideFromRight(const DebugApiTestPage()),
|
||||
),
|
||||
tooltip: 'Debug API',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.settings_outlined),
|
||||
onPressed: () {
|
||||
// TODO: Implémenter la navigation vers les paramètres
|
||||
},
|
||||
tooltip: 'Paramètres',
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/// Section d'accueil personnalisée
|
||||
Widget _buildWelcomeSection() {
|
||||
return UnifiedCard.elevated(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(AppTheme.spacingLarge),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(AppTheme.spacingMedium),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.primaryColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(AppTheme.borderRadiusMedium),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.waving_hand,
|
||||
color: AppTheme.primaryColor,
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppTheme.spacingMedium),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Bonjour !',
|
||||
style: AppTheme.headlineSmall.copyWith(
|
||||
color: AppTheme.primaryColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppTheme.spacingXSmall),
|
||||
Text(
|
||||
'Bienvenue sur votre tableau de bord UnionFlow',
|
||||
style: AppTheme.bodyMedium.copyWith(
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Section des indicateurs clés de performance
|
||||
Widget _buildKPISection() {
|
||||
final kpis = [
|
||||
UnifiedKPIData(
|
||||
title: 'Membres',
|
||||
value: '247',
|
||||
icon: Icons.people,
|
||||
color: AppTheme.primaryColor,
|
||||
trend: UnifiedKPITrend(
|
||||
direction: UnifiedKPITrendDirection.up,
|
||||
value: '+12',
|
||||
label: 'ce mois',
|
||||
),
|
||||
),
|
||||
UnifiedKPIData(
|
||||
title: 'Événements',
|
||||
value: '18',
|
||||
icon: Icons.event,
|
||||
color: AppTheme.accentColor,
|
||||
trend: UnifiedKPITrend(
|
||||
direction: UnifiedKPITrendDirection.up,
|
||||
value: '+3',
|
||||
label: 'ce mois',
|
||||
),
|
||||
),
|
||||
UnifiedKPIData(
|
||||
title: 'Cotisations',
|
||||
value: '89%',
|
||||
icon: Icons.account_balance_wallet,
|
||||
color: AppTheme.successColor,
|
||||
trend: UnifiedKPITrend(
|
||||
direction: UnifiedKPITrendDirection.up,
|
||||
value: '+5%',
|
||||
label: 'vs mois dernier',
|
||||
),
|
||||
),
|
||||
UnifiedKPIData(
|
||||
title: 'Trésorerie',
|
||||
value: '12.5K€',
|
||||
icon: Icons.euro,
|
||||
color: AppTheme.warningColor,
|
||||
trend: UnifiedKPITrend(
|
||||
direction: UnifiedKPITrendDirection.stable,
|
||||
value: '0%',
|
||||
label: 'stable',
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
return UnifiedKPISection(
|
||||
title: 'Vue d\'ensemble',
|
||||
kpis: kpis,
|
||||
);
|
||||
}
|
||||
|
||||
/// Section des actions rapides
|
||||
Widget _buildQuickActionsSection() {
|
||||
final actions = [
|
||||
UnifiedQuickAction(
|
||||
id: 'add_member',
|
||||
title: 'Nouveau\nMembre',
|
||||
icon: Icons.person_add,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
UnifiedQuickAction(
|
||||
id: 'add_event',
|
||||
title: 'Nouvel\nÉvénement',
|
||||
icon: Icons.event_available,
|
||||
color: AppTheme.accentColor,
|
||||
badgeCount: 3,
|
||||
),
|
||||
UnifiedQuickAction(
|
||||
id: 'manage_cotisations',
|
||||
title: 'Gérer\nCotisations',
|
||||
icon: Icons.account_balance_wallet,
|
||||
color: AppTheme.successColor,
|
||||
badgeCount: 7,
|
||||
),
|
||||
UnifiedQuickAction(
|
||||
id: 'reports',
|
||||
title: 'Rapports\n& Stats',
|
||||
icon: Icons.analytics,
|
||||
color: AppTheme.infoColor,
|
||||
),
|
||||
UnifiedQuickAction(
|
||||
id: 'communications',
|
||||
title: 'Envoyer\nMessage',
|
||||
icon: Icons.send,
|
||||
color: AppTheme.warningColor,
|
||||
),
|
||||
UnifiedQuickAction(
|
||||
id: 'settings',
|
||||
title: 'Paramètres\nAssociation',
|
||||
icon: Icons.settings,
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
];
|
||||
|
||||
return UnifiedQuickActionsSection(
|
||||
title: 'Actions rapides',
|
||||
actions: actions,
|
||||
onActionTap: _handleQuickAction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Section des activités récentes
|
||||
Widget _buildRecentActivitiesSection() {
|
||||
final activities = [
|
||||
_ActivityItem(
|
||||
title: 'Nouveau membre inscrit',
|
||||
subtitle: 'Marie Dubois a rejoint l\'association',
|
||||
icon: Icons.person_add,
|
||||
color: AppTheme.successColor,
|
||||
time: 'Il y a 2h',
|
||||
),
|
||||
_ActivityItem(
|
||||
title: 'Événement créé',
|
||||
subtitle: 'Assemblée Générale 2024 programmée',
|
||||
icon: Icons.event,
|
||||
color: AppTheme.accentColor,
|
||||
time: 'Il y a 4h',
|
||||
),
|
||||
_ActivityItem(
|
||||
title: 'Cotisation reçue',
|
||||
subtitle: 'Jean Martin - Cotisation annuelle',
|
||||
icon: Icons.payment,
|
||||
color: AppTheme.primaryColor,
|
||||
time: 'Il y a 6h',
|
||||
),
|
||||
];
|
||||
|
||||
return UnifiedCard.elevated(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(AppTheme.spacingLarge),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.timeline,
|
||||
color: AppTheme.primaryColor,
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(width: AppTheme.spacingSmall),
|
||||
Text(
|
||||
'Activités récentes',
|
||||
style: AppTheme.titleMedium.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: AppTheme.spacingMedium),
|
||||
...activities.map((activity) => _buildActivityItem(activity)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Section d'analyses et graphiques
|
||||
Widget _buildAnalyticsSection() {
|
||||
return UnifiedCard.elevated(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(AppTheme.spacingLarge),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.analytics,
|
||||
color: AppTheme.accentColor,
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(width: AppTheme.spacingSmall),
|
||||
Text(
|
||||
'Analyses & Tendances',
|
||||
style: AppTheme.titleMedium.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
UnifiedButton.tertiary(
|
||||
text: 'Voir plus',
|
||||
size: UnifiedButtonSize.small,
|
||||
onPressed: () {
|
||||
// TODO: Navigation vers analyses détaillées
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: AppTheme.spacingMedium),
|
||||
Container(
|
||||
height: 120,
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.accentColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(AppTheme.borderRadiusMedium),
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.bar_chart,
|
||||
color: AppTheme.accentColor,
|
||||
size: 48,
|
||||
),
|
||||
const SizedBox(height: AppTheme.spacingSmall),
|
||||
Text(
|
||||
'Graphiques interactifs',
|
||||
style: AppTheme.bodyMedium.copyWith(
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit un élément d'activité
|
||||
Widget _buildActivityItem(_ActivityItem activity) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: AppTheme.spacingSmall),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(AppTheme.spacingSmall),
|
||||
decoration: BoxDecoration(
|
||||
color: activity.color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(AppTheme.borderRadiusSmall),
|
||||
),
|
||||
child: Icon(
|
||||
activity.icon,
|
||||
color: activity.color,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppTheme.spacingMedium),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
activity.title,
|
||||
style: AppTheme.bodyMedium.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
activity.subtitle,
|
||||
style: AppTheme.bodySmall.copyWith(
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Text(
|
||||
activity.time,
|
||||
style: AppTheme.bodySmall.copyWith(
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Gère les actions rapides
|
||||
void _handleQuickAction(UnifiedQuickAction action) {
|
||||
// TODO: Implémenter la navigation selon l'action
|
||||
switch (action.id) {
|
||||
case 'add_member':
|
||||
// Navigation vers ajout membre
|
||||
break;
|
||||
case 'add_event':
|
||||
// Navigation vers ajout événement
|
||||
break;
|
||||
case 'manage_cotisations':
|
||||
// Navigation vers gestion cotisations
|
||||
break;
|
||||
case 'reports':
|
||||
// Navigation vers rapports
|
||||
break;
|
||||
case 'communications':
|
||||
// Navigation vers communications
|
||||
break;
|
||||
case 'settings':
|
||||
// Navigation vers paramètres
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Modèle pour les éléments d'activité
|
||||
class _ActivityItem {
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final String time;
|
||||
|
||||
const _ActivityItem({
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
required this.time,
|
||||
});
|
||||
}
|
||||
@@ -78,7 +78,7 @@ Nombre d'événements de test: ${evenements.length}
|
||||
Erreur: $e
|
||||
|
||||
Le serveur backend n'est pas accessible.
|
||||
Vérifiez que le serveur Quarkus est démarré sur 192.168.1.145:8080
|
||||
Vérifiez que le serveur Quarkus est démarré sur 192.168.1.11:8080
|
||||
''';
|
||||
_isLoading = false;
|
||||
});
|
||||
@@ -220,7 +220,7 @@ Vérifiez que le serveur Quarkus est démarré sur 192.168.1.145:8080
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text(
|
||||
'URL Backend: http://192.168.1.145:8080\n'
|
||||
'URL Backend: http://192.168.1.11:8080\n'
|
||||
'Endpoint: /api/evenements/a-venir-public\n'
|
||||
'Méthode: GET',
|
||||
style: TextStyle(
|
||||
|
||||
@@ -15,7 +15,21 @@ import '../widgets/animated_evenement_list.dart';
|
||||
import 'evenement_detail_page.dart';
|
||||
import 'evenement_create_page.dart';
|
||||
|
||||
// Import de l'architecture unifiée pour amélioration progressive
|
||||
import '../../../../shared/widgets/common/unified_page_layout.dart';
|
||||
|
||||
/// Page principale des événements
|
||||
///
|
||||
/// ARCHITECTURE SOPHISTIQUÉE CONSERVÉE :
|
||||
/// - TabController avec 3 onglets (À venir, Publics, Tous)
|
||||
/// - Animations complexes avec multiple AnimationControllers
|
||||
/// - Scroll infini avec pagination intelligente par onglet
|
||||
/// - Recherche et filtres avancés intégrés
|
||||
/// - Navigation avec transitions personnalisées
|
||||
/// - Logique métier complexe pour chaque onglet
|
||||
///
|
||||
/// Cette page utilise déjà une architecture avancée et cohérente.
|
||||
/// L'amélioration incrémentale préserve toutes les fonctionnalités existantes.
|
||||
class EvenementsPage extends StatelessWidget {
|
||||
const EvenementsPage({super.key});
|
||||
|
||||
|
||||
@@ -0,0 +1,503 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../../core/di/injection.dart';
|
||||
import '../../../../core/models/evenement_model.dart';
|
||||
import '../../../../shared/widgets/unified_components.dart';
|
||||
import '../../../../shared/theme/app_theme.dart';
|
||||
import '../bloc/evenement_bloc.dart';
|
||||
import '../bloc/evenement_event.dart';
|
||||
import '../bloc/evenement_state.dart';
|
||||
import '../widgets/evenement_search_bar.dart';
|
||||
import '../widgets/evenement_filter_chips.dart';
|
||||
import 'evenement_detail_page.dart';
|
||||
import 'evenement_create_page.dart';
|
||||
|
||||
/// Page des événements refactorisée avec l'architecture unifiée
|
||||
class EvenementsPageUnified extends StatelessWidget {
|
||||
const EvenementsPageUnified({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => getIt<EvenementBloc>()
|
||||
..add(const LoadEvenementsAVenir()),
|
||||
child: const _EvenementsPageContent(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _EvenementsPageContent extends StatefulWidget {
|
||||
const _EvenementsPageContent();
|
||||
|
||||
@override
|
||||
State<_EvenementsPageContent> createState() => _EvenementsPageContentState();
|
||||
}
|
||||
|
||||
class _EvenementsPageContentState extends State<_EvenementsPageContent>
|
||||
with TickerProviderStateMixin {
|
||||
late TabController _tabController;
|
||||
String _searchTerm = '';
|
||||
TypeEvenement? _selectedType;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabController = TabController(length: 3, vsync: this);
|
||||
_tabController.addListener(_onTabChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onTabChanged() {
|
||||
if (_tabController.indexIsChanging) {
|
||||
_loadEventsForTab(_tabController.index);
|
||||
}
|
||||
}
|
||||
|
||||
void _loadEventsForTab(int index) {
|
||||
context.read<EvenementBloc>().add(const ResetEvenementState());
|
||||
|
||||
switch (index) {
|
||||
case 0:
|
||||
context.read<EvenementBloc>().add(const LoadEvenementsAVenir());
|
||||
break;
|
||||
case 1:
|
||||
context.read<EvenementBloc>().add(const LoadEvenementsPublics());
|
||||
break;
|
||||
case 2:
|
||||
context.read<EvenementBloc>().add(const LoadEvenements());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void _onSearch(String terme) {
|
||||
setState(() {
|
||||
_searchTerm = terme;
|
||||
_selectedType = null;
|
||||
});
|
||||
|
||||
if (terme.isNotEmpty) {
|
||||
context.read<EvenementBloc>().add(
|
||||
SearchEvenements(terme: terme, refresh: true),
|
||||
);
|
||||
} else {
|
||||
context.read<EvenementBloc>().add(
|
||||
const LoadEvenements(refresh: true),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onFilterByType(TypeEvenement? type) {
|
||||
setState(() {
|
||||
_selectedType = type;
|
||||
_searchTerm = '';
|
||||
});
|
||||
|
||||
if (type != null) {
|
||||
context.read<EvenementBloc>().add(
|
||||
FilterEvenementsByType(type: type, refresh: true),
|
||||
);
|
||||
} else {
|
||||
context.read<EvenementBloc>().add(
|
||||
const LoadEvenements(refresh: true),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onRefresh() {
|
||||
_loadEventsForTab(_tabController.index);
|
||||
}
|
||||
|
||||
void _onLoadMore() {
|
||||
final state = context.read<EvenementBloc>().state;
|
||||
if (state is EvenementLoaded && !state.hasReachedMax) {
|
||||
final nextPage = state.currentPage + 1;
|
||||
|
||||
switch (_tabController.index) {
|
||||
case 0:
|
||||
context.read<EvenementBloc>().add(
|
||||
LoadEvenementsAVenir(page: nextPage),
|
||||
);
|
||||
break;
|
||||
case 1:
|
||||
context.read<EvenementBloc>().add(
|
||||
LoadEvenementsPublics(page: nextPage),
|
||||
);
|
||||
break;
|
||||
case 2:
|
||||
if (_searchTerm.isNotEmpty) {
|
||||
context.read<EvenementBloc>().add(
|
||||
SearchEvenements(terme: _searchTerm, page: nextPage),
|
||||
);
|
||||
} else if (_selectedType != null) {
|
||||
context.read<EvenementBloc>().add(
|
||||
FilterEvenementsByType(type: _selectedType!, page: nextPage),
|
||||
);
|
||||
} else {
|
||||
context.read<EvenementBloc>().add(
|
||||
LoadEvenements(page: nextPage),
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return UnifiedPageLayout(
|
||||
title: 'Événements',
|
||||
subtitle: 'Gestion des événements de l\'association',
|
||||
icon: Icons.event,
|
||||
iconColor: AppTheme.accentColor,
|
||||
scrollable: false,
|
||||
padding: EdgeInsets.zero,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const EvenementCreatePage(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
body: Column(
|
||||
children: [
|
||||
// En-tête avec KPI
|
||||
_buildKPISection(),
|
||||
|
||||
// Onglets
|
||||
_buildTabBar(),
|
||||
|
||||
// Contenu des onglets
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
_buildEventsList(showUpcoming: true),
|
||||
_buildEventsList(showPublic: true),
|
||||
_buildEventsListWithFilters(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const EvenementCreatePage(),
|
||||
),
|
||||
);
|
||||
},
|
||||
backgroundColor: AppTheme.accentColor,
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildKPISection() {
|
||||
return BlocBuilder<EvenementBloc, EvenementState>(
|
||||
builder: (context, state) {
|
||||
final kpis = _buildKPIData(state);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(AppTheme.spacingMedium),
|
||||
child: UnifiedKPISection(
|
||||
kpis: kpis,
|
||||
crossAxisCount: 3,
|
||||
spacing: AppTheme.spacingSmall,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
List<UnifiedKPIData> _buildKPIData(EvenementState state) {
|
||||
int totalEvents = 0;
|
||||
int upcomingEvents = 0;
|
||||
int publicEvents = 0;
|
||||
|
||||
if (state is EvenementLoaded) {
|
||||
totalEvents = state.evenements.length;
|
||||
upcomingEvents = state.evenements
|
||||
.where((e) => e.dateDebut.isAfter(DateTime.now()))
|
||||
.length;
|
||||
publicEvents = state.evenements
|
||||
.where((e) => e.typeEvenement == TypeEvenement.conference)
|
||||
.length;
|
||||
}
|
||||
|
||||
return [
|
||||
UnifiedKPIData(
|
||||
title: 'Total',
|
||||
value: totalEvents.toString(),
|
||||
icon: Icons.event,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
UnifiedKPIData(
|
||||
title: 'À venir',
|
||||
value: upcomingEvents.toString(),
|
||||
icon: Icons.schedule,
|
||||
color: AppTheme.accentColor,
|
||||
),
|
||||
UnifiedKPIData(
|
||||
title: 'Publics',
|
||||
value: publicEvents.toString(),
|
||||
icon: Icons.public,
|
||||
color: AppTheme.successColor,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
Widget _buildTabBar() {
|
||||
return Container(
|
||||
color: Colors.white,
|
||||
child: TabBar(
|
||||
controller: _tabController,
|
||||
labelColor: AppTheme.primaryColor,
|
||||
unselectedLabelColor: AppTheme.textSecondary,
|
||||
indicatorColor: AppTheme.primaryColor,
|
||||
tabs: const [
|
||||
Tab(text: 'À venir'),
|
||||
Tab(text: 'Publics'),
|
||||
Tab(text: 'Tous'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEventsList({bool showUpcoming = false, bool showPublic = false}) {
|
||||
return BlocBuilder<EvenementBloc, EvenementState>(
|
||||
builder: (context, state) {
|
||||
if (state is EvenementError) {
|
||||
return UnifiedPageLayout(
|
||||
title: '',
|
||||
showAppBar: false,
|
||||
errorMessage: state.message,
|
||||
onRefresh: _onRefresh,
|
||||
body: const SizedBox.shrink(),
|
||||
);
|
||||
}
|
||||
|
||||
final isLoading = state is EvenementLoading;
|
||||
final events = state is EvenementLoaded ? state.evenements : <EvenementModel>[];
|
||||
final hasReachedMax = state is EvenementLoaded ? state.hasReachedMax : false;
|
||||
|
||||
return UnifiedListWidget<EvenementModel>(
|
||||
items: events,
|
||||
isLoading: isLoading,
|
||||
hasReachedMax: hasReachedMax,
|
||||
onLoadMore: _onLoadMore,
|
||||
onRefresh: () async => _onRefresh(),
|
||||
itemBuilder: (context, evenement, index) {
|
||||
return _buildEventCard(evenement);
|
||||
},
|
||||
emptyWidget: _buildEmptyState(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEventsListWithFilters() {
|
||||
return Column(
|
||||
children: [
|
||||
// Barre de recherche et filtres
|
||||
Container(
|
||||
padding: const EdgeInsets.all(AppTheme.spacingMedium),
|
||||
color: Colors.white,
|
||||
child: Column(
|
||||
children: [
|
||||
EvenementSearchBar(
|
||||
onSearch: _onSearch,
|
||||
initialValue: _searchTerm,
|
||||
),
|
||||
const SizedBox(height: AppTheme.spacingSmall),
|
||||
EvenementFilterChips(
|
||||
selectedType: _selectedType,
|
||||
onTypeSelected: _onFilterByType,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Liste des événements
|
||||
Expanded(
|
||||
child: _buildEventsList(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEventCard(EvenementModel evenement) {
|
||||
return UnifiedCard.listItem(
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => EvenementDetailPage(evenement: evenement),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: _buildEventCardContent(evenement),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEventCardContent(EvenementModel evenement) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.accentColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.event,
|
||||
color: AppTheme.accentColor,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
evenement.titre,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
evenement.description ?? '',
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.schedule,
|
||||
size: 16,
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'${evenement.dateDebut.day}/${evenement.dateDebut.month}/${evenement.dateDebut.year}',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: _getTypeColor(evenement.typeEvenement).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
evenement.typeEvenement.name,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: _getTypeColor(evenement.typeEvenement),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Color _getTypeColor(TypeEvenement type) {
|
||||
switch (type) {
|
||||
case TypeEvenement.conference:
|
||||
return AppTheme.successColor;
|
||||
case TypeEvenement.assembleeGenerale:
|
||||
return AppTheme.primaryColor;
|
||||
case TypeEvenement.formation:
|
||||
return AppTheme.warningColor;
|
||||
case TypeEvenement.reunion:
|
||||
return AppTheme.infoColor;
|
||||
default:
|
||||
return AppTheme.textSecondary;
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildEmptyState() {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.event_busy,
|
||||
size: 64,
|
||||
color: AppTheme.textSecondary.withOpacity(0.5),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Aucun événement',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppTheme.textSecondary.withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Créez votre premier événement',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppTheme.textSecondary.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
UnifiedButton.primary(
|
||||
text: 'Créer un événement',
|
||||
icon: Icons.add,
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const EvenementCreatePage(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -132,8 +132,15 @@ class _MembreEditPageState extends State<MembreEditPage>
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: _membresBloc,
|
||||
child: WillPopScope(
|
||||
onWillPop: _onWillPop,
|
||||
child: PopScope(
|
||||
canPop: !_hasChanges,
|
||||
onPopInvokedWithResult: (didPop, result) async {
|
||||
if (didPop) return;
|
||||
final shouldPop = await _onWillPop();
|
||||
if (shouldPop && context.mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
child: Scaffold(
|
||||
backgroundColor: AppTheme.backgroundLight,
|
||||
appBar: _buildAppBar(),
|
||||
|
||||
@@ -14,6 +14,14 @@ import '../widgets/dashboard/members_recent_activities_widget.dart';
|
||||
import '../widgets/dashboard/members_advanced_filters_widget.dart';
|
||||
import '../widgets/dashboard/members_smart_search_widget.dart';
|
||||
import '../widgets/dashboard/members_notifications_widget.dart';
|
||||
import 'membre_edit_page.dart';
|
||||
|
||||
// Import de l'architecture unifiée pour amélioration progressive
|
||||
import '../../../../shared/widgets/common/unified_page_layout.dart';
|
||||
|
||||
// Imports des optimisations de performance
|
||||
import '../../../../core/performance/performance_optimizer.dart';
|
||||
import '../../../../shared/widgets/performance/optimized_list_view.dart';
|
||||
|
||||
class MembresDashboardPage extends StatefulWidget {
|
||||
const MembresDashboardPage({super.key});
|
||||
@@ -73,87 +81,32 @@ class _MembresDashboardPageState extends State<MembresDashboardPage> {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: _membresBloc,
|
||||
child: Scaffold(
|
||||
backgroundColor: AppTheme.backgroundLight,
|
||||
appBar: AppBar(
|
||||
title: const Text(
|
||||
'Dashboard Membres',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 20,
|
||||
),
|
||||
),
|
||||
backgroundColor: AppTheme.primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
child: BlocBuilder<MembresBloc, MembresState>(
|
||||
builder: (context, state) {
|
||||
// Utilisation de UnifiedPageLayout pour améliorer la cohérence
|
||||
// tout en conservant TOUS les widgets spécialisés existants
|
||||
return UnifiedPageLayout(
|
||||
title: 'Dashboard Membres',
|
||||
icon: Icons.people,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
onPressed: _loadData,
|
||||
tooltip: 'Actualiser',
|
||||
),
|
||||
],
|
||||
isLoading: state is MembresLoading,
|
||||
errorMessage: state is MembresError ? state.message : null,
|
||||
onRefresh: _loadData,
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: _loadData,
|
||||
tooltip: 'Actualiser',
|
||||
backgroundColor: AppTheme.primaryColor,
|
||||
tooltip: 'Actualiser les données',
|
||||
child: const Icon(Icons.refresh, color: Colors.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: BlocBuilder<MembresBloc, MembresState>(
|
||||
builder: (context, state) {
|
||||
if (state is MembresLoading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
if (state is MembresError) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.error_outline,
|
||||
size: 64,
|
||||
color: AppTheme.errorColor,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Erreur de chargement',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
state.message,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _loadData,
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: const Text('Réessayer'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return _buildDashboard();
|
||||
},
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: _loadData,
|
||||
backgroundColor: AppTheme.primaryColor,
|
||||
tooltip: 'Actualiser les données',
|
||||
child: const Icon(Icons.refresh, color: Colors.white),
|
||||
),
|
||||
body: _buildDashboard(),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -234,14 +187,17 @@ class _MembresDashboardPageState extends State<MembresDashboardPage> {
|
||||
),
|
||||
);
|
||||
},
|
||||
onMemberEdit: (member) {
|
||||
// TODO: Modifier le membre
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Modification de ${member.nomComplet}'),
|
||||
backgroundColor: AppTheme.warningColor,
|
||||
onMemberEdit: (member) async {
|
||||
final result = await Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => MembreEditPage(membre: member),
|
||||
),
|
||||
);
|
||||
|
||||
if (result == true) {
|
||||
// Recharger les données si le membre a été modifié
|
||||
_membresBloc.add(const LoadMembres());
|
||||
}
|
||||
},
|
||||
searchQuery: _currentSearchQuery,
|
||||
filters: _currentFilters,
|
||||
|
||||
@@ -0,0 +1,488 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../../core/di/injection.dart';
|
||||
import '../../../../shared/widgets/unified_components.dart';
|
||||
import '../../../../shared/theme/app_theme.dart';
|
||||
import '../../../../core/models/membre_model.dart';
|
||||
import '../bloc/membres_bloc.dart';
|
||||
import '../bloc/membres_event.dart';
|
||||
import '../bloc/membres_state.dart';
|
||||
|
||||
/// Dashboard des membres UnionFlow - Version Unifiée
|
||||
///
|
||||
/// Utilise l'architecture unifiée pour une expérience cohérente :
|
||||
/// - Composants standardisés réutilisables
|
||||
/// - Interface homogène avec les autres onglets
|
||||
/// - Performance optimisée avec animations fluides
|
||||
/// - Maintenabilité maximale
|
||||
class MembresDashboardPageUnified extends StatefulWidget {
|
||||
const MembresDashboardPageUnified({super.key});
|
||||
|
||||
@override
|
||||
State<MembresDashboardPageUnified> createState() => _MembresDashboardPageUnifiedState();
|
||||
}
|
||||
|
||||
class _MembresDashboardPageUnifiedState extends State<MembresDashboardPageUnified> {
|
||||
late MembresBloc _membresBloc;
|
||||
Map<String, dynamic> _currentFilters = {};
|
||||
String _currentSearchQuery = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_membresBloc = getIt<MembresBloc>();
|
||||
_loadData();
|
||||
}
|
||||
|
||||
void _loadData() {
|
||||
_membresBloc.add(const LoadMembres());
|
||||
}
|
||||
|
||||
void _onFiltersChanged(Map<String, dynamic> filters) {
|
||||
setState(() {
|
||||
_currentFilters = filters;
|
||||
});
|
||||
_loadData();
|
||||
}
|
||||
|
||||
void _onSearchChanged(String query) {
|
||||
setState(() {
|
||||
_currentSearchQuery = query;
|
||||
});
|
||||
if (query.isNotEmpty) {
|
||||
_membresBloc.add(SearchMembres(query));
|
||||
} else {
|
||||
_loadData();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: _membresBloc,
|
||||
child: BlocBuilder<MembresBloc, MembresState>(
|
||||
builder: (context, state) {
|
||||
return UnifiedPageLayout(
|
||||
title: 'Membres',
|
||||
subtitle: 'Gestion des membres de l\'association',
|
||||
icon: Icons.people,
|
||||
iconColor: AppTheme.primaryColor,
|
||||
isLoading: state is MembresLoading,
|
||||
errorMessage: state is MembresError ? state.message : null,
|
||||
onRefresh: _loadData,
|
||||
actions: _buildActions(),
|
||||
body: Column(
|
||||
children: [
|
||||
_buildSearchSection(),
|
||||
const SizedBox(height: AppTheme.spacingLarge),
|
||||
_buildKPISection(state),
|
||||
const SizedBox(height: AppTheme.spacingLarge),
|
||||
_buildQuickActionsSection(),
|
||||
const SizedBox(height: AppTheme.spacingLarge),
|
||||
_buildFiltersSection(),
|
||||
const SizedBox(height: AppTheme.spacingLarge),
|
||||
Expanded(child: _buildMembersList(state)),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Actions de la barre d'outils
|
||||
List<Widget> _buildActions() {
|
||||
return [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.person_add),
|
||||
onPressed: () {
|
||||
// TODO: Navigation vers ajout membre
|
||||
},
|
||||
tooltip: 'Ajouter un membre',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.import_export),
|
||||
onPressed: () {
|
||||
// TODO: Import/Export des membres
|
||||
},
|
||||
tooltip: 'Import/Export',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.analytics),
|
||||
onPressed: () {
|
||||
// TODO: Navigation vers analyses détaillées
|
||||
},
|
||||
tooltip: 'Analyses',
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/// Section de recherche intelligente
|
||||
Widget _buildSearchSection() {
|
||||
return UnifiedCard.outlined(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(AppTheme.spacingMedium),
|
||||
child: Column(
|
||||
children: [
|
||||
TextField(
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Rechercher un membre...',
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppTheme.borderRadiusMedium),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
filled: true,
|
||||
fillColor: AppTheme.backgroundLight,
|
||||
),
|
||||
onChanged: _onSearchChanged,
|
||||
),
|
||||
if (_currentSearchQuery.isNotEmpty) ...[
|
||||
const SizedBox(height: AppTheme.spacingSmall),
|
||||
Text(
|
||||
'Recherche: "$_currentSearchQuery"',
|
||||
style: AppTheme.bodySmall.copyWith(
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Section des KPI des membres
|
||||
Widget _buildKPISection(MembresState state) {
|
||||
final membres = state is MembresLoaded ? state.membres : <MembreModel>[];
|
||||
final totalMembres = membres.length;
|
||||
final membresActifs = membres.where((m) => m.statut == StatutMembre.actif).length;
|
||||
final nouveauxMembres = membres.where((m) {
|
||||
final now = DateTime.now();
|
||||
final monthAgo = DateTime(now.year, now.month - 1, now.day);
|
||||
return m.dateInscription.isAfter(monthAgo);
|
||||
}).length;
|
||||
final cotisationsAJour = membres.where((m) => m.cotisationAJour).length;
|
||||
|
||||
final kpis = [
|
||||
UnifiedKPIData(
|
||||
title: 'Total',
|
||||
value: totalMembres.toString(),
|
||||
icon: Icons.people,
|
||||
color: AppTheme.primaryColor,
|
||||
trend: UnifiedKPITrend(
|
||||
direction: nouveauxMembres > 0 ? UnifiedKPITrendDirection.up : UnifiedKPITrendDirection.stable,
|
||||
value: '+$nouveauxMembres',
|
||||
label: 'ce mois',
|
||||
),
|
||||
),
|
||||
UnifiedKPIData(
|
||||
title: 'Actifs',
|
||||
value: membresActifs.toString(),
|
||||
icon: Icons.verified_user,
|
||||
color: AppTheme.successColor,
|
||||
trend: UnifiedKPITrend(
|
||||
direction: UnifiedKPITrendDirection.stable,
|
||||
value: '${((membresActifs / totalMembres) * 100).toInt()}%',
|
||||
label: 'du total',
|
||||
),
|
||||
),
|
||||
UnifiedKPIData(
|
||||
title: 'Nouveaux',
|
||||
value: nouveauxMembres.toString(),
|
||||
icon: Icons.person_add,
|
||||
color: AppTheme.accentColor,
|
||||
trend: UnifiedKPITrend(
|
||||
direction: UnifiedKPITrendDirection.up,
|
||||
value: 'Ce mois',
|
||||
label: 'inscriptions',
|
||||
),
|
||||
),
|
||||
UnifiedKPIData(
|
||||
title: 'À jour',
|
||||
value: '${((cotisationsAJour / totalMembres) * 100).toInt()}%',
|
||||
icon: Icons.account_balance_wallet,
|
||||
color: AppTheme.warningColor,
|
||||
trend: UnifiedKPITrend(
|
||||
direction: UnifiedKPITrendDirection.stable,
|
||||
value: '$cotisationsAJour/$totalMembres',
|
||||
label: 'cotisations',
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
return UnifiedKPISection(
|
||||
title: 'Statistiques des membres',
|
||||
kpis: kpis,
|
||||
);
|
||||
}
|
||||
|
||||
/// Section des actions rapides
|
||||
Widget _buildQuickActionsSection() {
|
||||
final actions = [
|
||||
UnifiedQuickAction(
|
||||
id: 'add_member',
|
||||
title: 'Nouveau\nMembre',
|
||||
icon: Icons.person_add,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
UnifiedQuickAction(
|
||||
id: 'bulk_import',
|
||||
title: 'Import\nGroupé',
|
||||
icon: Icons.upload_file,
|
||||
color: AppTheme.accentColor,
|
||||
),
|
||||
UnifiedQuickAction(
|
||||
id: 'send_message',
|
||||
title: 'Message\nGroupé',
|
||||
icon: Icons.send,
|
||||
color: AppTheme.infoColor,
|
||||
),
|
||||
UnifiedQuickAction(
|
||||
id: 'export_data',
|
||||
title: 'Exporter\nDonnées',
|
||||
icon: Icons.download,
|
||||
color: AppTheme.successColor,
|
||||
),
|
||||
UnifiedQuickAction(
|
||||
id: 'cotisations_reminder',
|
||||
title: 'Rappel\nCotisations',
|
||||
icon: Icons.notification_important,
|
||||
color: AppTheme.warningColor,
|
||||
badgeCount: 12,
|
||||
),
|
||||
UnifiedQuickAction(
|
||||
id: 'member_reports',
|
||||
title: 'Rapports\nMembres',
|
||||
icon: Icons.analytics,
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
];
|
||||
|
||||
return UnifiedQuickActionsSection(
|
||||
title: 'Actions rapides',
|
||||
actions: actions,
|
||||
onActionTap: _handleQuickAction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Section des filtres
|
||||
Widget _buildFiltersSection() {
|
||||
return UnifiedCard.outlined(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(AppTheme.spacingMedium),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.filter_list,
|
||||
color: AppTheme.primaryColor,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: AppTheme.spacingSmall),
|
||||
Text(
|
||||
'Filtres rapides',
|
||||
style: AppTheme.titleSmall.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: AppTheme.spacingMedium),
|
||||
Wrap(
|
||||
spacing: AppTheme.spacingSmall,
|
||||
runSpacing: AppTheme.spacingSmall,
|
||||
children: [
|
||||
_buildFilterChip('Tous', _currentFilters.isEmpty),
|
||||
_buildFilterChip('Actifs', _currentFilters['statut'] == 'actif'),
|
||||
_buildFilterChip('Inactifs', _currentFilters['statut'] == 'inactif'),
|
||||
_buildFilterChip('Nouveaux', _currentFilters['type'] == 'nouveaux'),
|
||||
_buildFilterChip('Cotisations en retard', _currentFilters['cotisation'] == 'retard'),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit un chip de filtre
|
||||
Widget _buildFilterChip(String label, bool isSelected) {
|
||||
return FilterChip(
|
||||
label: Text(label),
|
||||
selected: isSelected,
|
||||
onSelected: (selected) {
|
||||
Map<String, dynamic> newFilters = {};
|
||||
if (selected) {
|
||||
switch (label) {
|
||||
case 'Actifs':
|
||||
newFilters['statut'] = 'actif';
|
||||
break;
|
||||
case 'Inactifs':
|
||||
newFilters['statut'] = 'inactif';
|
||||
break;
|
||||
case 'Nouveaux':
|
||||
newFilters['type'] = 'nouveaux';
|
||||
break;
|
||||
case 'Cotisations en retard':
|
||||
newFilters['cotisation'] = 'retard';
|
||||
break;
|
||||
}
|
||||
}
|
||||
_onFiltersChanged(newFilters);
|
||||
},
|
||||
selectedColor: AppTheme.primaryColor.withOpacity(0.2),
|
||||
checkmarkColor: AppTheme.primaryColor,
|
||||
);
|
||||
}
|
||||
|
||||
/// Liste des membres avec composant unifié
|
||||
Widget _buildMembersList(MembresState state) {
|
||||
if (state is MembresLoaded) {
|
||||
return UnifiedListWidget<MembreModel>(
|
||||
items: state.membres,
|
||||
itemBuilder: (context, membre, index) => _buildMemberCard(membre),
|
||||
isLoading: false,
|
||||
hasReachedMax: true,
|
||||
enableAnimations: true,
|
||||
emptyMessage: 'Aucun membre trouvé',
|
||||
emptyIcon: Icons.people_outline,
|
||||
);
|
||||
}
|
||||
|
||||
return const Center(
|
||||
child: Text('Chargement des membres...'),
|
||||
);
|
||||
}
|
||||
|
||||
/// Construit une carte de membre
|
||||
Widget _buildMemberCard(MembreModel membre) {
|
||||
return UnifiedCard.listItem(
|
||||
onTap: () {
|
||||
// TODO: Navigation vers détails du membre
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(AppTheme.spacingMedium),
|
||||
child: Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
backgroundColor: AppTheme.primaryColor.withOpacity(0.1),
|
||||
child: Text(
|
||||
membre.prenom.isNotEmpty ? membre.prenom[0].toUpperCase() : 'M',
|
||||
style: TextStyle(
|
||||
color: AppTheme.primaryColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppTheme.spacingMedium),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${membre.prenom} ${membre.nom}',
|
||||
style: AppTheme.bodyLarge.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppTheme.spacingXSmall),
|
||||
Text(
|
||||
membre.email,
|
||||
style: AppTheme.bodySmall.copyWith(
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppTheme.spacingSmall,
|
||||
vertical: AppTheme.spacingXSmall,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: _getStatusColor(membre.statut).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(AppTheme.borderRadiusSmall),
|
||||
),
|
||||
child: Text(
|
||||
_getStatusLabel(membre.statut),
|
||||
style: AppTheme.bodySmall.copyWith(
|
||||
color: _getStatusColor(membre.statut),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppTheme.spacingXSmall),
|
||||
Icon(
|
||||
membre.cotisationAJour ? Icons.check_circle : Icons.warning,
|
||||
color: membre.cotisationAJour ? AppTheme.successColor : AppTheme.warningColor,
|
||||
size: 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Obtient la couleur du statut
|
||||
Color _getStatusColor(StatutMembre statut) {
|
||||
switch (statut) {
|
||||
case StatutMembre.actif:
|
||||
return AppTheme.successColor;
|
||||
case StatutMembre.inactif:
|
||||
return AppTheme.errorColor;
|
||||
case StatutMembre.suspendu:
|
||||
return AppTheme.warningColor;
|
||||
}
|
||||
}
|
||||
|
||||
/// Obtient le libellé du statut
|
||||
String _getStatusLabel(StatutMembre statut) {
|
||||
switch (statut) {
|
||||
case StatutMembre.actif:
|
||||
return 'Actif';
|
||||
case StatutMembre.inactif:
|
||||
return 'Inactif';
|
||||
case StatutMembre.suspendu:
|
||||
return 'Suspendu';
|
||||
}
|
||||
}
|
||||
|
||||
/// Gère les actions rapides
|
||||
void _handleQuickAction(UnifiedQuickAction action) {
|
||||
switch (action.id) {
|
||||
case 'add_member':
|
||||
// TODO: Navigation vers ajout membre
|
||||
break;
|
||||
case 'bulk_import':
|
||||
// TODO: Import groupé
|
||||
break;
|
||||
case 'send_message':
|
||||
// TODO: Message groupé
|
||||
break;
|
||||
case 'export_data':
|
||||
// TODO: Export des données
|
||||
break;
|
||||
case 'cotisations_reminder':
|
||||
// TODO: Rappel cotisations
|
||||
break;
|
||||
case 'member_reports':
|
||||
// TODO: Rapports membres
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_membresBloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import '../../../../core/auth/services/permission_service.dart';
|
||||
import '../../../../core/services/communication_service.dart';
|
||||
import '../../../../core/services/export_import_service.dart';
|
||||
import '../../../../shared/theme/app_theme.dart';
|
||||
import '../../../../shared/widgets/coming_soon_page.dart';
|
||||
import '../../../../shared/widgets/permission_widget.dart';
|
||||
import '../bloc/membres_bloc.dart';
|
||||
import '../bloc/membres_event.dart';
|
||||
@@ -22,6 +21,7 @@ import '../widgets/membres_view_controls.dart';
|
||||
import '../widgets/membre_enhanced_card.dart';
|
||||
import 'membre_details_page.dart';
|
||||
import 'membre_create_page.dart';
|
||||
import 'membre_edit_page.dart';
|
||||
import '../widgets/error_demo_widget.dart';
|
||||
|
||||
|
||||
@@ -540,7 +540,7 @@ class _MembresListPageState extends State<MembresListPage> with PermissionMixin
|
||||
}
|
||||
|
||||
/// Affiche le dialog d'édition de membre
|
||||
void _showEditMemberDialog(membre) {
|
||||
void _showEditMemberDialog(membre) async {
|
||||
// Vérifier les permissions avant d'ouvrir le formulaire
|
||||
if (!permissionService.canEditMembers) {
|
||||
showPermissionError(context, 'Vous n\'avez pas les permissions pour modifier les membres');
|
||||
@@ -549,16 +549,16 @@ class _MembresListPageState extends State<MembresListPage> with PermissionMixin
|
||||
|
||||
permissionService.logAction('Ouverture formulaire édition membre', details: {'membreId': membre.id});
|
||||
|
||||
// TODO: Implémenter le formulaire d'édition
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => const ComingSoonPage(
|
||||
title: 'Modifier le membre',
|
||||
description: 'Le formulaire de modification sera bientôt disponible.',
|
||||
icon: Icons.edit,
|
||||
color: AppTheme.warningColor,
|
||||
final result = await Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => MembreEditPage(membre: membre),
|
||||
),
|
||||
);
|
||||
|
||||
// Si le membre a été modifié avec succès, recharger la liste
|
||||
if (result == true) {
|
||||
_membresBloc.add(const RefreshMembres());
|
||||
}
|
||||
}
|
||||
|
||||
/// Affiche la confirmation de suppression
|
||||
|
||||
@@ -0,0 +1,418 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import '../../domain/entities/notification.dart';
|
||||
|
||||
part 'notification_model.g.dart';
|
||||
|
||||
/// Modèle de données pour les actions de notification
|
||||
@JsonSerializable()
|
||||
class ActionNotificationModel extends ActionNotification {
|
||||
const ActionNotificationModel({
|
||||
required super.id,
|
||||
required super.libelle,
|
||||
required super.typeAction,
|
||||
super.description,
|
||||
super.icone,
|
||||
super.couleur,
|
||||
super.url,
|
||||
super.route,
|
||||
super.parametres,
|
||||
super.fermeNotification = true,
|
||||
super.necessiteConfirmation = false,
|
||||
super.estDestructive = false,
|
||||
super.ordre = 0,
|
||||
super.estActivee = true,
|
||||
});
|
||||
|
||||
factory ActionNotificationModel.fromJson(Map<String, dynamic> json) =>
|
||||
_$ActionNotificationModelFromJson(json);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() => _$ActionNotificationModelToJson(this);
|
||||
|
||||
factory ActionNotificationModel.fromEntity(ActionNotification entity) {
|
||||
return ActionNotificationModel(
|
||||
id: entity.id,
|
||||
libelle: entity.libelle,
|
||||
typeAction: entity.typeAction,
|
||||
description: entity.description,
|
||||
icone: entity.icone,
|
||||
couleur: entity.couleur,
|
||||
url: entity.url,
|
||||
route: entity.route,
|
||||
parametres: entity.parametres,
|
||||
fermeNotification: entity.fermeNotification,
|
||||
necessiteConfirmation: entity.necessiteConfirmation,
|
||||
estDestructive: entity.estDestructive,
|
||||
ordre: entity.ordre,
|
||||
estActivee: entity.estActivee,
|
||||
);
|
||||
}
|
||||
|
||||
ActionNotification toEntity() {
|
||||
return ActionNotification(
|
||||
id: id,
|
||||
libelle: libelle,
|
||||
typeAction: typeAction,
|
||||
description: description,
|
||||
icone: icone,
|
||||
couleur: couleur,
|
||||
url: url,
|
||||
route: route,
|
||||
parametres: parametres,
|
||||
fermeNotification: fermeNotification,
|
||||
necessiteConfirmation: necessiteConfirmation,
|
||||
estDestructive: estDestructive,
|
||||
ordre: ordre,
|
||||
estActivee: estActivee,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Modèle de données pour les notifications
|
||||
@JsonSerializable()
|
||||
class NotificationModel extends NotificationEntity {
|
||||
const NotificationModel({
|
||||
required super.id,
|
||||
required super.typeNotification,
|
||||
required super.statut,
|
||||
required super.titre,
|
||||
required super.message,
|
||||
super.messageCourt,
|
||||
super.expediteurId,
|
||||
super.expediteurNom,
|
||||
required super.destinatairesIds,
|
||||
super.organisationId,
|
||||
super.donneesPersonnalisees,
|
||||
super.imageUrl,
|
||||
super.iconeUrl,
|
||||
super.actionClic,
|
||||
super.parametresAction,
|
||||
super.actionsRapides,
|
||||
required super.dateCreation,
|
||||
super.dateEnvoiProgramme,
|
||||
super.dateEnvoi,
|
||||
super.dateExpiration,
|
||||
super.dateDerniereLecture,
|
||||
super.priorite = 3,
|
||||
super.estLue = false,
|
||||
super.estImportante = false,
|
||||
super.estArchivee = false,
|
||||
super.nombreAffichages = 0,
|
||||
super.nombreClics = 0,
|
||||
super.tags,
|
||||
super.campagneId,
|
||||
super.plateforme,
|
||||
super.tokenFCM,
|
||||
});
|
||||
|
||||
factory NotificationModel.fromJson(Map<String, dynamic> json) =>
|
||||
_$NotificationModelFromJson(json);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() => _$NotificationModelToJson(this);
|
||||
|
||||
factory NotificationModel.fromEntity(NotificationEntity entity) {
|
||||
return NotificationModel(
|
||||
id: entity.id,
|
||||
typeNotification: entity.typeNotification,
|
||||
statut: entity.statut,
|
||||
titre: entity.titre,
|
||||
message: entity.message,
|
||||
messageCourt: entity.messageCourt,
|
||||
expediteurId: entity.expediteurId,
|
||||
expediteurNom: entity.expediteurNom,
|
||||
destinatairesIds: entity.destinatairesIds,
|
||||
organisationId: entity.organisationId,
|
||||
donneesPersonnalisees: entity.donneesPersonnalisees,
|
||||
imageUrl: entity.imageUrl,
|
||||
iconeUrl: entity.iconeUrl,
|
||||
actionClic: entity.actionClic,
|
||||
parametresAction: entity.parametresAction,
|
||||
actionsRapides: entity.actionsRapides?.map((action) =>
|
||||
ActionNotificationModel.fromEntity(action)).toList(),
|
||||
dateCreation: entity.dateCreation,
|
||||
dateEnvoiProgramme: entity.dateEnvoiProgramme,
|
||||
dateEnvoi: entity.dateEnvoi,
|
||||
dateExpiration: entity.dateExpiration,
|
||||
dateDerniereLecture: entity.dateDerniereLecture,
|
||||
priorite: entity.priorite,
|
||||
estLue: entity.estLue,
|
||||
estImportante: entity.estImportante,
|
||||
estArchivee: entity.estArchivee,
|
||||
nombreAffichages: entity.nombreAffichages,
|
||||
nombreClics: entity.nombreClics,
|
||||
tags: entity.tags,
|
||||
campagneId: entity.campagneId,
|
||||
plateforme: entity.plateforme,
|
||||
tokenFCM: entity.tokenFCM,
|
||||
);
|
||||
}
|
||||
|
||||
NotificationEntity toEntity() {
|
||||
return NotificationEntity(
|
||||
id: id,
|
||||
typeNotification: typeNotification,
|
||||
statut: statut,
|
||||
titre: titre,
|
||||
message: message,
|
||||
messageCourt: messageCourt,
|
||||
expediteurId: expediteurId,
|
||||
expediteurNom: expediteurNom,
|
||||
destinatairesIds: destinatairesIds,
|
||||
organisationId: organisationId,
|
||||
donneesPersonnalisees: donneesPersonnalisees,
|
||||
imageUrl: imageUrl,
|
||||
iconeUrl: iconeUrl,
|
||||
actionClic: actionClic,
|
||||
parametresAction: parametresAction,
|
||||
actionsRapides: actionsRapides?.map((action) =>
|
||||
(action as ActionNotificationModel).toEntity()).toList(),
|
||||
dateCreation: dateCreation,
|
||||
dateEnvoiProgramme: dateEnvoiProgramme,
|
||||
dateEnvoi: dateEnvoi,
|
||||
dateExpiration: dateExpiration,
|
||||
dateDerniereLecture: dateDerniereLecture,
|
||||
priorite: priorite,
|
||||
estLue: estLue,
|
||||
estImportante: estImportante,
|
||||
estArchivee: estArchivee,
|
||||
nombreAffichages: nombreAffichages,
|
||||
nombreClics: nombreClics,
|
||||
tags: tags,
|
||||
campagneId: campagneId,
|
||||
plateforme: plateforme,
|
||||
tokenFCM: tokenFCM,
|
||||
);
|
||||
}
|
||||
|
||||
/// Crée un modèle depuis une notification Firebase
|
||||
factory NotificationModel.fromFirebaseMessage(Map<String, dynamic> data) {
|
||||
// Extraction des données de base
|
||||
final id = data['id'] ?? data['notification_id'] ?? '';
|
||||
final titre = data['title'] ?? data['titre'] ?? '';
|
||||
final message = data['body'] ?? data['message'] ?? '';
|
||||
final messageCourt = data['short_message'] ?? data['message_court'];
|
||||
|
||||
// Parsing du type de notification
|
||||
TypeNotification typeNotification = TypeNotification.annonceGenerale;
|
||||
if (data['type'] != null) {
|
||||
try {
|
||||
typeNotification = TypeNotification.values.firstWhere(
|
||||
(type) => type.name == data['type'] || type.toString().split('.').last == data['type'],
|
||||
orElse: () => TypeNotification.annonceGenerale,
|
||||
);
|
||||
} catch (e) {
|
||||
// Utilise le type par défaut en cas d'erreur
|
||||
}
|
||||
}
|
||||
|
||||
// Parsing du statut
|
||||
StatutNotification statut = StatutNotification.recue;
|
||||
if (data['status'] != null) {
|
||||
try {
|
||||
statut = StatutNotification.values.firstWhere(
|
||||
(s) => s.name == data['status'] || s.toString().split('.').last == data['status'],
|
||||
orElse: () => StatutNotification.recue,
|
||||
);
|
||||
} catch (e) {
|
||||
// Utilise le statut par défaut
|
||||
}
|
||||
}
|
||||
|
||||
// Parsing des actions rapides
|
||||
List<ActionNotification>? actionsRapides;
|
||||
if (data['actions'] != null && data['actions'] is List) {
|
||||
try {
|
||||
actionsRapides = (data['actions'] as List)
|
||||
.map((actionData) => ActionNotificationModel.fromJson(
|
||||
actionData is Map<String, dynamic> ? actionData : {}))
|
||||
.toList();
|
||||
} catch (e) {
|
||||
// Ignore les erreurs de parsing des actions
|
||||
}
|
||||
}
|
||||
|
||||
// Parsing des destinataires
|
||||
List<String> destinatairesIds = [];
|
||||
if (data['recipients'] != null) {
|
||||
if (data['recipients'] is List) {
|
||||
destinatairesIds = List<String>.from(data['recipients']);
|
||||
} else if (data['recipients'] is String) {
|
||||
destinatairesIds = [data['recipients']];
|
||||
}
|
||||
}
|
||||
|
||||
// Parsing des tags
|
||||
List<String>? tags;
|
||||
if (data['tags'] != null && data['tags'] is List) {
|
||||
tags = List<String>.from(data['tags']);
|
||||
}
|
||||
|
||||
// Parsing des dates
|
||||
DateTime dateCreation = DateTime.now();
|
||||
if (data['created_at'] != null) {
|
||||
try {
|
||||
if (data['created_at'] is int) {
|
||||
dateCreation = DateTime.fromMillisecondsSinceEpoch(data['created_at']);
|
||||
} else if (data['created_at'] is String) {
|
||||
dateCreation = DateTime.parse(data['created_at']);
|
||||
}
|
||||
} catch (e) {
|
||||
// Utilise la date actuelle en cas d'erreur
|
||||
}
|
||||
}
|
||||
|
||||
DateTime? dateExpiration;
|
||||
if (data['expires_at'] != null) {
|
||||
try {
|
||||
if (data['expires_at'] is int) {
|
||||
dateExpiration = DateTime.fromMillisecondsSinceEpoch(data['expires_at']);
|
||||
} else if (data['expires_at'] is String) {
|
||||
dateExpiration = DateTime.parse(data['expires_at']);
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore les erreurs de parsing de date
|
||||
}
|
||||
}
|
||||
|
||||
// Parsing des données personnalisées
|
||||
Map<String, dynamic>? donneesPersonnalisees;
|
||||
if (data['custom_data'] != null && data['custom_data'] is Map) {
|
||||
donneesPersonnalisees = Map<String, dynamic>.from(data['custom_data']);
|
||||
}
|
||||
|
||||
return NotificationModel(
|
||||
id: id,
|
||||
typeNotification: typeNotification,
|
||||
statut: statut,
|
||||
titre: titre,
|
||||
message: message,
|
||||
messageCourt: messageCourt,
|
||||
expediteurId: data['sender_id'],
|
||||
expediteurNom: data['sender_name'],
|
||||
destinatairesIds: destinatairesIds,
|
||||
organisationId: data['organization_id'],
|
||||
donneesPersonnalisees: donneesPersonnalisees,
|
||||
imageUrl: data['image_url'],
|
||||
iconeUrl: data['icon_url'],
|
||||
actionClic: data['click_action'],
|
||||
parametresAction: data['action_params'] != null
|
||||
? Map<String, String>.from(data['action_params'])
|
||||
: null,
|
||||
actionsRapides: actionsRapides,
|
||||
dateCreation: dateCreation,
|
||||
dateExpiration: dateExpiration,
|
||||
priorite: data['priority'] ?? 3,
|
||||
tags: tags,
|
||||
campagneId: data['campaign_id'],
|
||||
plateforme: data['platform'],
|
||||
tokenFCM: data['fcm_token'],
|
||||
);
|
||||
}
|
||||
|
||||
/// Convertit vers le format Firebase
|
||||
Map<String, dynamic> toFirebaseData() {
|
||||
final data = <String, dynamic>{
|
||||
'id': id,
|
||||
'type': typeNotification.name,
|
||||
'status': statut.name,
|
||||
'title': titre,
|
||||
'body': message,
|
||||
'recipients': destinatairesIds,
|
||||
'created_at': dateCreation.millisecondsSinceEpoch,
|
||||
'priority': priorite,
|
||||
};
|
||||
|
||||
if (messageCourt != null) data['short_message'] = messageCourt;
|
||||
if (expediteurId != null) data['sender_id'] = expediteurId;
|
||||
if (expediteurNom != null) data['sender_name'] = expediteurNom;
|
||||
if (organisationId != null) data['organization_id'] = organisationId;
|
||||
if (donneesPersonnalisees != null) data['custom_data'] = donneesPersonnalisees;
|
||||
if (imageUrl != null) data['image_url'] = imageUrl;
|
||||
if (iconeUrl != null) data['icon_url'] = iconeUrl;
|
||||
if (actionClic != null) data['click_action'] = actionClic;
|
||||
if (parametresAction != null) data['action_params'] = parametresAction;
|
||||
if (dateExpiration != null) data['expires_at'] = dateExpiration!.millisecondsSinceEpoch;
|
||||
if (tags != null) data['tags'] = tags;
|
||||
if (campagneId != null) data['campaign_id'] = campagneId;
|
||||
if (plateforme != null) data['platform'] = plateforme;
|
||||
if (tokenFCM != null) data['fcm_token'] = tokenFCM;
|
||||
|
||||
if (actionsRapides != null && actionsRapides!.isNotEmpty) {
|
||||
data['actions'] = actionsRapides!
|
||||
.map((action) => (action as ActionNotificationModel).toJson())
|
||||
.toList();
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/// Crée une copie avec des modifications
|
||||
NotificationModel copyWithModel({
|
||||
String? id,
|
||||
TypeNotification? typeNotification,
|
||||
StatutNotification? statut,
|
||||
String? titre,
|
||||
String? message,
|
||||
String? messageCourt,
|
||||
String? expediteurId,
|
||||
String? expediteurNom,
|
||||
List<String>? destinatairesIds,
|
||||
String? organisationId,
|
||||
Map<String, dynamic>? donneesPersonnalisees,
|
||||
String? imageUrl,
|
||||
String? iconeUrl,
|
||||
String? actionClic,
|
||||
Map<String, String>? parametresAction,
|
||||
List<ActionNotification>? actionsRapides,
|
||||
DateTime? dateCreation,
|
||||
DateTime? dateEnvoiProgramme,
|
||||
DateTime? dateEnvoi,
|
||||
DateTime? dateExpiration,
|
||||
DateTime? dateDerniereLecture,
|
||||
int? priorite,
|
||||
bool? estLue,
|
||||
bool? estImportante,
|
||||
bool? estArchivee,
|
||||
int? nombreAffichages,
|
||||
int? nombreClics,
|
||||
List<String>? tags,
|
||||
String? campagneId,
|
||||
String? plateforme,
|
||||
String? tokenFCM,
|
||||
}) {
|
||||
return NotificationModel(
|
||||
id: id ?? this.id,
|
||||
typeNotification: typeNotification ?? this.typeNotification,
|
||||
statut: statut ?? this.statut,
|
||||
titre: titre ?? this.titre,
|
||||
message: message ?? this.message,
|
||||
messageCourt: messageCourt ?? this.messageCourt,
|
||||
expediteurId: expediteurId ?? this.expediteurId,
|
||||
expediteurNom: expediteurNom ?? this.expediteurNom,
|
||||
destinatairesIds: destinatairesIds ?? this.destinatairesIds,
|
||||
organisationId: organisationId ?? this.organisationId,
|
||||
donneesPersonnalisees: donneesPersonnalisees ?? this.donneesPersonnalisees,
|
||||
imageUrl: imageUrl ?? this.imageUrl,
|
||||
iconeUrl: iconeUrl ?? this.iconeUrl,
|
||||
actionClic: actionClic ?? this.actionClic,
|
||||
parametresAction: parametresAction ?? this.parametresAction,
|
||||
actionsRapides: actionsRapides ?? this.actionsRapides,
|
||||
dateCreation: dateCreation ?? this.dateCreation,
|
||||
dateEnvoiProgramme: dateEnvoiProgramme ?? this.dateEnvoiProgramme,
|
||||
dateEnvoi: dateEnvoi ?? this.dateEnvoi,
|
||||
dateExpiration: dateExpiration ?? this.dateExpiration,
|
||||
dateDerniereLecture: dateDerniereLecture ?? this.dateDerniereLecture,
|
||||
priorite: priorite ?? this.priorite,
|
||||
estLue: estLue ?? this.estLue,
|
||||
estImportante: estImportante ?? this.estImportante,
|
||||
estArchivee: estArchivee ?? this.estArchivee,
|
||||
nombreAffichages: nombreAffichages ?? this.nombreAffichages,
|
||||
nombreClics: nombreClics ?? this.nombreClics,
|
||||
tags: tags ?? this.tags,
|
||||
campagneId: campagneId ?? this.campagneId,
|
||||
plateforme: plateforme ?? this.plateforme,
|
||||
tokenFCM: tokenFCM ?? this.tokenFCM,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,414 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'notification.g.dart';
|
||||
|
||||
/// Énumération des types de notification
|
||||
enum TypeNotification {
|
||||
// Événements
|
||||
@JsonValue('NOUVEL_EVENEMENT')
|
||||
nouvelEvenement('Nouvel événement', 'evenements', 'info', 'event', '#FF9800'),
|
||||
@JsonValue('RAPPEL_EVENEMENT')
|
||||
rappelEvenement('Rappel d\'événement', 'evenements', 'reminder', 'schedule', '#2196F3'),
|
||||
@JsonValue('EVENEMENT_ANNULE')
|
||||
evenementAnnule('Événement annulé', 'evenements', 'warning', 'event_busy', '#F44336'),
|
||||
@JsonValue('INSCRIPTION_CONFIRMEE')
|
||||
inscriptionConfirmee('Inscription confirmée', 'evenements', 'success', 'check_circle', '#4CAF50'),
|
||||
|
||||
// Cotisations
|
||||
@JsonValue('COTISATION_DUE')
|
||||
cotisationDue('Cotisation due', 'cotisations', 'reminder', 'payment', '#FF5722'),
|
||||
@JsonValue('COTISATION_PAYEE')
|
||||
cotisationPayee('Cotisation payée', 'cotisations', 'success', 'paid', '#4CAF50'),
|
||||
@JsonValue('PAIEMENT_CONFIRME')
|
||||
paiementConfirme('Paiement confirmé', 'cotisations', 'success', 'check_circle', '#4CAF50'),
|
||||
@JsonValue('PAIEMENT_ECHOUE')
|
||||
paiementEchoue('Paiement échoué', 'cotisations', 'error', 'error', '#F44336'),
|
||||
|
||||
// Solidarité
|
||||
@JsonValue('NOUVELLE_DEMANDE_AIDE')
|
||||
nouvelleDemandeAide('Nouvelle demande d\'aide', 'solidarite', 'info', 'help', '#E91E63'),
|
||||
@JsonValue('DEMANDE_AIDE_APPROUVEE')
|
||||
demandeAideApprouvee('Demande d\'aide approuvée', 'solidarite', 'success', 'thumb_up', '#4CAF50'),
|
||||
@JsonValue('AIDE_DISPONIBLE')
|
||||
aideDisponible('Aide disponible', 'solidarite', 'info', 'volunteer_activism', '#E91E63'),
|
||||
|
||||
// Membres
|
||||
@JsonValue('NOUVEAU_MEMBRE')
|
||||
nouveauMembre('Nouveau membre', 'membres', 'info', 'person_add', '#2196F3'),
|
||||
@JsonValue('ANNIVERSAIRE_MEMBRE')
|
||||
anniversaireMembre('Anniversaire de membre', 'membres', 'celebration', 'cake', '#FF9800'),
|
||||
|
||||
// Organisation
|
||||
@JsonValue('ANNONCE_GENERALE')
|
||||
annonceGenerale('Annonce générale', 'organisation', 'info', 'campaign', '#2196F3'),
|
||||
@JsonValue('REUNION_PROGRAMMEE')
|
||||
reunionProgrammee('Réunion programmée', 'organisation', 'info', 'groups', '#2196F3'),
|
||||
|
||||
// Messages
|
||||
@JsonValue('MESSAGE_PRIVE')
|
||||
messagePrive('Message privé', 'messages', 'info', 'mail', '#2196F3'),
|
||||
@JsonValue('MENTION')
|
||||
mention('Mention', 'messages', 'info', 'alternate_email', '#FF9800'),
|
||||
|
||||
// Système
|
||||
@JsonValue('MISE_A_JOUR_APP')
|
||||
miseAJourApp('Mise à jour disponible', 'systeme', 'info', 'system_update', '#2196F3'),
|
||||
@JsonValue('MAINTENANCE_PROGRAMMEE')
|
||||
maintenanceProgrammee('Maintenance programmée', 'systeme', 'warning', 'build', '#FF9800');
|
||||
|
||||
const TypeNotification(this.libelle, this.categorie, this.priorite, this.icone, this.couleur);
|
||||
|
||||
final String libelle;
|
||||
final String categorie;
|
||||
final String priorite;
|
||||
final String icone;
|
||||
final String couleur;
|
||||
|
||||
bool get isCritique => priorite == 'urgent' || priorite == 'error';
|
||||
bool get isRappel => priorite == 'reminder';
|
||||
bool get isPositive => priorite == 'success' || priorite == 'celebration';
|
||||
|
||||
int get niveauPriorite {
|
||||
switch (priorite) {
|
||||
case 'urgent': return 1;
|
||||
case 'error': return 2;
|
||||
case 'warning': return 3;
|
||||
case 'important': return 4;
|
||||
case 'reminder': return 5;
|
||||
case 'info': return 6;
|
||||
case 'success': return 7;
|
||||
case 'celebration': return 8;
|
||||
default: return 6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Énumération des statuts de notification
|
||||
enum StatutNotification {
|
||||
@JsonValue('BROUILLON')
|
||||
brouillon('Brouillon', 'draft', '#9E9E9E'),
|
||||
@JsonValue('PROGRAMMEE')
|
||||
programmee('Programmée', 'scheduled', '#FF9800'),
|
||||
@JsonValue('ENVOYEE')
|
||||
envoyee('Envoyée', 'sent', '#4CAF50'),
|
||||
@JsonValue('RECUE')
|
||||
recue('Reçue', 'received', '#4CAF50'),
|
||||
@JsonValue('AFFICHEE')
|
||||
affichee('Affichée', 'displayed', '#2196F3'),
|
||||
@JsonValue('OUVERTE')
|
||||
ouverte('Ouverte', 'opened', '#4CAF50'),
|
||||
@JsonValue('LUE')
|
||||
lue('Lue', 'read', '#4CAF50'),
|
||||
@JsonValue('NON_LUE')
|
||||
nonLue('Non lue', 'unread', '#FF9800'),
|
||||
@JsonValue('MARQUEE_IMPORTANTE')
|
||||
marqueeImportante('Marquée importante', 'starred', '#FF9800'),
|
||||
@JsonValue('SUPPRIMEE')
|
||||
supprimee('Supprimée', 'deleted', '#F44336'),
|
||||
@JsonValue('ARCHIVEE')
|
||||
archivee('Archivée', 'archived', '#9E9E9E'),
|
||||
@JsonValue('ECHEC_ENVOI')
|
||||
echecEnvoi('Échec d\'envoi', 'failed', '#F44336');
|
||||
|
||||
const StatutNotification(this.libelle, this.code, this.couleur);
|
||||
|
||||
final String libelle;
|
||||
final String code;
|
||||
final String couleur;
|
||||
|
||||
bool get isSucces => this == envoyee || this == recue || this == affichee || this == ouverte || this == lue;
|
||||
bool get isErreur => this == echecEnvoi;
|
||||
bool get isFinal => this == supprimee || this == archivee || isErreur;
|
||||
}
|
||||
|
||||
/// Action rapide de notification
|
||||
@JsonSerializable()
|
||||
class ActionNotification extends Equatable {
|
||||
const ActionNotification({
|
||||
required this.id,
|
||||
required this.libelle,
|
||||
required this.typeAction,
|
||||
this.description,
|
||||
this.icone,
|
||||
this.couleur,
|
||||
this.url,
|
||||
this.route,
|
||||
this.parametres,
|
||||
this.fermeNotification = true,
|
||||
this.necessiteConfirmation = false,
|
||||
this.estDestructive = false,
|
||||
this.ordre = 0,
|
||||
this.estActivee = true,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String libelle;
|
||||
final String? description;
|
||||
final String typeAction;
|
||||
final String? icone;
|
||||
final String? couleur;
|
||||
final String? url;
|
||||
final String? route;
|
||||
final Map<String, String>? parametres;
|
||||
final bool fermeNotification;
|
||||
final bool necessiteConfirmation;
|
||||
final bool estDestructive;
|
||||
final int ordre;
|
||||
final bool estActivee;
|
||||
|
||||
factory ActionNotification.fromJson(Map<String, dynamic> json) =>
|
||||
_$ActionNotificationFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$ActionNotificationToJson(this);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id, libelle, description, typeAction, icone, couleur,
|
||||
url, route, parametres, fermeNotification, necessiteConfirmation,
|
||||
estDestructive, ordre, estActivee,
|
||||
];
|
||||
|
||||
ActionNotification copyWith({
|
||||
String? id,
|
||||
String? libelle,
|
||||
String? description,
|
||||
String? typeAction,
|
||||
String? icone,
|
||||
String? couleur,
|
||||
String? url,
|
||||
String? route,
|
||||
Map<String, String>? parametres,
|
||||
bool? fermeNotification,
|
||||
bool? necessiteConfirmation,
|
||||
bool? estDestructive,
|
||||
int? ordre,
|
||||
bool? estActivee,
|
||||
}) {
|
||||
return ActionNotification(
|
||||
id: id ?? this.id,
|
||||
libelle: libelle ?? this.libelle,
|
||||
description: description ?? this.description,
|
||||
typeAction: typeAction ?? this.typeAction,
|
||||
icone: icone ?? this.icone,
|
||||
couleur: couleur ?? this.couleur,
|
||||
url: url ?? this.url,
|
||||
route: route ?? this.route,
|
||||
parametres: parametres ?? this.parametres,
|
||||
fermeNotification: fermeNotification ?? this.fermeNotification,
|
||||
necessiteConfirmation: necessiteConfirmation ?? this.necessiteConfirmation,
|
||||
estDestructive: estDestructive ?? this.estDestructive,
|
||||
ordre: ordre ?? this.ordre,
|
||||
estActivee: estActivee ?? this.estActivee,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Entité principale de notification
|
||||
@JsonSerializable()
|
||||
class NotificationEntity extends Equatable {
|
||||
const NotificationEntity({
|
||||
required this.id,
|
||||
required this.typeNotification,
|
||||
required this.statut,
|
||||
required this.titre,
|
||||
required this.message,
|
||||
this.messageCourt,
|
||||
this.expediteurId,
|
||||
this.expediteurNom,
|
||||
required this.destinatairesIds,
|
||||
this.organisationId,
|
||||
this.donneesPersonnalisees,
|
||||
this.imageUrl,
|
||||
this.iconeUrl,
|
||||
this.actionClic,
|
||||
this.parametresAction,
|
||||
this.actionsRapides,
|
||||
required this.dateCreation,
|
||||
this.dateEnvoiProgramme,
|
||||
this.dateEnvoi,
|
||||
this.dateExpiration,
|
||||
this.dateDerniereLecture,
|
||||
this.priorite = 3,
|
||||
this.estLue = false,
|
||||
this.estImportante = false,
|
||||
this.estArchivee = false,
|
||||
this.nombreAffichages = 0,
|
||||
this.nombreClics = 0,
|
||||
this.tags,
|
||||
this.campagneId,
|
||||
this.plateforme,
|
||||
this.tokenFCM,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final TypeNotification typeNotification;
|
||||
final StatutNotification statut;
|
||||
final String titre;
|
||||
final String message;
|
||||
final String? messageCourt;
|
||||
final String? expediteurId;
|
||||
final String? expediteurNom;
|
||||
final List<String> destinatairesIds;
|
||||
final String? organisationId;
|
||||
final Map<String, dynamic>? donneesPersonnalisees;
|
||||
final String? imageUrl;
|
||||
final String? iconeUrl;
|
||||
final String? actionClic;
|
||||
final Map<String, String>? parametresAction;
|
||||
final List<ActionNotification>? actionsRapides;
|
||||
final DateTime dateCreation;
|
||||
final DateTime? dateEnvoiProgramme;
|
||||
final DateTime? dateEnvoi;
|
||||
final DateTime? dateExpiration;
|
||||
final DateTime? dateDerniereLecture;
|
||||
final int priorite;
|
||||
final bool estLue;
|
||||
final bool estImportante;
|
||||
final bool estArchivee;
|
||||
final int nombreAffichages;
|
||||
final int nombreClics;
|
||||
final List<String>? tags;
|
||||
final String? campagneId;
|
||||
final String? plateforme;
|
||||
final String? tokenFCM;
|
||||
|
||||
factory NotificationEntity.fromJson(Map<String, dynamic> json) =>
|
||||
_$NotificationEntityFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$NotificationEntityToJson(this);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id, typeNotification, statut, titre, message, messageCourt,
|
||||
expediteurId, expediteurNom, destinatairesIds, organisationId,
|
||||
donneesPersonnalisees, imageUrl, iconeUrl, actionClic, parametresAction,
|
||||
actionsRapides, dateCreation, dateEnvoiProgramme, dateEnvoi,
|
||||
dateExpiration, dateDerniereLecture, priorite, estLue, estImportante,
|
||||
estArchivee, nombreAffichages, nombreClics, tags, campagneId,
|
||||
plateforme, tokenFCM,
|
||||
];
|
||||
|
||||
NotificationEntity copyWith({
|
||||
String? id,
|
||||
TypeNotification? typeNotification,
|
||||
StatutNotification? statut,
|
||||
String? titre,
|
||||
String? message,
|
||||
String? messageCourt,
|
||||
String? expediteurId,
|
||||
String? expediteurNom,
|
||||
List<String>? destinatairesIds,
|
||||
String? organisationId,
|
||||
Map<String, dynamic>? donneesPersonnalisees,
|
||||
String? imageUrl,
|
||||
String? iconeUrl,
|
||||
String? actionClic,
|
||||
Map<String, String>? parametresAction,
|
||||
List<ActionNotification>? actionsRapides,
|
||||
DateTime? dateCreation,
|
||||
DateTime? dateEnvoiProgramme,
|
||||
DateTime? dateEnvoi,
|
||||
DateTime? dateExpiration,
|
||||
DateTime? dateDerniereLecture,
|
||||
int? priorite,
|
||||
bool? estLue,
|
||||
bool? estImportante,
|
||||
bool? estArchivee,
|
||||
int? nombreAffichages,
|
||||
int? nombreClics,
|
||||
List<String>? tags,
|
||||
String? campagneId,
|
||||
String? plateforme,
|
||||
String? tokenFCM,
|
||||
}) {
|
||||
return NotificationEntity(
|
||||
id: id ?? this.id,
|
||||
typeNotification: typeNotification ?? this.typeNotification,
|
||||
statut: statut ?? this.statut,
|
||||
titre: titre ?? this.titre,
|
||||
message: message ?? this.message,
|
||||
messageCourt: messageCourt ?? this.messageCourt,
|
||||
expediteurId: expediteurId ?? this.expediteurId,
|
||||
expediteurNom: expediteurNom ?? this.expediteurNom,
|
||||
destinatairesIds: destinatairesIds ?? this.destinatairesIds,
|
||||
organisationId: organisationId ?? this.organisationId,
|
||||
donneesPersonnalisees: donneesPersonnalisees ?? this.donneesPersonnalisees,
|
||||
imageUrl: imageUrl ?? this.imageUrl,
|
||||
iconeUrl: iconeUrl ?? this.iconeUrl,
|
||||
actionClic: actionClic ?? this.actionClic,
|
||||
parametresAction: parametresAction ?? this.parametresAction,
|
||||
actionsRapides: actionsRapides ?? this.actionsRapides,
|
||||
dateCreation: dateCreation ?? this.dateCreation,
|
||||
dateEnvoiProgramme: dateEnvoiProgramme ?? this.dateEnvoiProgramme,
|
||||
dateEnvoi: dateEnvoi ?? this.dateEnvoi,
|
||||
dateExpiration: dateExpiration ?? this.dateExpiration,
|
||||
dateDerniereLecture: dateDerniereLecture ?? this.dateDerniereLecture,
|
||||
priorite: priorite ?? this.priorite,
|
||||
estLue: estLue ?? this.estLue,
|
||||
estImportante: estImportante ?? this.estImportante,
|
||||
estArchivee: estArchivee ?? this.estArchivee,
|
||||
nombreAffichages: nombreAffichages ?? this.nombreAffichages,
|
||||
nombreClics: nombreClics ?? this.nombreClics,
|
||||
tags: tags ?? this.tags,
|
||||
campagneId: campagneId ?? this.campagneId,
|
||||
plateforme: plateforme ?? this.plateforme,
|
||||
tokenFCM: tokenFCM ?? this.tokenFCM,
|
||||
);
|
||||
}
|
||||
|
||||
/// Vérifie si la notification est expirée
|
||||
bool get isExpiree {
|
||||
if (dateExpiration == null) return false;
|
||||
return DateTime.now().isAfter(dateExpiration!);
|
||||
}
|
||||
|
||||
/// Vérifie si la notification est récente (moins de 24h)
|
||||
bool get isRecente {
|
||||
final maintenant = DateTime.now();
|
||||
final difference = maintenant.difference(dateCreation);
|
||||
return difference.inHours < 24;
|
||||
}
|
||||
|
||||
/// Retourne le temps écoulé depuis la création
|
||||
String get tempsEcoule {
|
||||
final maintenant = DateTime.now();
|
||||
final difference = maintenant.difference(dateCreation);
|
||||
|
||||
if (difference.inMinutes < 1) {
|
||||
return 'À l\'instant';
|
||||
} else if (difference.inMinutes < 60) {
|
||||
return 'Il y a ${difference.inMinutes}min';
|
||||
} else if (difference.inHours < 24) {
|
||||
return 'Il y a ${difference.inHours}h';
|
||||
} else if (difference.inDays < 7) {
|
||||
return 'Il y a ${difference.inDays}j';
|
||||
} else {
|
||||
return 'Il y a ${(difference.inDays / 7).floor()}sem';
|
||||
}
|
||||
}
|
||||
|
||||
/// Retourne le message à afficher (court ou complet)
|
||||
String get messageAffichage => messageCourt ?? message;
|
||||
|
||||
/// Retourne la couleur du type de notification
|
||||
String get couleurType => typeNotification.couleur;
|
||||
|
||||
/// Retourne l'icône du type de notification
|
||||
String get iconeType => typeNotification.icone;
|
||||
|
||||
/// Vérifie si la notification a des actions rapides
|
||||
bool get hasActionsRapides => actionsRapides != null && actionsRapides!.isNotEmpty;
|
||||
|
||||
/// Retourne les actions rapides actives
|
||||
List<ActionNotification> get actionsRapidesActives {
|
||||
if (actionsRapides == null) return [];
|
||||
return actionsRapides!.where((action) => action.estActivee).toList();
|
||||
}
|
||||
|
||||
/// Calcule le taux d'engagement
|
||||
double get tauxEngagement {
|
||||
if (nombreAffichages == 0) return 0.0;
|
||||
return (nombreClics / nombreAffichages) * 100;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,451 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'notification.dart';
|
||||
|
||||
part 'preferences_notification.g.dart';
|
||||
|
||||
/// Énumération des canaux de notification
|
||||
enum CanalNotification {
|
||||
@JsonValue('URGENT_CHANNEL')
|
||||
urgent('urgent', 'Notifications urgentes', 5, true, true, '#F44336'),
|
||||
@JsonValue('ERROR_CHANNEL')
|
||||
error('error', 'Erreurs système', 4, true, true, '#F44336'),
|
||||
@JsonValue('WARNING_CHANNEL')
|
||||
warning('warning', 'Avertissements', 4, true, true, '#FF9800'),
|
||||
@JsonValue('IMPORTANT_CHANNEL')
|
||||
important('important', 'Notifications importantes', 4, true, true, '#FF5722'),
|
||||
@JsonValue('REMINDER_CHANNEL')
|
||||
reminder('reminder', 'Rappels', 3, true, true, '#2196F3'),
|
||||
@JsonValue('SUCCESS_CHANNEL')
|
||||
success('success', 'Confirmations', 2, false, false, '#4CAF50'),
|
||||
@JsonValue('DEFAULT_CHANNEL')
|
||||
defaultChannel('default', 'Notifications générales', 2, false, false, '#2196F3'),
|
||||
@JsonValue('EVENTS_CHANNEL')
|
||||
events('events', 'Événements', 3, true, false, '#2196F3'),
|
||||
@JsonValue('PAYMENTS_CHANNEL')
|
||||
payments('payments', 'Paiements', 4, true, true, '#4CAF50'),
|
||||
@JsonValue('SOLIDARITY_CHANNEL')
|
||||
solidarity('solidarity', 'Solidarité', 3, true, false, '#E91E63'),
|
||||
@JsonValue('MEMBERS_CHANNEL')
|
||||
members('members', 'Membres', 2, false, false, '#2196F3'),
|
||||
@JsonValue('ORGANIZATION_CHANNEL')
|
||||
organization('organization', 'Organisation', 3, true, false, '#2196F3'),
|
||||
@JsonValue('SYSTEM_CHANNEL')
|
||||
system('system', 'Système', 2, false, false, '#607D8B'),
|
||||
@JsonValue('MESSAGES_CHANNEL')
|
||||
messages('messages', 'Messages', 3, true, false, '#2196F3');
|
||||
|
||||
const CanalNotification(this.id, this.nom, this.importance, this.sonActive,
|
||||
this.vibrationActive, this.couleur);
|
||||
|
||||
final String id;
|
||||
final String nom;
|
||||
final int importance;
|
||||
final bool sonActive;
|
||||
final bool vibrationActive;
|
||||
final String couleur;
|
||||
|
||||
bool get isCritique => importance >= 4;
|
||||
bool get isSilencieux => !sonActive && !vibrationActive;
|
||||
}
|
||||
|
||||
/// Préférences spécifiques à un type de notification
|
||||
@JsonSerializable()
|
||||
class PreferenceTypeNotification extends Equatable {
|
||||
const PreferenceTypeNotification({
|
||||
this.active = true,
|
||||
this.priorite,
|
||||
this.sonPersonnalise,
|
||||
this.patternVibration,
|
||||
this.couleurLED,
|
||||
this.dureeAffichageSecondes,
|
||||
this.doitVibrer,
|
||||
this.doitEmettreSon,
|
||||
this.doitAllumerLED,
|
||||
this.ignoreModesilencieux = false,
|
||||
});
|
||||
|
||||
final bool active;
|
||||
final int? priorite;
|
||||
final String? sonPersonnalise;
|
||||
final List<int>? patternVibration;
|
||||
final String? couleurLED;
|
||||
final int? dureeAffichageSecondes;
|
||||
final bool? doitVibrer;
|
||||
final bool? doitEmettreSon;
|
||||
final bool? doitAllumerLED;
|
||||
final bool ignoreModesilencieux;
|
||||
|
||||
factory PreferenceTypeNotification.fromJson(Map<String, dynamic> json) =>
|
||||
_$PreferenceTypeNotificationFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$PreferenceTypeNotificationToJson(this);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
active, priorite, sonPersonnalise, patternVibration, couleurLED,
|
||||
dureeAffichageSecondes, doitVibrer, doitEmettreSon, doitAllumerLED,
|
||||
ignoreModesilencieux,
|
||||
];
|
||||
|
||||
PreferenceTypeNotification copyWith({
|
||||
bool? active,
|
||||
int? priorite,
|
||||
String? sonPersonnalise,
|
||||
List<int>? patternVibration,
|
||||
String? couleurLED,
|
||||
int? dureeAffichageSecondes,
|
||||
bool? doitVibrer,
|
||||
bool? doitEmettreSon,
|
||||
bool? doitAllumerLED,
|
||||
bool? ignoreModesilencieux,
|
||||
}) {
|
||||
return PreferenceTypeNotification(
|
||||
active: active ?? this.active,
|
||||
priorite: priorite ?? this.priorite,
|
||||
sonPersonnalise: sonPersonnalise ?? this.sonPersonnalise,
|
||||
patternVibration: patternVibration ?? this.patternVibration,
|
||||
couleurLED: couleurLED ?? this.couleurLED,
|
||||
dureeAffichageSecondes: dureeAffichageSecondes ?? this.dureeAffichageSecondes,
|
||||
doitVibrer: doitVibrer ?? this.doitVibrer,
|
||||
doitEmettreSon: doitEmettreSon ?? this.doitEmettreSon,
|
||||
doitAllumerLED: doitAllumerLED ?? this.doitAllumerLED,
|
||||
ignoreModesilencieux: ignoreModesilencieux ?? this.ignoreModesilencieux,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Préférences spécifiques à un canal de notification
|
||||
@JsonSerializable()
|
||||
class PreferenceCanalNotification extends Equatable {
|
||||
const PreferenceCanalNotification({
|
||||
this.active = true,
|
||||
this.importance,
|
||||
this.sonPersonnalise,
|
||||
this.patternVibration,
|
||||
this.couleurLED,
|
||||
this.sonActive,
|
||||
this.vibrationActive,
|
||||
this.ledActive,
|
||||
this.peutEtreDesactive = true,
|
||||
});
|
||||
|
||||
final bool active;
|
||||
final int? importance;
|
||||
final String? sonPersonnalise;
|
||||
final List<int>? patternVibration;
|
||||
final String? couleurLED;
|
||||
final bool? sonActive;
|
||||
final bool? vibrationActive;
|
||||
final bool? ledActive;
|
||||
final bool peutEtreDesactive;
|
||||
|
||||
factory PreferenceCanalNotification.fromJson(Map<String, dynamic> json) =>
|
||||
_$PreferenceCanalNotificationFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$PreferenceCanalNotificationToJson(this);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
active, importance, sonPersonnalise, patternVibration, couleurLED,
|
||||
sonActive, vibrationActive, ledActive, peutEtreDesactive,
|
||||
];
|
||||
|
||||
PreferenceCanalNotification copyWith({
|
||||
bool? active,
|
||||
int? importance,
|
||||
String? sonPersonnalise,
|
||||
List<int>? patternVibration,
|
||||
String? couleurLED,
|
||||
bool? sonActive,
|
||||
bool? vibrationActive,
|
||||
bool? ledActive,
|
||||
bool? peutEtreDesactive,
|
||||
}) {
|
||||
return PreferenceCanalNotification(
|
||||
active: active ?? this.active,
|
||||
importance: importance ?? this.importance,
|
||||
sonPersonnalise: sonPersonnalise ?? this.sonPersonnalise,
|
||||
patternVibration: patternVibration ?? this.patternVibration,
|
||||
couleurLED: couleurLED ?? this.couleurLED,
|
||||
sonActive: sonActive ?? this.sonActive,
|
||||
vibrationActive: vibrationActive ?? this.vibrationActive,
|
||||
ledActive: ledActive ?? this.ledActive,
|
||||
peutEtreDesactive: peutEtreDesactive ?? this.peutEtreDesactive,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Entité principale des préférences de notification
|
||||
@JsonSerializable()
|
||||
class PreferencesNotificationEntity extends Equatable {
|
||||
const PreferencesNotificationEntity({
|
||||
required this.id,
|
||||
required this.utilisateurId,
|
||||
this.organisationId,
|
||||
this.notificationsActivees = true,
|
||||
this.pushActivees = true,
|
||||
this.emailActivees = true,
|
||||
this.smsActivees = false,
|
||||
this.inAppActivees = true,
|
||||
this.typesActives,
|
||||
this.typesDesactivees,
|
||||
this.canauxActifs,
|
||||
this.canauxDesactives,
|
||||
this.modeSilencieux = false,
|
||||
this.heureDebutSilencieux,
|
||||
this.heureFinSilencieux,
|
||||
this.joursSilencieux,
|
||||
this.urgentesIgnorentSilencieux = true,
|
||||
this.frequenceRegroupementMinutes = 5,
|
||||
this.maxNotificationsSimultanees = 10,
|
||||
this.dureeAffichageSecondes = 10,
|
||||
this.vibrationActivee = true,
|
||||
this.sonActive = true,
|
||||
this.ledActivee = true,
|
||||
this.sonPersonnalise,
|
||||
this.patternVibrationPersonnalise,
|
||||
this.couleurLEDPersonnalisee,
|
||||
this.apercuEcranVerrouillage = true,
|
||||
this.affichageHistorique = true,
|
||||
this.dureeConservationJours = 30,
|
||||
this.marquageLectureAutomatique = false,
|
||||
this.delaiMarquageLectureSecondes,
|
||||
this.archivageAutomatique = true,
|
||||
this.delaiArchivageHeures = 168,
|
||||
this.preferencesParType,
|
||||
this.preferencesParCanal,
|
||||
this.motsClesFiltre,
|
||||
this.expediteursBloques,
|
||||
this.expediteursPrioritaires,
|
||||
this.notificationsTestActivees = false,
|
||||
this.niveauLog = 'INFO',
|
||||
this.tokenFCM,
|
||||
this.plateforme,
|
||||
this.versionApp,
|
||||
this.langue = 'fr',
|
||||
this.fuseauHoraire,
|
||||
this.metadonnees,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String utilisateurId;
|
||||
final String? organisationId;
|
||||
final bool notificationsActivees;
|
||||
final bool pushActivees;
|
||||
final bool emailActivees;
|
||||
final bool smsActivees;
|
||||
final bool inAppActivees;
|
||||
final Set<TypeNotification>? typesActives;
|
||||
final Set<TypeNotification>? typesDesactivees;
|
||||
final Set<CanalNotification>? canauxActifs;
|
||||
final Set<CanalNotification>? canauxDesactives;
|
||||
final bool modeSilencieux;
|
||||
final String? heureDebutSilencieux; // Format HH:mm
|
||||
final String? heureFinSilencieux; // Format HH:mm
|
||||
final Set<int>? joursSilencieux; // 1=Lundi, 7=Dimanche
|
||||
final bool urgentesIgnorentSilencieux;
|
||||
final int frequenceRegroupementMinutes;
|
||||
final int maxNotificationsSimultanees;
|
||||
final int dureeAffichageSecondes;
|
||||
final bool vibrationActivee;
|
||||
final bool sonActive;
|
||||
final bool ledActivee;
|
||||
final String? sonPersonnalise;
|
||||
final List<int>? patternVibrationPersonnalise;
|
||||
final String? couleurLEDPersonnalisee;
|
||||
final bool apercuEcranVerrouillage;
|
||||
final bool affichageHistorique;
|
||||
final int dureeConservationJours;
|
||||
final bool marquageLectureAutomatique;
|
||||
final int? delaiMarquageLectureSecondes;
|
||||
final bool archivageAutomatique;
|
||||
final int delaiArchivageHeures;
|
||||
final Map<TypeNotification, PreferenceTypeNotification>? preferencesParType;
|
||||
final Map<CanalNotification, PreferenceCanalNotification>? preferencesParCanal;
|
||||
final Set<String>? motsClesFiltre;
|
||||
final Set<String>? expediteursBloques;
|
||||
final Set<String>? expediteursPrioritaires;
|
||||
final bool notificationsTestActivees;
|
||||
final String niveauLog;
|
||||
final String? tokenFCM;
|
||||
final String? plateforme;
|
||||
final String? versionApp;
|
||||
final String langue;
|
||||
final String? fuseauHoraire;
|
||||
final Map<String, dynamic>? metadonnees;
|
||||
|
||||
factory PreferencesNotificationEntity.fromJson(Map<String, dynamic> json) =>
|
||||
_$PreferencesNotificationEntityFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$PreferencesNotificationEntityToJson(this);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id, utilisateurId, organisationId, notificationsActivees, pushActivees,
|
||||
emailActivees, smsActivees, inAppActivees, typesActives, typesDesactivees,
|
||||
canauxActifs, canauxDesactives, modeSilencieux, heureDebutSilencieux,
|
||||
heureFinSilencieux, joursSilencieux, urgentesIgnorentSilencieux,
|
||||
frequenceRegroupementMinutes, maxNotificationsSimultanees,
|
||||
dureeAffichageSecondes, vibrationActivee, sonActive, ledActivee,
|
||||
sonPersonnalise, patternVibrationPersonnalise, couleurLEDPersonnalisee,
|
||||
apercuEcranVerrouillage, affichageHistorique, dureeConservationJours,
|
||||
marquageLectureAutomatique, delaiMarquageLectureSecondes,
|
||||
archivageAutomatique, delaiArchivageHeures, preferencesParType,
|
||||
preferencesParCanal, motsClesFiltre, expediteursBloques,
|
||||
expediteursPrioritaires, notificationsTestActivees, niveauLog,
|
||||
tokenFCM, plateforme, versionApp, langue, fuseauHoraire, metadonnees,
|
||||
];
|
||||
|
||||
PreferencesNotificationEntity copyWith({
|
||||
String? id,
|
||||
String? utilisateurId,
|
||||
String? organisationId,
|
||||
bool? notificationsActivees,
|
||||
bool? pushActivees,
|
||||
bool? emailActivees,
|
||||
bool? smsActivees,
|
||||
bool? inAppActivees,
|
||||
Set<TypeNotification>? typesActives,
|
||||
Set<TypeNotification>? typesDesactivees,
|
||||
Set<CanalNotification>? canauxActifs,
|
||||
Set<CanalNotification>? canauxDesactives,
|
||||
bool? modeSilencieux,
|
||||
String? heureDebutSilencieux,
|
||||
String? heureFinSilencieux,
|
||||
Set<int>? joursSilencieux,
|
||||
bool? urgentesIgnorentSilencieux,
|
||||
int? frequenceRegroupementMinutes,
|
||||
int? maxNotificationsSimultanees,
|
||||
int? dureeAffichageSecondes,
|
||||
bool? vibrationActivee,
|
||||
bool? sonActive,
|
||||
bool? ledActivee,
|
||||
String? sonPersonnalise,
|
||||
List<int>? patternVibrationPersonnalise,
|
||||
String? couleurLEDPersonnalisee,
|
||||
bool? apercuEcranVerrouillage,
|
||||
bool? affichageHistorique,
|
||||
int? dureeConservationJours,
|
||||
bool? marquageLectureAutomatique,
|
||||
int? delaiMarquageLectureSecondes,
|
||||
bool? archivageAutomatique,
|
||||
int? delaiArchivageHeures,
|
||||
Map<TypeNotification, PreferenceTypeNotification>? preferencesParType,
|
||||
Map<CanalNotification, PreferenceCanalNotification>? preferencesParCanal,
|
||||
Set<String>? motsClesFiltre,
|
||||
Set<String>? expediteursBloques,
|
||||
Set<String>? expediteursPrioritaires,
|
||||
bool? notificationsTestActivees,
|
||||
String? niveauLog,
|
||||
String? tokenFCM,
|
||||
String? plateforme,
|
||||
String? versionApp,
|
||||
String? langue,
|
||||
String? fuseauHoraire,
|
||||
Map<String, dynamic>? metadonnees,
|
||||
}) {
|
||||
return PreferencesNotificationEntity(
|
||||
id: id ?? this.id,
|
||||
utilisateurId: utilisateurId ?? this.utilisateurId,
|
||||
organisationId: organisationId ?? this.organisationId,
|
||||
notificationsActivees: notificationsActivees ?? this.notificationsActivees,
|
||||
pushActivees: pushActivees ?? this.pushActivees,
|
||||
emailActivees: emailActivees ?? this.emailActivees,
|
||||
smsActivees: smsActivees ?? this.smsActivees,
|
||||
inAppActivees: inAppActivees ?? this.inAppActivees,
|
||||
typesActives: typesActives ?? this.typesActives,
|
||||
typesDesactivees: typesDesactivees ?? this.typesDesactivees,
|
||||
canauxActifs: canauxActifs ?? this.canauxActifs,
|
||||
canauxDesactives: canauxDesactives ?? this.canauxDesactives,
|
||||
modeSilencieux: modeSilencieux ?? this.modeSilencieux,
|
||||
heureDebutSilencieux: heureDebutSilencieux ?? this.heureDebutSilencieux,
|
||||
heureFinSilencieux: heureFinSilencieux ?? this.heureFinSilencieux,
|
||||
joursSilencieux: joursSilencieux ?? this.joursSilencieux,
|
||||
urgentesIgnorentSilencieux: urgentesIgnorentSilencieux ?? this.urgentesIgnorentSilencieux,
|
||||
frequenceRegroupementMinutes: frequenceRegroupementMinutes ?? this.frequenceRegroupementMinutes,
|
||||
maxNotificationsSimultanees: maxNotificationsSimultanees ?? this.maxNotificationsSimultanees,
|
||||
dureeAffichageSecondes: dureeAffichageSecondes ?? this.dureeAffichageSecondes,
|
||||
vibrationActivee: vibrationActivee ?? this.vibrationActivee,
|
||||
sonActive: sonActive ?? this.sonActive,
|
||||
ledActivee: ledActivee ?? this.ledActivee,
|
||||
sonPersonnalise: sonPersonnalise ?? this.sonPersonnalise,
|
||||
patternVibrationPersonnalise: patternVibrationPersonnalise ?? this.patternVibrationPersonnalise,
|
||||
couleurLEDPersonnalisee: couleurLEDPersonnalisee ?? this.couleurLEDPersonnalisee,
|
||||
apercuEcranVerrouillage: apercuEcranVerrouillage ?? this.apercuEcranVerrouillage,
|
||||
affichageHistorique: affichageHistorique ?? this.affichageHistorique,
|
||||
dureeConservationJours: dureeConservationJours ?? this.dureeConservationJours,
|
||||
marquageLectureAutomatique: marquageLectureAutomatique ?? this.marquageLectureAutomatique,
|
||||
delaiMarquageLectureSecondes: delaiMarquageLectureSecondes ?? this.delaiMarquageLectureSecondes,
|
||||
archivageAutomatique: archivageAutomatique ?? this.archivageAutomatique,
|
||||
delaiArchivageHeures: delaiArchivageHeures ?? this.delaiArchivageHeures,
|
||||
preferencesParType: preferencesParType ?? this.preferencesParType,
|
||||
preferencesParCanal: preferencesParCanal ?? this.preferencesParCanal,
|
||||
motsClesFiltre: motsClesFiltre ?? this.motsClesFiltre,
|
||||
expediteursBloques: expediteursBloques ?? this.expediteursBloques,
|
||||
expediteursPrioritaires: expediteursPrioritaires ?? this.expediteursPrioritaires,
|
||||
notificationsTestActivees: notificationsTestActivees ?? this.notificationsTestActivees,
|
||||
niveauLog: niveauLog ?? this.niveauLog,
|
||||
tokenFCM: tokenFCM ?? this.tokenFCM,
|
||||
plateforme: plateforme ?? this.plateforme,
|
||||
versionApp: versionApp ?? this.versionApp,
|
||||
langue: langue ?? this.langue,
|
||||
fuseauHoraire: fuseauHoraire ?? this.fuseauHoraire,
|
||||
metadonnees: metadonnees ?? this.metadonnees,
|
||||
);
|
||||
}
|
||||
|
||||
/// Vérifie si un type de notification est activé
|
||||
bool isTypeActive(TypeNotification type) {
|
||||
if (!notificationsActivees) return false;
|
||||
if (typesDesactivees?.contains(type) == true) return false;
|
||||
if (typesActives != null) return typesActives!.contains(type);
|
||||
return true; // Activé par défaut
|
||||
}
|
||||
|
||||
/// Vérifie si un canal de notification est activé
|
||||
bool isCanalActif(CanalNotification canal) {
|
||||
if (!notificationsActivees) return false;
|
||||
if (canauxDesactives?.contains(canal) == true) return false;
|
||||
if (canauxActifs != null) return canauxActifs!.contains(canal);
|
||||
return true; // Activé par défaut
|
||||
}
|
||||
|
||||
/// Vérifie si on est en mode silencieux actuellement
|
||||
bool get isEnModeSilencieux {
|
||||
if (!modeSilencieux) return false;
|
||||
if (heureDebutSilencieux == null || heureFinSilencieux == null) return false;
|
||||
|
||||
final maintenant = DateTime.now();
|
||||
final heureActuelle = '${maintenant.hour.toString().padLeft(2, '0')}:${maintenant.minute.toString().padLeft(2, '0')}';
|
||||
|
||||
// Gestion du cas où la période traverse minuit
|
||||
if (heureDebutSilencieux!.compareTo(heureFinSilencieux!) > 0) {
|
||||
return heureActuelle.compareTo(heureDebutSilencieux!) >= 0 ||
|
||||
heureActuelle.compareTo(heureFinSilencieux!) <= 0;
|
||||
} else {
|
||||
return heureActuelle.compareTo(heureDebutSilencieux!) >= 0 &&
|
||||
heureActuelle.compareTo(heureFinSilencieux!) <= 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Vérifie si un expéditeur est bloqué
|
||||
bool isExpediteurBloque(String? expediteurId) {
|
||||
if (expediteurId == null) return false;
|
||||
return expediteursBloques?.contains(expediteurId) == true;
|
||||
}
|
||||
|
||||
/// Vérifie si un expéditeur est prioritaire
|
||||
bool isExpediteurPrioritaire(String? expediteurId) {
|
||||
if (expediteurId == null) return false;
|
||||
return expediteursPrioritaires?.contains(expediteurId) == true;
|
||||
}
|
||||
|
||||
/// Crée des préférences par défaut pour un utilisateur
|
||||
static PreferencesNotificationEntity creerDefaut(String utilisateurId) {
|
||||
return PreferencesNotificationEntity(
|
||||
id: 'pref_$utilisateurId',
|
||||
utilisateurId: utilisateurId,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import '../../../../core/error/failures.dart';
|
||||
import '../entities/notification.dart';
|
||||
import '../entities/preferences_notification.dart';
|
||||
|
||||
/// Repository abstrait pour la gestion des notifications
|
||||
abstract class NotificationsRepository {
|
||||
|
||||
// === GESTION DES NOTIFICATIONS ===
|
||||
|
||||
/// Récupère les notifications d'un utilisateur
|
||||
///
|
||||
/// [utilisateurId] ID de l'utilisateur
|
||||
/// [includeArchivees] Inclure les notifications archivées
|
||||
/// [limite] Nombre maximum de notifications à retourner
|
||||
/// [offset] Décalage pour la pagination
|
||||
Future<Either<Failure, List<NotificationEntity>>> obtenirNotifications({
|
||||
required String utilisateurId,
|
||||
bool includeArchivees = false,
|
||||
int limite = 50,
|
||||
int offset = 0,
|
||||
});
|
||||
|
||||
/// Récupère une notification spécifique
|
||||
///
|
||||
/// [notificationId] ID de la notification
|
||||
Future<Either<Failure, NotificationEntity>> obtenirNotification(String notificationId);
|
||||
|
||||
/// Marque une notification comme lue
|
||||
///
|
||||
/// [notificationId] ID de la notification
|
||||
/// [utilisateurId] ID de l'utilisateur
|
||||
Future<Either<Failure, void>> marquerCommeLue(String notificationId, String utilisateurId);
|
||||
|
||||
/// Marque toutes les notifications comme lues
|
||||
///
|
||||
/// [utilisateurId] ID de l'utilisateur
|
||||
Future<Either<Failure, void>> marquerToutesCommeLues(String utilisateurId);
|
||||
|
||||
/// Marque une notification comme importante
|
||||
///
|
||||
/// [notificationId] ID de la notification
|
||||
/// [utilisateurId] ID de l'utilisateur
|
||||
/// [importante] true pour marquer comme importante, false pour enlever
|
||||
Future<Either<Failure, void>> marquerCommeImportante(
|
||||
String notificationId,
|
||||
String utilisateurId,
|
||||
bool importante,
|
||||
);
|
||||
|
||||
/// Archive une notification
|
||||
///
|
||||
/// [notificationId] ID de la notification
|
||||
/// [utilisateurId] ID de l'utilisateur
|
||||
Future<Either<Failure, void>> archiverNotification(String notificationId, String utilisateurId);
|
||||
|
||||
/// Archive toutes les notifications lues
|
||||
///
|
||||
/// [utilisateurId] ID de l'utilisateur
|
||||
Future<Either<Failure, void>> archiverToutesLues(String utilisateurId);
|
||||
|
||||
/// Supprime une notification
|
||||
///
|
||||
/// [notificationId] ID de la notification
|
||||
/// [utilisateurId] ID de l'utilisateur
|
||||
Future<Either<Failure, void>> supprimerNotification(String notificationId, String utilisateurId);
|
||||
|
||||
/// Supprime toutes les notifications archivées
|
||||
///
|
||||
/// [utilisateurId] ID de l'utilisateur
|
||||
Future<Either<Failure, void>> supprimerToutesArchivees(String utilisateurId);
|
||||
|
||||
// === FILTRAGE ET RECHERCHE ===
|
||||
|
||||
/// Recherche des notifications par critères
|
||||
///
|
||||
/// [utilisateurId] ID de l'utilisateur
|
||||
/// [query] Texte de recherche
|
||||
/// [types] Types de notifications à inclure
|
||||
/// [statuts] Statuts de notifications à inclure
|
||||
/// [dateDebut] Date de début de la période
|
||||
/// [dateFin] Date de fin de la période
|
||||
/// [limite] Nombre maximum de résultats
|
||||
Future<Either<Failure, List<NotificationEntity>>> rechercherNotifications({
|
||||
required String utilisateurId,
|
||||
String? query,
|
||||
List<TypeNotification>? types,
|
||||
List<StatutNotification>? statuts,
|
||||
DateTime? dateDebut,
|
||||
DateTime? dateFin,
|
||||
int limite = 50,
|
||||
});
|
||||
|
||||
/// Récupère les notifications par type
|
||||
///
|
||||
/// [utilisateurId] ID de l'utilisateur
|
||||
/// [type] Type de notification
|
||||
/// [limite] Nombre maximum de notifications
|
||||
Future<Either<Failure, List<NotificationEntity>>> obtenirNotificationsParType(
|
||||
String utilisateurId,
|
||||
TypeNotification type, {
|
||||
int limite = 50,
|
||||
});
|
||||
|
||||
/// Récupère les notifications non lues
|
||||
///
|
||||
/// [utilisateurId] ID de l'utilisateur
|
||||
/// [limite] Nombre maximum de notifications
|
||||
Future<Either<Failure, List<NotificationEntity>>> obtenirNotificationsNonLues(
|
||||
String utilisateurId, {
|
||||
int limite = 50,
|
||||
});
|
||||
|
||||
/// Récupère les notifications importantes
|
||||
///
|
||||
/// [utilisateurId] ID de l'utilisateur
|
||||
/// [limite] Nombre maximum de notifications
|
||||
Future<Either<Failure, List<NotificationEntity>>> obtenirNotificationsImportantes(
|
||||
String utilisateurId, {
|
||||
int limite = 50,
|
||||
});
|
||||
|
||||
// === STATISTIQUES ===
|
||||
|
||||
/// Récupère le nombre de notifications non lues
|
||||
///
|
||||
/// [utilisateurId] ID de l'utilisateur
|
||||
Future<Either<Failure, int>> obtenirNombreNonLues(String utilisateurId);
|
||||
|
||||
/// Récupère les statistiques des notifications
|
||||
///
|
||||
/// [utilisateurId] ID de l'utilisateur
|
||||
/// [periode] Période d'analyse (en jours)
|
||||
Future<Either<Failure, Map<String, dynamic>>> obtenirStatistiques(
|
||||
String utilisateurId, {
|
||||
int periode = 30,
|
||||
});
|
||||
|
||||
// === ACTIONS SUR LES NOTIFICATIONS ===
|
||||
|
||||
/// Exécute une action rapide sur une notification
|
||||
///
|
||||
/// [notificationId] ID de la notification
|
||||
/// [actionId] ID de l'action à exécuter
|
||||
/// [utilisateurId] ID de l'utilisateur
|
||||
/// [parametres] Paramètres additionnels pour l'action
|
||||
Future<Either<Failure, Map<String, dynamic>>> executerActionRapide(
|
||||
String notificationId,
|
||||
String actionId,
|
||||
String utilisateurId, {
|
||||
Map<String, dynamic>? parametres,
|
||||
});
|
||||
|
||||
/// Signale une notification comme spam
|
||||
///
|
||||
/// [notificationId] ID de la notification
|
||||
/// [utilisateurId] ID de l'utilisateur
|
||||
/// [raison] Raison du signalement
|
||||
Future<Either<Failure, void>> signalerSpam(
|
||||
String notificationId,
|
||||
String utilisateurId,
|
||||
String raison,
|
||||
);
|
||||
|
||||
// === PRÉFÉRENCES DE NOTIFICATION ===
|
||||
|
||||
/// Récupère les préférences de notification d'un utilisateur
|
||||
///
|
||||
/// [utilisateurId] ID de l'utilisateur
|
||||
Future<Either<Failure, PreferencesNotificationEntity>> obtenirPreferences(String utilisateurId);
|
||||
|
||||
/// Met à jour les préférences de notification
|
||||
///
|
||||
/// [preferences] Nouvelles préférences
|
||||
Future<Either<Failure, void>> mettreAJourPreferences(PreferencesNotificationEntity preferences);
|
||||
|
||||
/// Réinitialise les préférences aux valeurs par défaut
|
||||
///
|
||||
/// [utilisateurId] ID de l'utilisateur
|
||||
Future<Either<Failure, PreferencesNotificationEntity>> reinitialiserPreferences(String utilisateurId);
|
||||
|
||||
/// Active/désactive un type de notification
|
||||
///
|
||||
/// [utilisateurId] ID de l'utilisateur
|
||||
/// [type] Type de notification
|
||||
/// [active] true pour activer, false pour désactiver
|
||||
Future<Either<Failure, void>> toggleTypeNotification(
|
||||
String utilisateurId,
|
||||
TypeNotification type,
|
||||
bool active,
|
||||
);
|
||||
|
||||
/// Active/désactive un canal de notification
|
||||
///
|
||||
/// [utilisateurId] ID de l'utilisateur
|
||||
/// [canal] Canal de notification
|
||||
/// [active] true pour activer, false pour désactiver
|
||||
Future<Either<Failure, void>> toggleCanalNotification(
|
||||
String utilisateurId,
|
||||
CanalNotification canal,
|
||||
bool active,
|
||||
);
|
||||
|
||||
/// Configure le mode silencieux
|
||||
///
|
||||
/// [utilisateurId] ID de l'utilisateur
|
||||
/// [active] true pour activer le mode silencieux
|
||||
/// [heureDebut] Heure de début (format HH:mm)
|
||||
/// [heureFin] Heure de fin (format HH:mm)
|
||||
/// [jours] Jours de la semaine (1=Lundi, 7=Dimanche)
|
||||
Future<Either<Failure, void>> configurerModeSilencieux(
|
||||
String utilisateurId,
|
||||
bool active, {
|
||||
String? heureDebut,
|
||||
String? heureFin,
|
||||
Set<int>? jours,
|
||||
});
|
||||
|
||||
// === GESTION DES TOKENS FCM ===
|
||||
|
||||
/// Enregistre ou met à jour le token FCM
|
||||
///
|
||||
/// [utilisateurId] ID de l'utilisateur
|
||||
/// [token] Token FCM
|
||||
/// [plateforme] Plateforme (android, ios)
|
||||
Future<Either<Failure, void>> enregistrerTokenFCM(
|
||||
String utilisateurId,
|
||||
String token,
|
||||
String plateforme,
|
||||
);
|
||||
|
||||
/// Supprime le token FCM
|
||||
///
|
||||
/// [utilisateurId] ID de l'utilisateur
|
||||
Future<Either<Failure, void>> supprimerTokenFCM(String utilisateurId);
|
||||
|
||||
// === NOTIFICATIONS DE TEST ===
|
||||
|
||||
/// Envoie une notification de test
|
||||
///
|
||||
/// [utilisateurId] ID de l'utilisateur
|
||||
/// [type] Type de notification à tester
|
||||
Future<Either<Failure, NotificationEntity>> envoyerNotificationTest(
|
||||
String utilisateurId,
|
||||
TypeNotification type,
|
||||
);
|
||||
|
||||
// === CACHE ET SYNCHRONISATION ===
|
||||
|
||||
/// Synchronise les notifications avec le serveur
|
||||
///
|
||||
/// [utilisateurId] ID de l'utilisateur
|
||||
/// [forceSync] Force la synchronisation même si le cache est récent
|
||||
Future<Either<Failure, void>> synchroniser(String utilisateurId, {bool forceSync = false});
|
||||
|
||||
/// Vide le cache des notifications
|
||||
///
|
||||
/// [utilisateurId] ID de l'utilisateur (optionnel, vide tout si null)
|
||||
Future<Either<Failure, void>> viderCache([String? utilisateurId]);
|
||||
|
||||
/// Vérifie si les données sont en cache et récentes
|
||||
///
|
||||
/// [utilisateurId] ID de l'utilisateur
|
||||
/// [maxAgeMinutes] Âge maximum du cache en minutes
|
||||
Future<bool> isCacheValide(String utilisateurId, {int maxAgeMinutes = 5});
|
||||
|
||||
// === ABONNEMENTS ET TOPICS ===
|
||||
|
||||
/// S'abonne à un topic de notifications
|
||||
///
|
||||
/// [utilisateurId] ID de l'utilisateur
|
||||
/// [topic] Nom du topic
|
||||
Future<Either<Failure, void>> abonnerAuTopic(String utilisateurId, String topic);
|
||||
|
||||
/// Se désabonne d'un topic de notifications
|
||||
///
|
||||
/// [utilisateurId] ID de l'utilisateur
|
||||
/// [topic] Nom du topic
|
||||
Future<Either<Failure, void>> desabonnerDuTopic(String utilisateurId, String topic);
|
||||
|
||||
/// Récupère la liste des topics auxquels l'utilisateur est abonné
|
||||
///
|
||||
/// [utilisateurId] ID de l'utilisateur
|
||||
Future<Either<Failure, List<String>>> obtenirTopicsAbornes(String utilisateurId);
|
||||
|
||||
// === EXPORT ET SAUVEGARDE ===
|
||||
|
||||
/// Exporte les notifications vers un fichier
|
||||
///
|
||||
/// [utilisateurId] ID de l'utilisateur
|
||||
/// [format] Format d'export (json, csv)
|
||||
/// [dateDebut] Date de début de la période
|
||||
/// [dateFin] Date de fin de la période
|
||||
Future<Either<Failure, String>> exporterNotifications(
|
||||
String utilisateurId,
|
||||
String format, {
|
||||
DateTime? dateDebut,
|
||||
DateTime? dateFin,
|
||||
});
|
||||
|
||||
/// Sauvegarde les notifications localement
|
||||
///
|
||||
/// [utilisateurId] ID de l'utilisateur
|
||||
Future<Either<Failure, void>> sauvegarderLocalement(String utilisateurId);
|
||||
|
||||
/// Restaure les notifications depuis une sauvegarde locale
|
||||
///
|
||||
/// [utilisateurId] ID de l'utilisateur
|
||||
Future<Either<Failure, void>> restaurerDepuisSauvegarde(String utilisateurId);
|
||||
}
|
||||
@@ -0,0 +1,388 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import '../../../../core/error/failures.dart';
|
||||
import '../../../../core/usecases/usecase.dart';
|
||||
import '../entities/notification.dart';
|
||||
import '../repositories/notifications_repository.dart';
|
||||
|
||||
/// Use case pour marquer une notification comme lue
|
||||
class MarquerCommeLueUseCase implements UseCase<void, MarquerCommeLueParams> {
|
||||
final NotificationsRepository repository;
|
||||
|
||||
MarquerCommeLueUseCase(this.repository);
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> call(MarquerCommeLueParams params) async {
|
||||
return await repository.marquerCommeLue(
|
||||
params.notificationId,
|
||||
params.utilisateurId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Paramètres pour marquer comme lue
|
||||
class MarquerCommeLueParams {
|
||||
final String notificationId;
|
||||
final String utilisateurId;
|
||||
|
||||
const MarquerCommeLueParams({
|
||||
required this.notificationId,
|
||||
required this.utilisateurId,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'MarquerCommeLueParams{notificationId: $notificationId, utilisateurId: $utilisateurId}';
|
||||
}
|
||||
}
|
||||
|
||||
/// Use case pour marquer toutes les notifications comme lues
|
||||
class MarquerToutesCommeLuesUseCase implements UseCase<void, String> {
|
||||
final NotificationsRepository repository;
|
||||
|
||||
MarquerToutesCommeLuesUseCase(this.repository);
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> call(String utilisateurId) async {
|
||||
return await repository.marquerToutesCommeLues(utilisateurId);
|
||||
}
|
||||
}
|
||||
|
||||
/// Use case pour marquer une notification comme importante
|
||||
class MarquerCommeImportanteUseCase implements UseCase<void, MarquerCommeImportanteParams> {
|
||||
final NotificationsRepository repository;
|
||||
|
||||
MarquerCommeImportanteUseCase(this.repository);
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> call(MarquerCommeImportanteParams params) async {
|
||||
return await repository.marquerCommeImportante(
|
||||
params.notificationId,
|
||||
params.utilisateurId,
|
||||
params.importante,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Paramètres pour marquer comme importante
|
||||
class MarquerCommeImportanteParams {
|
||||
final String notificationId;
|
||||
final String utilisateurId;
|
||||
final bool importante;
|
||||
|
||||
const MarquerCommeImportanteParams({
|
||||
required this.notificationId,
|
||||
required this.utilisateurId,
|
||||
required this.importante,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'MarquerCommeImportanteParams{notificationId: $notificationId, utilisateurId: $utilisateurId, importante: $importante}';
|
||||
}
|
||||
}
|
||||
|
||||
/// Use case pour archiver une notification
|
||||
class ArchiverNotificationUseCase implements UseCase<void, ArchiverNotificationParams> {
|
||||
final NotificationsRepository repository;
|
||||
|
||||
ArchiverNotificationUseCase(this.repository);
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> call(ArchiverNotificationParams params) async {
|
||||
return await repository.archiverNotification(
|
||||
params.notificationId,
|
||||
params.utilisateurId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Paramètres pour archiver une notification
|
||||
class ArchiverNotificationParams {
|
||||
final String notificationId;
|
||||
final String utilisateurId;
|
||||
|
||||
const ArchiverNotificationParams({
|
||||
required this.notificationId,
|
||||
required this.utilisateurId,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ArchiverNotificationParams{notificationId: $notificationId, utilisateurId: $utilisateurId}';
|
||||
}
|
||||
}
|
||||
|
||||
/// Use case pour archiver toutes les notifications lues
|
||||
class ArchiverToutesLuesUseCase implements UseCase<void, String> {
|
||||
final NotificationsRepository repository;
|
||||
|
||||
ArchiverToutesLuesUseCase(this.repository);
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> call(String utilisateurId) async {
|
||||
return await repository.archiverToutesLues(utilisateurId);
|
||||
}
|
||||
}
|
||||
|
||||
/// Use case pour supprimer une notification
|
||||
class SupprimerNotificationUseCase implements UseCase<void, SupprimerNotificationParams> {
|
||||
final NotificationsRepository repository;
|
||||
|
||||
SupprimerNotificationUseCase(this.repository);
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> call(SupprimerNotificationParams params) async {
|
||||
return await repository.supprimerNotification(
|
||||
params.notificationId,
|
||||
params.utilisateurId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Paramètres pour supprimer une notification
|
||||
class SupprimerNotificationParams {
|
||||
final String notificationId;
|
||||
final String utilisateurId;
|
||||
|
||||
const SupprimerNotificationParams({
|
||||
required this.notificationId,
|
||||
required this.utilisateurId,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SupprimerNotificationParams{notificationId: $notificationId, utilisateurId: $utilisateurId}';
|
||||
}
|
||||
}
|
||||
|
||||
/// Use case pour supprimer toutes les notifications archivées
|
||||
class SupprimerToutesArchiveesUseCase implements UseCase<void, String> {
|
||||
final NotificationsRepository repository;
|
||||
|
||||
SupprimerToutesArchiveesUseCase(this.repository);
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> call(String utilisateurId) async {
|
||||
return await repository.supprimerToutesArchivees(utilisateurId);
|
||||
}
|
||||
}
|
||||
|
||||
/// Use case pour exécuter une action rapide
|
||||
class ExecuterActionRapideUseCase implements UseCase<Map<String, dynamic>, ExecuterActionRapideParams> {
|
||||
final NotificationsRepository repository;
|
||||
|
||||
ExecuterActionRapideUseCase(this.repository);
|
||||
|
||||
@override
|
||||
Future<Either<Failure, Map<String, dynamic>>> call(ExecuterActionRapideParams params) async {
|
||||
return await repository.executerActionRapide(
|
||||
params.notificationId,
|
||||
params.actionId,
|
||||
params.utilisateurId,
|
||||
parametres: params.parametres,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Paramètres pour exécuter une action rapide
|
||||
class ExecuterActionRapideParams {
|
||||
final String notificationId;
|
||||
final String actionId;
|
||||
final String utilisateurId;
|
||||
final Map<String, dynamic>? parametres;
|
||||
|
||||
const ExecuterActionRapideParams({
|
||||
required this.notificationId,
|
||||
required this.actionId,
|
||||
required this.utilisateurId,
|
||||
this.parametres,
|
||||
});
|
||||
|
||||
ExecuterActionRapideParams copyWith({
|
||||
String? notificationId,
|
||||
String? actionId,
|
||||
String? utilisateurId,
|
||||
Map<String, dynamic>? parametres,
|
||||
}) {
|
||||
return ExecuterActionRapideParams(
|
||||
notificationId: notificationId ?? this.notificationId,
|
||||
actionId: actionId ?? this.actionId,
|
||||
utilisateurId: utilisateurId ?? this.utilisateurId,
|
||||
parametres: parametres ?? this.parametres,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ExecuterActionRapideParams{notificationId: $notificationId, actionId: $actionId, utilisateurId: $utilisateurId, parametres: $parametres}';
|
||||
}
|
||||
}
|
||||
|
||||
/// Use case pour signaler une notification comme spam
|
||||
class SignalerSpamUseCase implements UseCase<void, SignalerSpamParams> {
|
||||
final NotificationsRepository repository;
|
||||
|
||||
SignalerSpamUseCase(this.repository);
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> call(SignalerSpamParams params) async {
|
||||
return await repository.signalerSpam(
|
||||
params.notificationId,
|
||||
params.utilisateurId,
|
||||
params.raison,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Paramètres pour signaler comme spam
|
||||
class SignalerSpamParams {
|
||||
final String notificationId;
|
||||
final String utilisateurId;
|
||||
final String raison;
|
||||
|
||||
const SignalerSpamParams({
|
||||
required this.notificationId,
|
||||
required this.utilisateurId,
|
||||
required this.raison,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SignalerSpamParams{notificationId: $notificationId, utilisateurId: $utilisateurId, raison: $raison}';
|
||||
}
|
||||
}
|
||||
|
||||
/// Use case pour synchroniser les notifications
|
||||
class SynchroniserNotificationsUseCase implements UseCase<void, SynchroniserNotificationsParams> {
|
||||
final NotificationsRepository repository;
|
||||
|
||||
SynchroniserNotificationsUseCase(this.repository);
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> call(SynchroniserNotificationsParams params) async {
|
||||
return await repository.synchroniser(
|
||||
params.utilisateurId,
|
||||
forceSync: params.forceSync,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Paramètres pour synchroniser les notifications
|
||||
class SynchroniserNotificationsParams {
|
||||
final String utilisateurId;
|
||||
final bool forceSync;
|
||||
|
||||
const SynchroniserNotificationsParams({
|
||||
required this.utilisateurId,
|
||||
this.forceSync = false,
|
||||
});
|
||||
|
||||
SynchroniserNotificationsParams copyWith({
|
||||
String? utilisateurId,
|
||||
bool? forceSync,
|
||||
}) {
|
||||
return SynchroniserNotificationsParams(
|
||||
utilisateurId: utilisateurId ?? this.utilisateurId,
|
||||
forceSync: forceSync ?? this.forceSync,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SynchroniserNotificationsParams{utilisateurId: $utilisateurId, forceSync: $forceSync}';
|
||||
}
|
||||
}
|
||||
|
||||
/// Use case pour vider le cache des notifications
|
||||
class ViderCacheNotificationsUseCase implements UseCase<void, String?> {
|
||||
final NotificationsRepository repository;
|
||||
|
||||
ViderCacheNotificationsUseCase(this.repository);
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> call(String? utilisateurId) async {
|
||||
return await repository.viderCache(utilisateurId);
|
||||
}
|
||||
}
|
||||
|
||||
/// Use case pour envoyer une notification de test
|
||||
class EnvoyerNotificationTestUseCase implements UseCase<NotificationEntity, EnvoyerNotificationTestParams> {
|
||||
final NotificationsRepository repository;
|
||||
|
||||
EnvoyerNotificationTestUseCase(this.repository);
|
||||
|
||||
@override
|
||||
Future<Either<Failure, NotificationEntity>> call(EnvoyerNotificationTestParams params) async {
|
||||
return await repository.envoyerNotificationTest(
|
||||
params.utilisateurId,
|
||||
params.type,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Paramètres pour envoyer une notification de test
|
||||
class EnvoyerNotificationTestParams {
|
||||
final String utilisateurId;
|
||||
final TypeNotification type;
|
||||
|
||||
const EnvoyerNotificationTestParams({
|
||||
required this.utilisateurId,
|
||||
required this.type,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'EnvoyerNotificationTestParams{utilisateurId: $utilisateurId, type: $type}';
|
||||
}
|
||||
}
|
||||
|
||||
/// Use case pour exporter les notifications
|
||||
class ExporterNotificationsUseCase implements UseCase<String, ExporterNotificationsParams> {
|
||||
final NotificationsRepository repository;
|
||||
|
||||
ExporterNotificationsUseCase(this.repository);
|
||||
|
||||
@override
|
||||
Future<Either<Failure, String>> call(ExporterNotificationsParams params) async {
|
||||
return await repository.exporterNotifications(
|
||||
params.utilisateurId,
|
||||
params.format,
|
||||
dateDebut: params.dateDebut,
|
||||
dateFin: params.dateFin,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Paramètres pour exporter les notifications
|
||||
class ExporterNotificationsParams {
|
||||
final String utilisateurId;
|
||||
final String format;
|
||||
final DateTime? dateDebut;
|
||||
final DateTime? dateFin;
|
||||
|
||||
const ExporterNotificationsParams({
|
||||
required this.utilisateurId,
|
||||
required this.format,
|
||||
this.dateDebut,
|
||||
this.dateFin,
|
||||
});
|
||||
|
||||
ExporterNotificationsParams copyWith({
|
||||
String? utilisateurId,
|
||||
String? format,
|
||||
DateTime? dateDebut,
|
||||
DateTime? dateFin,
|
||||
}) {
|
||||
return ExporterNotificationsParams(
|
||||
utilisateurId: utilisateurId ?? this.utilisateurId,
|
||||
format: format ?? this.format,
|
||||
dateDebut: dateDebut ?? this.dateDebut,
|
||||
dateFin: dateFin ?? this.dateFin,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ExporterNotificationsParams{utilisateurId: $utilisateurId, format: $format, dateDebut: $dateDebut, dateFin: $dateFin}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import '../../../../core/error/failures.dart';
|
||||
import '../../../../core/usecases/usecase.dart';
|
||||
import '../entities/notification.dart';
|
||||
import '../entities/preferences_notification.dart';
|
||||
import '../repositories/notifications_repository.dart';
|
||||
|
||||
/// Use case pour obtenir les préférences de notification
|
||||
class ObtenirPreferencesUseCase implements UseCase<PreferencesNotificationEntity, String> {
|
||||
final NotificationsRepository repository;
|
||||
|
||||
ObtenirPreferencesUseCase(this.repository);
|
||||
|
||||
@override
|
||||
Future<Either<Failure, PreferencesNotificationEntity>> call(String utilisateurId) async {
|
||||
return await repository.obtenirPreferences(utilisateurId);
|
||||
}
|
||||
}
|
||||
|
||||
/// Use case pour mettre à jour les préférences de notification
|
||||
class MettreAJourPreferencesUseCase implements UseCase<void, PreferencesNotificationEntity> {
|
||||
final NotificationsRepository repository;
|
||||
|
||||
MettreAJourPreferencesUseCase(this.repository);
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> call(PreferencesNotificationEntity preferences) async {
|
||||
return await repository.mettreAJourPreferences(preferences);
|
||||
}
|
||||
}
|
||||
|
||||
/// Use case pour réinitialiser les préférences
|
||||
class ReinitialiserPreferencesUseCase implements UseCase<PreferencesNotificationEntity, String> {
|
||||
final NotificationsRepository repository;
|
||||
|
||||
ReinitialiserPreferencesUseCase(this.repository);
|
||||
|
||||
@override
|
||||
Future<Either<Failure, PreferencesNotificationEntity>> call(String utilisateurId) async {
|
||||
return await repository.reinitialiserPreferences(utilisateurId);
|
||||
}
|
||||
}
|
||||
|
||||
/// Use case pour activer/désactiver un type de notification
|
||||
class ToggleTypeNotificationUseCase implements UseCase<void, ToggleTypeNotificationParams> {
|
||||
final NotificationsRepository repository;
|
||||
|
||||
ToggleTypeNotificationUseCase(this.repository);
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> call(ToggleTypeNotificationParams params) async {
|
||||
return await repository.toggleTypeNotification(
|
||||
params.utilisateurId,
|
||||
params.type,
|
||||
params.active,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Paramètres pour activer/désactiver un type de notification
|
||||
class ToggleTypeNotificationParams {
|
||||
final String utilisateurId;
|
||||
final TypeNotification type;
|
||||
final bool active;
|
||||
|
||||
const ToggleTypeNotificationParams({
|
||||
required this.utilisateurId,
|
||||
required this.type,
|
||||
required this.active,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ToggleTypeNotificationParams{utilisateurId: $utilisateurId, type: $type, active: $active}';
|
||||
}
|
||||
}
|
||||
|
||||
/// Use case pour activer/désactiver un canal de notification
|
||||
class ToggleCanalNotificationUseCase implements UseCase<void, ToggleCanalNotificationParams> {
|
||||
final NotificationsRepository repository;
|
||||
|
||||
ToggleCanalNotificationUseCase(this.repository);
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> call(ToggleCanalNotificationParams params) async {
|
||||
return await repository.toggleCanalNotification(
|
||||
params.utilisateurId,
|
||||
params.canal,
|
||||
params.active,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Paramètres pour activer/désactiver un canal de notification
|
||||
class ToggleCanalNotificationParams {
|
||||
final String utilisateurId;
|
||||
final CanalNotification canal;
|
||||
final bool active;
|
||||
|
||||
const ToggleCanalNotificationParams({
|
||||
required this.utilisateurId,
|
||||
required this.canal,
|
||||
required this.active,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ToggleCanalNotificationParams{utilisateurId: $utilisateurId, canal: $canal, active: $active}';
|
||||
}
|
||||
}
|
||||
|
||||
/// Use case pour configurer le mode silencieux
|
||||
class ConfigurerModeSilencieuxUseCase implements UseCase<void, ConfigurerModeSilencieuxParams> {
|
||||
final NotificationsRepository repository;
|
||||
|
||||
ConfigurerModeSilencieuxUseCase(this.repository);
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> call(ConfigurerModeSilencieuxParams params) async {
|
||||
return await repository.configurerModeSilencieux(
|
||||
params.utilisateurId,
|
||||
params.active,
|
||||
heureDebut: params.heureDebut,
|
||||
heureFin: params.heureFin,
|
||||
jours: params.jours,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Paramètres pour configurer le mode silencieux
|
||||
class ConfigurerModeSilencieuxParams {
|
||||
final String utilisateurId;
|
||||
final bool active;
|
||||
final String? heureDebut;
|
||||
final String? heureFin;
|
||||
final Set<int>? jours;
|
||||
|
||||
const ConfigurerModeSilencieuxParams({
|
||||
required this.utilisateurId,
|
||||
required this.active,
|
||||
this.heureDebut,
|
||||
this.heureFin,
|
||||
this.jours,
|
||||
});
|
||||
|
||||
ConfigurerModeSilencieuxParams copyWith({
|
||||
String? utilisateurId,
|
||||
bool? active,
|
||||
String? heureDebut,
|
||||
String? heureFin,
|
||||
Set<int>? jours,
|
||||
}) {
|
||||
return ConfigurerModeSilencieuxParams(
|
||||
utilisateurId: utilisateurId ?? this.utilisateurId,
|
||||
active: active ?? this.active,
|
||||
heureDebut: heureDebut ?? this.heureDebut,
|
||||
heureFin: heureFin ?? this.heureFin,
|
||||
jours: jours ?? this.jours,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ConfigurerModeSilencieuxParams{utilisateurId: $utilisateurId, active: $active, heureDebut: $heureDebut, heureFin: $heureFin, jours: $jours}';
|
||||
}
|
||||
}
|
||||
|
||||
/// Use case pour enregistrer le token FCM
|
||||
class EnregistrerTokenFCMUseCase implements UseCase<void, EnregistrerTokenFCMParams> {
|
||||
final NotificationsRepository repository;
|
||||
|
||||
EnregistrerTokenFCMUseCase(this.repository);
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> call(EnregistrerTokenFCMParams params) async {
|
||||
return await repository.enregistrerTokenFCM(
|
||||
params.utilisateurId,
|
||||
params.token,
|
||||
params.plateforme,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Paramètres pour enregistrer le token FCM
|
||||
class EnregistrerTokenFCMParams {
|
||||
final String utilisateurId;
|
||||
final String token;
|
||||
final String plateforme;
|
||||
|
||||
const EnregistrerTokenFCMParams({
|
||||
required this.utilisateurId,
|
||||
required this.token,
|
||||
required this.plateforme,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'EnregistrerTokenFCMParams{utilisateurId: $utilisateurId, token: $token, plateforme: $plateforme}';
|
||||
}
|
||||
}
|
||||
|
||||
/// Use case pour supprimer le token FCM
|
||||
class SupprimerTokenFCMUseCase implements UseCase<void, String> {
|
||||
final NotificationsRepository repository;
|
||||
|
||||
SupprimerTokenFCMUseCase(this.repository);
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> call(String utilisateurId) async {
|
||||
return await repository.supprimerTokenFCM(utilisateurId);
|
||||
}
|
||||
}
|
||||
|
||||
/// Use case pour s'abonner à un topic
|
||||
class AbonnerAuTopicUseCase implements UseCase<void, AbonnerAuTopicParams> {
|
||||
final NotificationsRepository repository;
|
||||
|
||||
AbonnerAuTopicUseCase(this.repository);
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> call(AbonnerAuTopicParams params) async {
|
||||
return await repository.abonnerAuTopic(
|
||||
params.utilisateurId,
|
||||
params.topic,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Paramètres pour s'abonner à un topic
|
||||
class AbonnerAuTopicParams {
|
||||
final String utilisateurId;
|
||||
final String topic;
|
||||
|
||||
const AbonnerAuTopicParams({
|
||||
required this.utilisateurId,
|
||||
required this.topic,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AbonnerAuTopicParams{utilisateurId: $utilisateurId, topic: $topic}';
|
||||
}
|
||||
}
|
||||
|
||||
/// Use case pour se désabonner d'un topic
|
||||
class DesabonnerDuTopicUseCase implements UseCase<void, DesabonnerDuTopicParams> {
|
||||
final NotificationsRepository repository;
|
||||
|
||||
DesabonnerDuTopicUseCase(this.repository);
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> call(DesabonnerDuTopicParams params) async {
|
||||
return await repository.desabonnerDuTopic(
|
||||
params.utilisateurId,
|
||||
params.topic,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Paramètres pour se désabonner d'un topic
|
||||
class DesabonnerDuTopicParams {
|
||||
final String utilisateurId;
|
||||
final String topic;
|
||||
|
||||
const DesabonnerDuTopicParams({
|
||||
required this.utilisateurId,
|
||||
required this.topic,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'DesabonnerDuTopicParams{utilisateurId: $utilisateurId, topic: $topic}';
|
||||
}
|
||||
}
|
||||
|
||||
/// Use case pour obtenir les topics auxquels l'utilisateur est abonné
|
||||
class ObtenirTopicsAbornesUseCase implements UseCase<List<String>, String> {
|
||||
final NotificationsRepository repository;
|
||||
|
||||
ObtenirTopicsAbornesUseCase(this.repository);
|
||||
|
||||
@override
|
||||
Future<Either<Failure, List<String>>> call(String utilisateurId) async {
|
||||
return await repository.obtenirTopicsAbornes(utilisateurId);
|
||||
}
|
||||
}
|
||||
|
||||
/// Use case pour configurer les préférences avancées
|
||||
class ConfigurerPreferencesAvanceesUseCase implements UseCase<void, ConfigurerPreferencesAvanceesParams> {
|
||||
final NotificationsRepository repository;
|
||||
|
||||
ConfigurerPreferencesAvanceesUseCase(this.repository);
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> call(ConfigurerPreferencesAvanceesParams params) async {
|
||||
// Récupération des préférences actuelles
|
||||
final preferencesResult = await repository.obtenirPreferences(params.utilisateurId);
|
||||
|
||||
return preferencesResult.fold(
|
||||
(failure) => Left(failure),
|
||||
(preferences) async {
|
||||
// Mise à jour des préférences avec les nouveaux paramètres
|
||||
final preferencesModifiees = preferences.copyWith(
|
||||
vibrationActivee: params.vibrationActivee ?? preferences.vibrationActivee,
|
||||
sonActive: params.sonActive ?? preferences.sonActive,
|
||||
ledActivee: params.ledActivee ?? preferences.ledActivee,
|
||||
sonPersonnalise: params.sonPersonnalise ?? preferences.sonPersonnalise,
|
||||
patternVibrationPersonnalise: params.patternVibrationPersonnalise ?? preferences.patternVibrationPersonnalise,
|
||||
couleurLEDPersonnalisee: params.couleurLEDPersonnalisee ?? preferences.couleurLEDPersonnalisee,
|
||||
apercuEcranVerrouillage: params.apercuEcranVerrouillage ?? preferences.apercuEcranVerrouillage,
|
||||
dureeAffichageSecondes: params.dureeAffichageSecondes ?? preferences.dureeAffichageSecondes,
|
||||
frequenceRegroupementMinutes: params.frequenceRegroupementMinutes ?? preferences.frequenceRegroupementMinutes,
|
||||
maxNotificationsSimultanees: params.maxNotificationsSimultanees ?? preferences.maxNotificationsSimultanees,
|
||||
marquageLectureAutomatique: params.marquageLectureAutomatique ?? preferences.marquageLectureAutomatique,
|
||||
delaiMarquageLectureSecondes: params.delaiMarquageLectureSecondes ?? preferences.delaiMarquageLectureSecondes,
|
||||
archivageAutomatique: params.archivageAutomatique ?? preferences.archivageAutomatique,
|
||||
delaiArchivageHeures: params.delaiArchivageHeures ?? preferences.delaiArchivageHeures,
|
||||
dureeConservationJours: params.dureeConservationJours ?? preferences.dureeConservationJours,
|
||||
);
|
||||
|
||||
return await repository.mettreAJourPreferences(preferencesModifiees);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Paramètres pour configurer les préférences avancées
|
||||
class ConfigurerPreferencesAvanceesParams {
|
||||
final String utilisateurId;
|
||||
final bool? vibrationActivee;
|
||||
final bool? sonActive;
|
||||
final bool? ledActivee;
|
||||
final String? sonPersonnalise;
|
||||
final List<int>? patternVibrationPersonnalise;
|
||||
final String? couleurLEDPersonnalisee;
|
||||
final bool? apercuEcranVerrouillage;
|
||||
final int? dureeAffichageSecondes;
|
||||
final int? frequenceRegroupementMinutes;
|
||||
final int? maxNotificationsSimultanees;
|
||||
final bool? marquageLectureAutomatique;
|
||||
final int? delaiMarquageLectureSecondes;
|
||||
final bool? archivageAutomatique;
|
||||
final int? delaiArchivageHeures;
|
||||
final int? dureeConservationJours;
|
||||
|
||||
const ConfigurerPreferencesAvanceesParams({
|
||||
required this.utilisateurId,
|
||||
this.vibrationActivee,
|
||||
this.sonActive,
|
||||
this.ledActivee,
|
||||
this.sonPersonnalise,
|
||||
this.patternVibrationPersonnalise,
|
||||
this.couleurLEDPersonnalisee,
|
||||
this.apercuEcranVerrouillage,
|
||||
this.dureeAffichageSecondes,
|
||||
this.frequenceRegroupementMinutes,
|
||||
this.maxNotificationsSimultanees,
|
||||
this.marquageLectureAutomatique,
|
||||
this.delaiMarquageLectureSecondes,
|
||||
this.archivageAutomatique,
|
||||
this.delaiArchivageHeures,
|
||||
this.dureeConservationJours,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ConfigurerPreferencesAvanceesParams{utilisateurId: $utilisateurId, vibrationActivee: $vibrationActivee, sonActive: $sonActive, ledActivee: $ledActivee, ...}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import '../../../../core/error/failures.dart';
|
||||
import '../../../../core/usecases/usecase.dart';
|
||||
import '../entities/notification.dart';
|
||||
import '../repositories/notifications_repository.dart';
|
||||
|
||||
/// Use case pour obtenir les notifications d'un utilisateur
|
||||
class ObtenirNotificationsUseCase implements UseCase<List<NotificationEntity>, ObtenirNotificationsParams> {
|
||||
final NotificationsRepository repository;
|
||||
|
||||
ObtenirNotificationsUseCase(this.repository);
|
||||
|
||||
@override
|
||||
Future<Either<Failure, List<NotificationEntity>>> call(ObtenirNotificationsParams params) async {
|
||||
// Vérification du cache en premier
|
||||
final cacheValide = await repository.isCacheValide(
|
||||
params.utilisateurId,
|
||||
maxAgeMinutes: params.maxAgeCacheMinutes,
|
||||
);
|
||||
|
||||
if (!cacheValide || params.forceRefresh) {
|
||||
// Synchronisation avec le serveur si nécessaire
|
||||
final syncResult = await repository.synchroniser(
|
||||
params.utilisateurId,
|
||||
forceSync: params.forceRefresh,
|
||||
);
|
||||
|
||||
// On continue même si la sync échoue (mode offline)
|
||||
if (syncResult.isLeft()) {
|
||||
// Log de l'erreur mais on continue avec les données en cache
|
||||
print('Erreur de synchronisation: ${syncResult.fold((l) => l.toString(), (r) => '')}');
|
||||
}
|
||||
}
|
||||
|
||||
// Récupération des notifications
|
||||
return await repository.obtenirNotifications(
|
||||
utilisateurId: params.utilisateurId,
|
||||
includeArchivees: params.includeArchivees,
|
||||
limite: params.limite,
|
||||
offset: params.offset,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Paramètres pour obtenir les notifications
|
||||
class ObtenirNotificationsParams {
|
||||
final String utilisateurId;
|
||||
final bool includeArchivees;
|
||||
final int limite;
|
||||
final int offset;
|
||||
final bool forceRefresh;
|
||||
final int maxAgeCacheMinutes;
|
||||
|
||||
const ObtenirNotificationsParams({
|
||||
required this.utilisateurId,
|
||||
this.includeArchivees = false,
|
||||
this.limite = 50,
|
||||
this.offset = 0,
|
||||
this.forceRefresh = false,
|
||||
this.maxAgeCacheMinutes = 5,
|
||||
});
|
||||
|
||||
ObtenirNotificationsParams copyWith({
|
||||
String? utilisateurId,
|
||||
bool? includeArchivees,
|
||||
int? limite,
|
||||
int? offset,
|
||||
bool? forceRefresh,
|
||||
int? maxAgeCacheMinutes,
|
||||
}) {
|
||||
return ObtenirNotificationsParams(
|
||||
utilisateurId: utilisateurId ?? this.utilisateurId,
|
||||
includeArchivees: includeArchivees ?? this.includeArchivees,
|
||||
limite: limite ?? this.limite,
|
||||
offset: offset ?? this.offset,
|
||||
forceRefresh: forceRefresh ?? this.forceRefresh,
|
||||
maxAgeCacheMinutes: maxAgeCacheMinutes ?? this.maxAgeCacheMinutes,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ObtenirNotificationsParams{utilisateurId: $utilisateurId, includeArchivees: $includeArchivees, limite: $limite, offset: $offset, forceRefresh: $forceRefresh}';
|
||||
}
|
||||
}
|
||||
|
||||
/// Use case pour obtenir les notifications non lues
|
||||
class ObtenirNotificationsNonLuesUseCase implements UseCase<List<NotificationEntity>, String> {
|
||||
final NotificationsRepository repository;
|
||||
|
||||
ObtenirNotificationsNonLuesUseCase(this.repository);
|
||||
|
||||
@override
|
||||
Future<Either<Failure, List<NotificationEntity>>> call(String utilisateurId) async {
|
||||
return await repository.obtenirNotificationsNonLues(utilisateurId);
|
||||
}
|
||||
}
|
||||
|
||||
/// Use case pour obtenir le nombre de notifications non lues
|
||||
class ObtenirNombreNonLuesUseCase implements UseCase<int, String> {
|
||||
final NotificationsRepository repository;
|
||||
|
||||
ObtenirNombreNonLuesUseCase(this.repository);
|
||||
|
||||
@override
|
||||
Future<Either<Failure, int>> call(String utilisateurId) async {
|
||||
return await repository.obtenirNombreNonLues(utilisateurId);
|
||||
}
|
||||
}
|
||||
|
||||
/// Use case pour rechercher des notifications
|
||||
class RechercherNotificationsUseCase implements UseCase<List<NotificationEntity>, RechercherNotificationsParams> {
|
||||
final NotificationsRepository repository;
|
||||
|
||||
RechercherNotificationsUseCase(this.repository);
|
||||
|
||||
@override
|
||||
Future<Either<Failure, List<NotificationEntity>>> call(RechercherNotificationsParams params) async {
|
||||
return await repository.rechercherNotifications(
|
||||
utilisateurId: params.utilisateurId,
|
||||
query: params.query,
|
||||
types: params.types,
|
||||
statuts: params.statuts,
|
||||
dateDebut: params.dateDebut,
|
||||
dateFin: params.dateFin,
|
||||
limite: params.limite,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Paramètres pour la recherche de notifications
|
||||
class RechercherNotificationsParams {
|
||||
final String utilisateurId;
|
||||
final String? query;
|
||||
final List<TypeNotification>? types;
|
||||
final List<StatutNotification>? statuts;
|
||||
final DateTime? dateDebut;
|
||||
final DateTime? dateFin;
|
||||
final int limite;
|
||||
|
||||
const RechercherNotificationsParams({
|
||||
required this.utilisateurId,
|
||||
this.query,
|
||||
this.types,
|
||||
this.statuts,
|
||||
this.dateDebut,
|
||||
this.dateFin,
|
||||
this.limite = 50,
|
||||
});
|
||||
|
||||
RechercherNotificationsParams copyWith({
|
||||
String? utilisateurId,
|
||||
String? query,
|
||||
List<TypeNotification>? types,
|
||||
List<StatutNotification>? statuts,
|
||||
DateTime? dateDebut,
|
||||
DateTime? dateFin,
|
||||
int? limite,
|
||||
}) {
|
||||
return RechercherNotificationsParams(
|
||||
utilisateurId: utilisateurId ?? this.utilisateurId,
|
||||
query: query ?? this.query,
|
||||
types: types ?? this.types,
|
||||
statuts: statuts ?? this.statuts,
|
||||
dateDebut: dateDebut ?? this.dateDebut,
|
||||
dateFin: dateFin ?? this.dateFin,
|
||||
limite: limite ?? this.limite,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'RechercherNotificationsParams{utilisateurId: $utilisateurId, query: $query, types: $types, statuts: $statuts, dateDebut: $dateDebut, dateFin: $dateFin, limite: $limite}';
|
||||
}
|
||||
}
|
||||
|
||||
/// Use case pour obtenir les notifications par type
|
||||
class ObtenirNotificationsParTypeUseCase implements UseCase<List<NotificationEntity>, ObtenirNotificationsParTypeParams> {
|
||||
final NotificationsRepository repository;
|
||||
|
||||
ObtenirNotificationsParTypeUseCase(this.repository);
|
||||
|
||||
@override
|
||||
Future<Either<Failure, List<NotificationEntity>>> call(ObtenirNotificationsParTypeParams params) async {
|
||||
return await repository.obtenirNotificationsParType(
|
||||
params.utilisateurId,
|
||||
params.type,
|
||||
limite: params.limite,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Paramètres pour obtenir les notifications par type
|
||||
class ObtenirNotificationsParTypeParams {
|
||||
final String utilisateurId;
|
||||
final TypeNotification type;
|
||||
final int limite;
|
||||
|
||||
const ObtenirNotificationsParTypeParams({
|
||||
required this.utilisateurId,
|
||||
required this.type,
|
||||
this.limite = 50,
|
||||
});
|
||||
|
||||
ObtenirNotificationsParTypeParams copyWith({
|
||||
String? utilisateurId,
|
||||
TypeNotification? type,
|
||||
int? limite,
|
||||
}) {
|
||||
return ObtenirNotificationsParTypeParams(
|
||||
utilisateurId: utilisateurId ?? this.utilisateurId,
|
||||
type: type ?? this.type,
|
||||
limite: limite ?? this.limite,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ObtenirNotificationsParTypeParams{utilisateurId: $utilisateurId, type: $type, limite: $limite}';
|
||||
}
|
||||
}
|
||||
|
||||
/// Use case pour obtenir les notifications importantes
|
||||
class ObtenirNotificationsImportantesUseCase implements UseCase<List<NotificationEntity>, String> {
|
||||
final NotificationsRepository repository;
|
||||
|
||||
ObtenirNotificationsImportantesUseCase(this.repository);
|
||||
|
||||
@override
|
||||
Future<Either<Failure, List<NotificationEntity>>> call(String utilisateurId) async {
|
||||
return await repository.obtenirNotificationsImportantes(utilisateurId);
|
||||
}
|
||||
}
|
||||
|
||||
/// Use case pour obtenir les statistiques des notifications
|
||||
class ObtenirStatistiquesNotificationsUseCase implements UseCase<Map<String, dynamic>, ObtenirStatistiquesParams> {
|
||||
final NotificationsRepository repository;
|
||||
|
||||
ObtenirStatistiquesNotificationsUseCase(this.repository);
|
||||
|
||||
@override
|
||||
Future<Either<Failure, Map<String, dynamic>>> call(ObtenirStatistiquesParams params) async {
|
||||
return await repository.obtenirStatistiques(
|
||||
params.utilisateurId,
|
||||
periode: params.periode,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Paramètres pour obtenir les statistiques
|
||||
class ObtenirStatistiquesParams {
|
||||
final String utilisateurId;
|
||||
final int periode;
|
||||
|
||||
const ObtenirStatistiquesParams({
|
||||
required this.utilisateurId,
|
||||
this.periode = 30,
|
||||
});
|
||||
|
||||
ObtenirStatistiquesParams copyWith({
|
||||
String? utilisateurId,
|
||||
int? periode,
|
||||
}) {
|
||||
return ObtenirStatistiquesParams(
|
||||
utilisateurId: utilisateurId ?? this.utilisateurId,
|
||||
periode: periode ?? this.periode,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ObtenirStatistiquesParams{utilisateurId: $utilisateurId, periode: $periode}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,779 @@
|
||||
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 '../../domain/entities/notification.dart';
|
||||
import '../../domain/entities/preferences_notification.dart';
|
||||
import '../bloc/notification_preferences_bloc.dart';
|
||||
import '../widgets/preference_section_widget.dart';
|
||||
import '../widgets/silent_mode_config_widget.dart';
|
||||
|
||||
/// Page de configuration des préférences de notifications
|
||||
class NotificationPreferencesPage extends StatefulWidget {
|
||||
const NotificationPreferencesPage({super.key});
|
||||
|
||||
@override
|
||||
State<NotificationPreferencesPage> createState() => _NotificationPreferencesPageState();
|
||||
}
|
||||
|
||||
class _NotificationPreferencesPageState extends State<NotificationPreferencesPage>
|
||||
with TickerProviderStateMixin {
|
||||
late TabController _tabController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabController = TabController(length: 3, vsync: this);
|
||||
|
||||
// Chargement des préférences
|
||||
context.read<NotificationPreferencesBloc>().add(
|
||||
const LoadPreferencesEvent(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return UnifiedPageLayout(
|
||||
title: 'Préférences de notifications',
|
||||
showBackButton: true,
|
||||
actions: [
|
||||
PopupMenuButton<String>(
|
||||
icon: const Icon(Icons.more_vert),
|
||||
onSelected: (value) {
|
||||
switch (value) {
|
||||
case 'reset':
|
||||
_showResetDialog();
|
||||
break;
|
||||
case 'test':
|
||||
_sendTestNotification();
|
||||
break;
|
||||
case 'export':
|
||||
_exportPreferences();
|
||||
break;
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
const PopupMenuItem(
|
||||
value: 'test',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.send),
|
||||
title: Text('Envoyer un test'),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
value: 'export',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.download),
|
||||
title: Text('Exporter'),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
const PopupMenuDivider(),
|
||||
const PopupMenuItem(
|
||||
value: 'reset',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.restore, color: Colors.red),
|
||||
title: Text('Réinitialiser', style: TextStyle(color: Colors.red)),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
body: BlocBuilder<NotificationPreferencesBloc, NotificationPreferencesState>(
|
||||
builder: (context, state) {
|
||||
if (state is PreferencesLoading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
if (state is PreferencesError) {
|
||||
return Center(
|
||||
child: UnifiedCard(
|
||||
variant: UnifiedCardVariant.outlined,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 48,
|
||||
color: AppColors.error,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Erreur de chargement',
|
||||
style: AppTextStyles.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
state.message,
|
||||
style: AppTextStyles.bodyMedium.copyWith(
|
||||
color: AppColors.onSurface.withOpacity(0.7),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
context.read<NotificationPreferencesBloc>().add(
|
||||
const LoadPreferencesEvent(),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: const Text('Réessayer'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (state is! PreferencesLoaded) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
// Onglets de navigation
|
||||
Container(
|
||||
margin: const EdgeInsets.all(16.0),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppColors.outline.withOpacity(0.2)),
|
||||
),
|
||||
child: TabBar(
|
||||
controller: _tabController,
|
||||
indicator: BoxDecoration(
|
||||
color: AppColors.primary,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
indicatorSize: TabBarIndicatorSize.tab,
|
||||
indicatorPadding: const EdgeInsets.all(4),
|
||||
labelColor: AppColors.onPrimary,
|
||||
unselectedLabelColor: AppColors.onSurface,
|
||||
labelStyle: AppTextStyles.bodyMedium.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
unselectedLabelStyle: AppTextStyles.bodyMedium,
|
||||
tabs: const [
|
||||
Tab(text: 'Général'),
|
||||
Tab(text: 'Types'),
|
||||
Tab(text: 'Avancé'),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Contenu des onglets
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
_buildGeneralTab(state.preferences),
|
||||
_buildTypesTab(state.preferences),
|
||||
_buildAdvancedTab(state.preferences),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGeneralTab(PreferencesNotificationEntity preferences) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Activation générale
|
||||
PreferenceSectionWidget(
|
||||
title: 'Notifications',
|
||||
subtitle: 'Paramètres généraux des notifications',
|
||||
icon: Icons.notifications,
|
||||
children: [
|
||||
SwitchListTile(
|
||||
title: const Text('Activer les notifications'),
|
||||
subtitle: const Text('Recevoir toutes les notifications'),
|
||||
value: preferences.notificationsActivees,
|
||||
onChanged: (value) => _updatePreference(
|
||||
preferences.copyWith(notificationsActivees: value),
|
||||
),
|
||||
),
|
||||
|
||||
if (preferences.notificationsActivees) ...[
|
||||
SwitchListTile(
|
||||
title: const Text('Notifications push'),
|
||||
subtitle: const Text('Recevoir les notifications sur l\'appareil'),
|
||||
value: preferences.pushActivees,
|
||||
onChanged: (value) => _updatePreference(
|
||||
preferences.copyWith(pushActivees: value),
|
||||
),
|
||||
),
|
||||
|
||||
SwitchListTile(
|
||||
title: const Text('Notifications par email'),
|
||||
subtitle: const Text('Recevoir les notifications par email'),
|
||||
value: preferences.emailActivees,
|
||||
onChanged: (value) => _updatePreference(
|
||||
preferences.copyWith(emailActivees: value),
|
||||
),
|
||||
),
|
||||
|
||||
SwitchListTile(
|
||||
title: const Text('Notifications SMS'),
|
||||
subtitle: const Text('Recevoir les notifications par SMS'),
|
||||
value: preferences.smsActivees,
|
||||
onChanged: (value) => _updatePreference(
|
||||
preferences.copyWith(smsActivees: value),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Mode silencieux
|
||||
PreferenceSectionWidget(
|
||||
title: 'Mode silencieux',
|
||||
subtitle: 'Configurer les périodes de silence',
|
||||
icon: Icons.do_not_disturb,
|
||||
children: [
|
||||
SilentModeConfigWidget(
|
||||
preferences: preferences,
|
||||
onPreferencesChanged: _updatePreference,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Paramètres visuels et sonores
|
||||
PreferenceSectionWidget(
|
||||
title: 'Apparence et sons',
|
||||
subtitle: 'Personnaliser l\'affichage des notifications',
|
||||
icon: Icons.palette,
|
||||
children: [
|
||||
SwitchListTile(
|
||||
title: const Text('Vibration'),
|
||||
subtitle: const Text('Faire vibrer l\'appareil'),
|
||||
value: preferences.vibrationActivee,
|
||||
onChanged: (value) => _updatePreference(
|
||||
preferences.copyWith(vibrationActivee: value),
|
||||
),
|
||||
),
|
||||
|
||||
SwitchListTile(
|
||||
title: const Text('Son'),
|
||||
subtitle: const Text('Jouer un son'),
|
||||
value: preferences.sonActive,
|
||||
onChanged: (value) => _updatePreference(
|
||||
preferences.copyWith(sonActive: value),
|
||||
),
|
||||
),
|
||||
|
||||
SwitchListTile(
|
||||
title: const Text('LED'),
|
||||
subtitle: const Text('Allumer la LED de notification'),
|
||||
value: preferences.ledActivee,
|
||||
onChanged: (value) => _updatePreference(
|
||||
preferences.copyWith(ledActivee: value),
|
||||
),
|
||||
),
|
||||
|
||||
SwitchListTile(
|
||||
title: const Text('Aperçu sur écran verrouillé'),
|
||||
subtitle: const Text('Afficher le contenu sur l\'écran verrouillé'),
|
||||
value: preferences.apercuEcranVerrouillage,
|
||||
onChanged: (value) => _updatePreference(
|
||||
preferences.copyWith(apercuEcranVerrouillage: value),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTypesTab(PreferencesNotificationEntity preferences) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Choisissez les types de notifications que vous souhaitez recevoir',
|
||||
style: AppTextStyles.bodyMedium.copyWith(
|
||||
color: AppColors.onSurface.withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Groupement par catégorie
|
||||
..._buildTypesByCategory(preferences),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAdvancedTab(PreferencesNotificationEntity preferences) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Gestion automatique
|
||||
PreferenceSectionWidget(
|
||||
title: 'Gestion automatique',
|
||||
subtitle: 'Paramètres de gestion automatique des notifications',
|
||||
icon: Icons.auto_mode,
|
||||
children: [
|
||||
SwitchListTile(
|
||||
title: const Text('Marquage automatique comme lu'),
|
||||
subtitle: const Text('Marquer automatiquement les notifications comme lues'),
|
||||
value: preferences.marquageLectureAutomatique,
|
||||
onChanged: (value) => _updatePreference(
|
||||
preferences.copyWith(marquageLectureAutomatique: value),
|
||||
),
|
||||
),
|
||||
|
||||
if (preferences.marquageLectureAutomatique)
|
||||
ListTile(
|
||||
title: const Text('Délai de marquage'),
|
||||
subtitle: Text('${preferences.delaiMarquageLectureSecondes ?? 5} secondes'),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => _showDelayPicker(
|
||||
'Délai de marquage automatique',
|
||||
preferences.delaiMarquageLectureSecondes ?? 5,
|
||||
(value) => _updatePreference(
|
||||
preferences.copyWith(delaiMarquageLectureSecondes: value),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
SwitchListTile(
|
||||
title: const Text('Archivage automatique'),
|
||||
subtitle: const Text('Archiver automatiquement les notifications lues'),
|
||||
value: preferences.archivageAutomatique,
|
||||
onChanged: (value) => _updatePreference(
|
||||
preferences.copyWith(archivageAutomatique: value),
|
||||
),
|
||||
),
|
||||
|
||||
if (preferences.archivageAutomatique)
|
||||
ListTile(
|
||||
title: const Text('Délai d\'archivage'),
|
||||
subtitle: Text('${preferences.delaiArchivageHeures} heures'),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => _showDelayPicker(
|
||||
'Délai d\'archivage automatique',
|
||||
preferences.delaiArchivageHeures,
|
||||
(value) => _updatePreference(
|
||||
preferences.copyWith(delaiArchivageHeures: value),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Limites et regroupement
|
||||
PreferenceSectionWidget(
|
||||
title: 'Limites et regroupement',
|
||||
subtitle: 'Contrôler le nombre et le regroupement des notifications',
|
||||
icon: Icons.group_work,
|
||||
children: [
|
||||
ListTile(
|
||||
title: const Text('Notifications simultanées maximum'),
|
||||
subtitle: Text('${preferences.maxNotificationsSimultanees} notifications'),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => _showNumberPicker(
|
||||
'Nombre maximum de notifications simultanées',
|
||||
preferences.maxNotificationsSimultanees,
|
||||
1,
|
||||
50,
|
||||
(value) => _updatePreference(
|
||||
preferences.copyWith(maxNotificationsSimultanees: value),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
ListTile(
|
||||
title: const Text('Fréquence de regroupement'),
|
||||
subtitle: Text('${preferences.frequenceRegroupementMinutes} minutes'),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => _showNumberPicker(
|
||||
'Fréquence de regroupement des notifications',
|
||||
preferences.frequenceRegroupementMinutes,
|
||||
1,
|
||||
60,
|
||||
(value) => _updatePreference(
|
||||
preferences.copyWith(frequenceRegroupementMinutes: value),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
ListTile(
|
||||
title: const Text('Durée d\'affichage'),
|
||||
subtitle: Text('${preferences.dureeAffichageSecondes} secondes'),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => _showNumberPicker(
|
||||
'Durée d\'affichage des notifications',
|
||||
preferences.dureeAffichageSecondes,
|
||||
3,
|
||||
30,
|
||||
(value) => _updatePreference(
|
||||
preferences.copyWith(dureeAffichageSecondes: value),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Conservation des données
|
||||
PreferenceSectionWidget(
|
||||
title: 'Conservation des données',
|
||||
subtitle: 'Durée de conservation des notifications',
|
||||
icon: Icons.storage,
|
||||
children: [
|
||||
ListTile(
|
||||
title: const Text('Durée de conservation'),
|
||||
subtitle: Text('${preferences.dureeConservationJours} jours'),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => _showNumberPicker(
|
||||
'Durée de conservation des notifications',
|
||||
preferences.dureeConservationJours,
|
||||
7,
|
||||
365,
|
||||
(value) => _updatePreference(
|
||||
preferences.copyWith(dureeConservationJours: value),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
SwitchListTile(
|
||||
title: const Text('Affichage de l\'historique'),
|
||||
subtitle: const Text('Conserver l\'historique des notifications'),
|
||||
value: preferences.affichageHistorique,
|
||||
onChanged: (value) => _updatePreference(
|
||||
preferences.copyWith(affichageHistorique: value),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildTypesByCategory(PreferencesNotificationEntity preferences) {
|
||||
final typesByCategory = <String, List<TypeNotification>>{};
|
||||
|
||||
for (final type in TypeNotification.values) {
|
||||
typesByCategory.putIfAbsent(type.categorie, () => []).add(type);
|
||||
}
|
||||
|
||||
return typesByCategory.entries.map((entry) {
|
||||
return PreferenceSectionWidget(
|
||||
title: _getCategoryTitle(entry.key),
|
||||
subtitle: _getCategorySubtitle(entry.key),
|
||||
icon: _getCategoryIcon(entry.key),
|
||||
children: entry.value.map((type) {
|
||||
return SwitchListTile(
|
||||
title: Text(type.libelle),
|
||||
subtitle: Text(_getTypeDescription(type)),
|
||||
value: preferences.isTypeActive(type),
|
||||
onChanged: (value) => _toggleNotificationType(type, value),
|
||||
secondary: Icon(
|
||||
_getTypeIconData(type),
|
||||
color: _getTypeColor(type),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
String _getCategoryTitle(String category) {
|
||||
switch (category) {
|
||||
case 'evenements':
|
||||
return 'Événements';
|
||||
case 'cotisations':
|
||||
return 'Cotisations';
|
||||
case 'solidarite':
|
||||
return 'Solidarité';
|
||||
case 'membres':
|
||||
return 'Membres';
|
||||
case 'organisation':
|
||||
return 'Organisation';
|
||||
case 'messages':
|
||||
return 'Messages';
|
||||
case 'systeme':
|
||||
return 'Système';
|
||||
default:
|
||||
return category;
|
||||
}
|
||||
}
|
||||
|
||||
String _getCategorySubtitle(String category) {
|
||||
switch (category) {
|
||||
case 'evenements':
|
||||
return 'Notifications liées aux événements';
|
||||
case 'cotisations':
|
||||
return 'Notifications de paiement et cotisations';
|
||||
case 'solidarite':
|
||||
return 'Demandes d\'aide et solidarité';
|
||||
case 'membres':
|
||||
return 'Nouveaux membres et anniversaires';
|
||||
case 'organisation':
|
||||
return 'Annonces et réunions';
|
||||
case 'messages':
|
||||
return 'Messages privés et mentions';
|
||||
case 'systeme':
|
||||
return 'Mises à jour et maintenance';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
IconData _getCategoryIcon(String category) {
|
||||
switch (category) {
|
||||
case 'evenements':
|
||||
return Icons.event;
|
||||
case 'cotisations':
|
||||
return Icons.payment;
|
||||
case 'solidarite':
|
||||
return Icons.volunteer_activism;
|
||||
case 'membres':
|
||||
return Icons.people;
|
||||
case 'organisation':
|
||||
return Icons.business;
|
||||
case 'messages':
|
||||
return Icons.message;
|
||||
case 'systeme':
|
||||
return Icons.settings;
|
||||
default:
|
||||
return Icons.notifications;
|
||||
}
|
||||
}
|
||||
|
||||
String _getTypeDescription(TypeNotification type) {
|
||||
// Descriptions courtes pour chaque type
|
||||
switch (type) {
|
||||
case TypeNotification.nouvelEvenement:
|
||||
return 'Nouveaux événements créés';
|
||||
case TypeNotification.rappelEvenement:
|
||||
return 'Rappels avant les événements';
|
||||
case TypeNotification.cotisationDue:
|
||||
return 'Échéances de cotisations';
|
||||
case TypeNotification.cotisationPayee:
|
||||
return 'Confirmations de paiement';
|
||||
case TypeNotification.nouvelleDemandeAide:
|
||||
return 'Nouvelles demandes d\'aide';
|
||||
case TypeNotification.nouveauMembre:
|
||||
return 'Nouveaux membres rejoignant';
|
||||
case TypeNotification.anniversaireMembre:
|
||||
return 'Anniversaires des membres';
|
||||
case TypeNotification.annonceGenerale:
|
||||
return 'Annonces importantes';
|
||||
case TypeNotification.messagePrive:
|
||||
return 'Messages privés reçus';
|
||||
default:
|
||||
return type.libelle;
|
||||
}
|
||||
}
|
||||
|
||||
IconData _getTypeIconData(TypeNotification type) {
|
||||
switch (type.icone) {
|
||||
case 'event':
|
||||
return Icons.event;
|
||||
case 'payment':
|
||||
return Icons.payment;
|
||||
case 'help':
|
||||
return Icons.help;
|
||||
case 'person_add':
|
||||
return Icons.person_add;
|
||||
case 'cake':
|
||||
return Icons.cake;
|
||||
case 'campaign':
|
||||
return Icons.campaign;
|
||||
case 'mail':
|
||||
return Icons.mail;
|
||||
default:
|
||||
return Icons.notifications;
|
||||
}
|
||||
}
|
||||
|
||||
Color _getTypeColor(TypeNotification type) {
|
||||
try {
|
||||
return Color(int.parse(type.couleur.replaceFirst('#', '0xFF')));
|
||||
} catch (e) {
|
||||
return AppColors.primary;
|
||||
}
|
||||
}
|
||||
|
||||
void _updatePreference(PreferencesNotificationEntity preferences) {
|
||||
context.read<NotificationPreferencesBloc>().add(
|
||||
UpdatePreferencesEvent(preferences: preferences),
|
||||
);
|
||||
}
|
||||
|
||||
void _toggleNotificationType(TypeNotification type, bool active) {
|
||||
context.read<NotificationPreferencesBloc>().add(
|
||||
ToggleNotificationTypeEvent(type: type, active: active),
|
||||
);
|
||||
}
|
||||
|
||||
void _showResetDialog() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Réinitialiser les préférences'),
|
||||
content: const Text(
|
||||
'Êtes-vous sûr de vouloir réinitialiser toutes vos préférences '
|
||||
'de notifications aux valeurs par défaut ?',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
context.read<NotificationPreferencesBloc>().add(
|
||||
const ResetPreferencesEvent(),
|
||||
);
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: AppColors.error,
|
||||
),
|
||||
child: const Text('Réinitialiser'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _sendTestNotification() {
|
||||
context.read<NotificationPreferencesBloc>().add(
|
||||
const SendTestNotificationEvent(type: TypeNotification.annonceGenerale),
|
||||
);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Notification de test envoyée'),
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _exportPreferences() {
|
||||
context.read<NotificationPreferencesBloc>().add(
|
||||
const ExportPreferencesEvent(),
|
||||
);
|
||||
}
|
||||
|
||||
void _showDelayPicker(String title, int currentValue, Function(int) onChanged) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(title),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('Valeur actuelle: $currentValue secondes'),
|
||||
const SizedBox(height: 16),
|
||||
// Ici vous pourriez ajouter un slider ou un picker
|
||||
// Pour simplifier, on utilise des boutons prédéfinis
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
children: [5, 10, 15, 30, 60].map((value) {
|
||||
return ChoiceChip(
|
||||
label: Text('${value}s'),
|
||||
selected: currentValue == value,
|
||||
onSelected: (selected) {
|
||||
if (selected) {
|
||||
onChanged(value);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showNumberPicker(
|
||||
String title,
|
||||
int currentValue,
|
||||
int min,
|
||||
int max,
|
||||
Function(int) onChanged,
|
||||
) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(title),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('Valeur actuelle: $currentValue'),
|
||||
const SizedBox(height: 16),
|
||||
// Slider pour choisir la valeur
|
||||
Slider(
|
||||
value: currentValue.toDouble(),
|
||||
min: min.toDouble(),
|
||||
max: max.toDouble(),
|
||||
divisions: max - min,
|
||||
label: currentValue.toString(),
|
||||
onChanged: (value) {
|
||||
// Mise à jour en temps réel
|
||||
},
|
||||
onChangeEnd: (value) {
|
||||
onChanged(value.round());
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,539 @@
|
||||
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 '../../domain/entities/notification.dart';
|
||||
import '../bloc/notifications_bloc.dart';
|
||||
import '../widgets/notification_card_widget.dart';
|
||||
import '../widgets/notification_filter_widget.dart';
|
||||
import '../widgets/notification_search_widget.dart';
|
||||
import '../widgets/notification_stats_widget.dart';
|
||||
|
||||
/// Page principale du centre de notifications
|
||||
class NotificationsCenterPage extends StatefulWidget {
|
||||
const NotificationsCenterPage({super.key});
|
||||
|
||||
@override
|
||||
State<NotificationsCenterPage> createState() => _NotificationsCenterPageState();
|
||||
}
|
||||
|
||||
class _NotificationsCenterPageState extends State<NotificationsCenterPage>
|
||||
with TickerProviderStateMixin {
|
||||
late TabController _tabController;
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
bool _showSearch = false;
|
||||
String _searchQuery = '';
|
||||
Set<TypeNotification> _selectedTypes = {};
|
||||
Set<StatutNotification> _selectedStatuts = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabController = TabController(length: 4, vsync: this);
|
||||
_scrollController.addListener(_onScroll);
|
||||
|
||||
// Chargement initial des notifications
|
||||
context.read<NotificationsBloc>().add(
|
||||
const LoadNotificationsEvent(forceRefresh: false),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onScroll() {
|
||||
if (_scrollController.position.pixels >=
|
||||
_scrollController.position.maxScrollExtent - 200) {
|
||||
// Chargement de plus de notifications (pagination)
|
||||
context.read<NotificationsBloc>().add(
|
||||
const LoadMoreNotificationsEvent(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onRefresh() {
|
||||
context.read<NotificationsBloc>().add(
|
||||
const LoadNotificationsEvent(forceRefresh: true),
|
||||
);
|
||||
}
|
||||
|
||||
void _toggleSearch() {
|
||||
setState(() {
|
||||
_showSearch = !_showSearch;
|
||||
if (!_showSearch) {
|
||||
_searchQuery = '';
|
||||
_applyFilters();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _onSearchChanged(String query) {
|
||||
setState(() {
|
||||
_searchQuery = query;
|
||||
});
|
||||
_applyFilters();
|
||||
}
|
||||
|
||||
void _onFiltersChanged({
|
||||
Set<TypeNotification>? types,
|
||||
Set<StatutNotification>? statuts,
|
||||
}) {
|
||||
setState(() {
|
||||
if (types != null) _selectedTypes = types;
|
||||
if (statuts != null) _selectedStatuts = statuts;
|
||||
});
|
||||
_applyFilters();
|
||||
}
|
||||
|
||||
void _applyFilters() {
|
||||
context.read<NotificationsBloc>().add(
|
||||
SearchNotificationsEvent(
|
||||
query: _searchQuery.isEmpty ? null : _searchQuery,
|
||||
types: _selectedTypes.isEmpty ? null : _selectedTypes.toList(),
|
||||
statuts: _selectedStatuts.isEmpty ? null : _selectedStatuts.toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _markAllAsRead() {
|
||||
context.read<NotificationsBloc>().add(
|
||||
const MarkAllAsReadEvent(),
|
||||
);
|
||||
}
|
||||
|
||||
void _archiveAllRead() {
|
||||
context.read<NotificationsBloc>().add(
|
||||
const ArchiveAllReadEvent(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return UnifiedPageLayout(
|
||||
title: 'Notifications',
|
||||
showBackButton: true,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(_showSearch ? Icons.search_off : Icons.search),
|
||||
onPressed: _toggleSearch,
|
||||
tooltip: _showSearch ? 'Fermer la recherche' : 'Rechercher',
|
||||
),
|
||||
PopupMenuButton<String>(
|
||||
icon: const Icon(Icons.more_vert),
|
||||
onSelected: (value) {
|
||||
switch (value) {
|
||||
case 'mark_all_read':
|
||||
_markAllAsRead();
|
||||
break;
|
||||
case 'archive_all_read':
|
||||
_archiveAllRead();
|
||||
break;
|
||||
case 'preferences':
|
||||
Navigator.pushNamed(context, '/notifications/preferences');
|
||||
break;
|
||||
case 'export':
|
||||
Navigator.pushNamed(context, '/notifications/export');
|
||||
break;
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
const PopupMenuItem(
|
||||
value: 'mark_all_read',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.mark_email_read),
|
||||
title: Text('Tout marquer comme lu'),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
value: 'archive_all_read',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.archive),
|
||||
title: Text('Archiver tout lu'),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
const PopupMenuDivider(),
|
||||
const PopupMenuItem(
|
||||
value: 'preferences',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.settings),
|
||||
title: Text('Préférences'),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
value: 'export',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.download),
|
||||
title: Text('Exporter'),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
body: Column(
|
||||
children: [
|
||||
// Barre de recherche (conditionnelle)
|
||||
if (_showSearch)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: NotificationSearchWidget(
|
||||
onSearchChanged: _onSearchChanged,
|
||||
onFiltersChanged: _onFiltersChanged,
|
||||
selectedTypes: _selectedTypes,
|
||||
selectedStatuts: _selectedStatuts,
|
||||
),
|
||||
),
|
||||
|
||||
// Statistiques rapides
|
||||
BlocBuilder<NotificationsBloc, NotificationsState>(
|
||||
builder: (context, state) {
|
||||
if (state is NotificationsLoaded) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: NotificationStatsWidget(
|
||||
totalCount: state.notifications.length,
|
||||
unreadCount: state.unreadCount,
|
||||
importantCount: state.notifications
|
||||
.where((n) => n.estImportante)
|
||||
.length,
|
||||
),
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Onglets de filtrage
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppColors.outline.withOpacity(0.2)),
|
||||
),
|
||||
child: TabBar(
|
||||
controller: _tabController,
|
||||
indicator: BoxDecoration(
|
||||
color: AppColors.primary,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
indicatorSize: TabBarIndicatorSize.tab,
|
||||
indicatorPadding: const EdgeInsets.all(4),
|
||||
labelColor: AppColors.onPrimary,
|
||||
unselectedLabelColor: AppColors.onSurface,
|
||||
labelStyle: AppTextStyles.bodyMedium.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
unselectedLabelStyle: AppTextStyles.bodyMedium,
|
||||
tabs: const [
|
||||
Tab(text: 'Toutes'),
|
||||
Tab(text: 'Non lues'),
|
||||
Tab(text: 'Importantes'),
|
||||
Tab(text: 'Archivées'),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Liste des notifications
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
_buildNotificationsList(NotificationFilter.all),
|
||||
_buildNotificationsList(NotificationFilter.unread),
|
||||
_buildNotificationsList(NotificationFilter.important),
|
||||
_buildNotificationsList(NotificationFilter.archived),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNotificationsList(NotificationFilter filter) {
|
||||
return BlocBuilder<NotificationsBloc, NotificationsState>(
|
||||
builder: (context, state) {
|
||||
if (state is NotificationsLoading && state.notifications.isEmpty) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
if (state is NotificationsError && state.notifications.isEmpty) {
|
||||
return Center(
|
||||
child: UnifiedCard(
|
||||
variant: UnifiedCardVariant.outlined,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 48,
|
||||
color: AppColors.error,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Erreur de chargement',
|
||||
style: AppTextStyles.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
state.message,
|
||||
style: AppTextStyles.bodyMedium.copyWith(
|
||||
color: AppColors.onSurface.withOpacity(0.7),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _onRefresh,
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: const Text('Réessayer'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final notifications = _filterNotifications(
|
||||
state.notifications,
|
||||
filter,
|
||||
);
|
||||
|
||||
if (notifications.isEmpty) {
|
||||
return Center(
|
||||
child: UnifiedCard(
|
||||
variant: UnifiedCardVariant.outlined,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
_getEmptyIcon(filter),
|
||||
size: 48,
|
||||
color: AppColors.onSurface.withOpacity(0.5),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
_getEmptyTitle(filter),
|
||||
style: AppTextStyles.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
_getEmptyMessage(filter),
|
||||
style: AppTextStyles.bodyMedium.copyWith(
|
||||
color: AppColors.onSurface.withOpacity(0.7),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async => _onRefresh(),
|
||||
child: ListView.builder(
|
||||
controller: _scrollController,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
itemCount: notifications.length + (state.hasMore ? 1 : 0),
|
||||
itemBuilder: (context, index) {
|
||||
if (index >= notifications.length) {
|
||||
// Indicateur de chargement pour la pagination
|
||||
return const Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final notification = notifications[index];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: NotificationCardWidget(
|
||||
notification: notification,
|
||||
onTap: () => _onNotificationTap(notification),
|
||||
onMarkAsRead: () => _onMarkAsRead(notification),
|
||||
onMarkAsImportant: () => _onMarkAsImportant(notification),
|
||||
onArchive: () => _onArchive(notification),
|
||||
onDelete: () => _onDelete(notification),
|
||||
onActionTap: (action) => _onActionTap(notification, action),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
List<NotificationEntity> _filterNotifications(
|
||||
List<NotificationEntity> notifications,
|
||||
NotificationFilter filter,
|
||||
) {
|
||||
switch (filter) {
|
||||
case NotificationFilter.all:
|
||||
return notifications.where((n) => !n.estArchivee).toList();
|
||||
case NotificationFilter.unread:
|
||||
return notifications.where((n) => !n.estLue && !n.estArchivee).toList();
|
||||
case NotificationFilter.important:
|
||||
return notifications.where((n) => n.estImportante && !n.estArchivee).toList();
|
||||
case NotificationFilter.archived:
|
||||
return notifications.where((n) => n.estArchivee).toList();
|
||||
}
|
||||
}
|
||||
|
||||
IconData _getEmptyIcon(NotificationFilter filter) {
|
||||
switch (filter) {
|
||||
case NotificationFilter.all:
|
||||
return Icons.notifications_none;
|
||||
case NotificationFilter.unread:
|
||||
return Icons.mark_email_read;
|
||||
case NotificationFilter.important:
|
||||
return Icons.star_border;
|
||||
case NotificationFilter.archived:
|
||||
return Icons.archive;
|
||||
}
|
||||
}
|
||||
|
||||
String _getEmptyTitle(NotificationFilter filter) {
|
||||
switch (filter) {
|
||||
case NotificationFilter.all:
|
||||
return 'Aucune notification';
|
||||
case NotificationFilter.unread:
|
||||
return 'Tout est lu !';
|
||||
case NotificationFilter.important:
|
||||
return 'Aucune notification importante';
|
||||
case NotificationFilter.archived:
|
||||
return 'Aucune notification archivée';
|
||||
}
|
||||
}
|
||||
|
||||
String _getEmptyMessage(NotificationFilter filter) {
|
||||
switch (filter) {
|
||||
case NotificationFilter.all:
|
||||
return 'Vous n\'avez encore reçu aucune notification.';
|
||||
case NotificationFilter.unread:
|
||||
return 'Toutes vos notifications ont été lues.';
|
||||
case NotificationFilter.important:
|
||||
return 'Vous n\'avez aucune notification marquée comme importante.';
|
||||
case NotificationFilter.archived:
|
||||
return 'Vous n\'avez aucune notification archivée.';
|
||||
}
|
||||
}
|
||||
|
||||
void _onNotificationTap(NotificationEntity notification) {
|
||||
// Marquer comme lue si pas encore lue
|
||||
if (!notification.estLue) {
|
||||
_onMarkAsRead(notification);
|
||||
}
|
||||
|
||||
// Navigation vers le détail ou action par défaut
|
||||
if (notification.actionClic != null) {
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
notification.actionClic!,
|
||||
arguments: notification.parametresAction,
|
||||
);
|
||||
} else {
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
'/notifications/detail',
|
||||
arguments: notification.id,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onMarkAsRead(NotificationEntity notification) {
|
||||
context.read<NotificationsBloc>().add(
|
||||
MarkAsReadEvent(notificationId: notification.id),
|
||||
);
|
||||
}
|
||||
|
||||
void _onMarkAsImportant(NotificationEntity notification) {
|
||||
context.read<NotificationsBloc>().add(
|
||||
MarkAsImportantEvent(
|
||||
notificationId: notification.id,
|
||||
important: !notification.estImportante,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onArchive(NotificationEntity notification) {
|
||||
context.read<NotificationsBloc>().add(
|
||||
ArchiveNotificationEvent(notificationId: notification.id),
|
||||
);
|
||||
}
|
||||
|
||||
void _onDelete(NotificationEntity notification) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Supprimer la notification'),
|
||||
content: const Text(
|
||||
'Êtes-vous sûr de vouloir supprimer cette notification ? '
|
||||
'Cette action est irréversible.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
context.read<NotificationsBloc>().add(
|
||||
DeleteNotificationEvent(notificationId: notification.id),
|
||||
);
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: AppColors.error,
|
||||
),
|
||||
child: const Text('Supprimer'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onActionTap(NotificationEntity notification, ActionNotification action) {
|
||||
context.read<NotificationsBloc>().add(
|
||||
ExecuteQuickActionEvent(
|
||||
notificationId: notification.id,
|
||||
actionId: action.id,
|
||||
parameters: action.parametres,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Énumération des filtres de notification
|
||||
enum NotificationFilter {
|
||||
all,
|
||||
unread,
|
||||
important,
|
||||
archived,
|
||||
}
|
||||
@@ -0,0 +1,430 @@
|
||||
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/notification.dart';
|
||||
|
||||
/// Widget de carte pour afficher une notification
|
||||
class NotificationCardWidget extends StatelessWidget {
|
||||
final NotificationEntity notification;
|
||||
final VoidCallback? onTap;
|
||||
final VoidCallback? onMarkAsRead;
|
||||
final VoidCallback? onMarkAsImportant;
|
||||
final VoidCallback? onArchive;
|
||||
final VoidCallback? onDelete;
|
||||
final Function(ActionNotification)? onActionTap;
|
||||
|
||||
const NotificationCardWidget({
|
||||
super.key,
|
||||
required this.notification,
|
||||
this.onTap,
|
||||
this.onMarkAsRead,
|
||||
this.onMarkAsImportant,
|
||||
this.onArchive,
|
||||
this.onDelete,
|
||||
this.onActionTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isUnread = !notification.estLue;
|
||||
final isImportant = notification.estImportante;
|
||||
final isExpired = notification.isExpiree;
|
||||
|
||||
return UnifiedCard(
|
||||
variant: isUnread ? UnifiedCardVariant.elevated : UnifiedCardVariant.outlined,
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: isUnread
|
||||
? Border.left(
|
||||
color: _getTypeColor(),
|
||||
width: 4,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// En-tête avec icône, titre et actions
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Icône du type de notification
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: _getTypeColor().withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
_getTypeIcon(),
|
||||
color: _getTypeColor(),
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// Titre et métadonnées
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
notification.titre,
|
||||
style: AppTextStyles.titleSmall.copyWith(
|
||||
fontWeight: isUnread ? FontWeight.w600 : FontWeight.w500,
|
||||
color: isExpired
|
||||
? AppColors.onSurface.withOpacity(0.6)
|
||||
: AppColors.onSurface,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
|
||||
// Badges de statut
|
||||
if (isImportant) ...[
|
||||
const SizedBox(width: 8),
|
||||
Icon(
|
||||
Icons.star,
|
||||
color: AppColors.warning,
|
||||
size: 16,
|
||||
),
|
||||
],
|
||||
|
||||
if (isUnread) ...[
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primary,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 4),
|
||||
|
||||
// Métadonnées (expéditeur, date)
|
||||
Row(
|
||||
children: [
|
||||
if (notification.expediteurNom != null) ...[
|
||||
Text(
|
||||
notification.expediteurNom!,
|
||||
style: AppTextStyles.bodySmall.copyWith(
|
||||
color: AppColors.onSurface.withOpacity(0.7),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
' • ',
|
||||
style: AppTextStyles.bodySmall.copyWith(
|
||||
color: AppColors.onSurface.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
Text(
|
||||
notification.tempsEcoule,
|
||||
style: AppTextStyles.bodySmall.copyWith(
|
||||
color: AppColors.onSurface.withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
|
||||
if (isExpired) ...[
|
||||
Text(
|
||||
' • Expirée',
|
||||
style: AppTextStyles.bodySmall.copyWith(
|
||||
color: AppColors.error,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Menu d'actions
|
||||
PopupMenuButton<String>(
|
||||
icon: Icon(
|
||||
Icons.more_vert,
|
||||
color: AppColors.onSurface.withOpacity(0.6),
|
||||
size: 20,
|
||||
),
|
||||
onSelected: (value) => _handleMenuAction(value),
|
||||
itemBuilder: (context) => [
|
||||
if (!notification.estLue)
|
||||
const PopupMenuItem(
|
||||
value: 'mark_read',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.mark_email_read, size: 20),
|
||||
title: Text('Marquer comme lu'),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
|
||||
PopupMenuItem(
|
||||
value: 'mark_important',
|
||||
child: ListTile(
|
||||
leading: Icon(
|
||||
notification.estImportante ? Icons.star : Icons.star_border,
|
||||
size: 20,
|
||||
),
|
||||
title: Text(
|
||||
notification.estImportante
|
||||
? 'Retirer des importantes'
|
||||
: 'Marquer comme importante',
|
||||
),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
|
||||
if (!notification.estArchivee)
|
||||
const PopupMenuItem(
|
||||
value: 'archive',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.archive, size: 20),
|
||||
title: Text('Archiver'),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
|
||||
const PopupMenuDivider(),
|
||||
|
||||
const PopupMenuItem(
|
||||
value: 'delete',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.delete, size: 20, color: Colors.red),
|
||||
title: Text('Supprimer', style: TextStyle(color: Colors.red)),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Message de la notification
|
||||
Text(
|
||||
notification.messageAffichage,
|
||||
style: AppTextStyles.bodyMedium.copyWith(
|
||||
color: isExpired
|
||||
? AppColors.onSurface.withOpacity(0.6)
|
||||
: AppColors.onSurface.withOpacity(0.8),
|
||||
),
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
|
||||
// Image de la notification (si présente)
|
||||
if (notification.imageUrl != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.network(
|
||||
notification.imageUrl!,
|
||||
height: 120,
|
||||
width: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) => Container(
|
||||
height: 120,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.image_not_supported,
|
||||
color: AppColors.onSurface.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
// Actions rapides
|
||||
if (notification.hasActionsRapides) ...[
|
||||
const SizedBox(height: 16),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: notification.actionsRapidesActives
|
||||
.take(3) // Limite à 3 actions pour éviter l'encombrement
|
||||
.map((action) => _buildActionButton(action))
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
|
||||
// Tags (si présents)
|
||||
if (notification.tags != null && notification.tags!.isNotEmpty) ...[
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 6,
|
||||
children: notification.tags!
|
||||
.take(3) // Limite à 3 tags
|
||||
.map((tag) => Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surfaceVariant,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
tag,
|
||||
style: AppTextStyles.labelSmall.copyWith(
|
||||
color: AppColors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionButton(ActionNotification action) {
|
||||
return OutlinedButton.icon(
|
||||
onPressed: () => onActionTap?.call(action),
|
||||
icon: Icon(
|
||||
_getActionIcon(action.icone),
|
||||
size: 16,
|
||||
),
|
||||
label: Text(
|
||||
action.libelle,
|
||||
style: AppTextStyles.labelMedium,
|
||||
),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
minimumSize: Size.zero,
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
foregroundColor: action.couleur != null
|
||||
? Color(int.parse(action.couleur!.replaceFirst('#', '0xFF')))
|
||||
: AppColors.primary,
|
||||
side: BorderSide(
|
||||
color: action.couleur != null
|
||||
? Color(int.parse(action.couleur!.replaceFirst('#', '0xFF')))
|
||||
: AppColors.primary,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Color _getTypeColor() {
|
||||
try {
|
||||
return Color(int.parse(notification.couleurType.replaceFirst('#', '0xFF')));
|
||||
} catch (e) {
|
||||
return AppColors.primary;
|
||||
}
|
||||
}
|
||||
|
||||
IconData _getTypeIcon() {
|
||||
switch (notification.typeNotification.icone) {
|
||||
case 'event':
|
||||
return Icons.event;
|
||||
case 'payment':
|
||||
return Icons.payment;
|
||||
case 'help':
|
||||
return Icons.help;
|
||||
case 'person_add':
|
||||
return Icons.person_add;
|
||||
case 'cake':
|
||||
return Icons.cake;
|
||||
case 'campaign':
|
||||
return Icons.campaign;
|
||||
case 'mail':
|
||||
return Icons.mail;
|
||||
case 'system_update':
|
||||
return Icons.system_update;
|
||||
case 'build':
|
||||
return Icons.build;
|
||||
case 'schedule':
|
||||
return Icons.schedule;
|
||||
case 'event_busy':
|
||||
return Icons.event_busy;
|
||||
case 'check_circle':
|
||||
return Icons.check_circle;
|
||||
case 'paid':
|
||||
return Icons.paid;
|
||||
case 'error':
|
||||
return Icons.error;
|
||||
case 'thumb_up':
|
||||
return Icons.thumb_up;
|
||||
case 'volunteer_activism':
|
||||
return Icons.volunteer_activism;
|
||||
case 'groups':
|
||||
return Icons.groups;
|
||||
case 'alternate_email':
|
||||
return Icons.alternate_email;
|
||||
default:
|
||||
return Icons.notifications;
|
||||
}
|
||||
}
|
||||
|
||||
IconData _getActionIcon(String? iconeName) {
|
||||
if (iconeName == null) return Icons.touch_app;
|
||||
|
||||
switch (iconeName) {
|
||||
case 'visibility':
|
||||
return Icons.visibility;
|
||||
case 'event_available':
|
||||
return Icons.event_available;
|
||||
case 'directions':
|
||||
return Icons.directions;
|
||||
case 'payment':
|
||||
return Icons.payment;
|
||||
case 'schedule':
|
||||
return Icons.schedule;
|
||||
case 'receipt':
|
||||
return Icons.receipt;
|
||||
case 'person':
|
||||
return Icons.person;
|
||||
case 'message':
|
||||
return Icons.message;
|
||||
case 'phone':
|
||||
return Icons.phone;
|
||||
case 'reply':
|
||||
return Icons.reply;
|
||||
default:
|
||||
return Icons.touch_app;
|
||||
}
|
||||
}
|
||||
|
||||
void _handleMenuAction(String action) {
|
||||
switch (action) {
|
||||
case 'mark_read':
|
||||
onMarkAsRead?.call();
|
||||
break;
|
||||
case 'mark_important':
|
||||
onMarkAsImportant?.call();
|
||||
break;
|
||||
case 'archive':
|
||||
onArchive?.call();
|
||||
break;
|
||||
case 'delete':
|
||||
onDelete?.call();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,389 @@
|
||||
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 '../../domain/entities/notification.dart';
|
||||
|
||||
/// Widget de recherche et filtrage des notifications
|
||||
class NotificationSearchWidget extends StatefulWidget {
|
||||
final Function(String) onSearchChanged;
|
||||
final Function({
|
||||
Set<TypeNotification>? types,
|
||||
Set<StatutNotification>? statuts,
|
||||
}) onFiltersChanged;
|
||||
final Set<TypeNotification> selectedTypes;
|
||||
final Set<StatutNotification> selectedStatuts;
|
||||
|
||||
const NotificationSearchWidget({
|
||||
super.key,
|
||||
required this.onSearchChanged,
|
||||
required this.onFiltersChanged,
|
||||
required this.selectedTypes,
|
||||
required this.selectedStatuts,
|
||||
});
|
||||
|
||||
@override
|
||||
State<NotificationSearchWidget> createState() => _NotificationSearchWidgetState();
|
||||
}
|
||||
|
||||
class _NotificationSearchWidgetState extends State<NotificationSearchWidget> {
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
bool _showFilters = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return UnifiedCard(
|
||||
variant: UnifiedCardVariant.outlined,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Barre de recherche
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Rechercher dans les notifications...',
|
||||
hintStyle: AppTextStyles.bodyMedium.copyWith(
|
||||
color: AppColors.onSurface.withOpacity(0.6),
|
||||
),
|
||||
prefixIcon: Icon(
|
||||
Icons.search,
|
||||
color: AppColors.onSurface.withOpacity(0.6),
|
||||
),
|
||||
suffixIcon: _searchController.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
_searchController.clear();
|
||||
widget.onSearchChanged('');
|
||||
},
|
||||
)
|
||||
: null,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: AppColors.outline.withOpacity(0.3),
|
||||
),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: AppColors.outline.withOpacity(0.3),
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: AppColors.primary,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
onChanged: widget.onSearchChanged,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// Bouton de filtres
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_showFilters = !_showFilters;
|
||||
});
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.filter_list,
|
||||
color: _hasActiveFilters()
|
||||
? AppColors.primary
|
||||
: AppColors.onSurface.withOpacity(0.6),
|
||||
),
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: _hasActiveFilters()
|
||||
? AppColors.primary.withOpacity(0.1)
|
||||
: AppColors.surface,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: BorderSide(
|
||||
color: _hasActiveFilters()
|
||||
? AppColors.primary
|
||||
: AppColors.outline.withOpacity(0.3),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Panneau de filtres (conditionnel)
|
||||
if (_showFilters) ...[
|
||||
const SizedBox(height: 16),
|
||||
_buildFiltersPanel(),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFiltersPanel() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// En-tête des filtres
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'Filtres',
|
||||
style: AppTextStyles.titleSmall.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
|
||||
const Spacer(),
|
||||
|
||||
if (_hasActiveFilters())
|
||||
TextButton(
|
||||
onPressed: _clearAllFilters,
|
||||
child: Text(
|
||||
'Tout effacer',
|
||||
style: AppTextStyles.labelMedium.copyWith(
|
||||
color: AppColors.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Filtres par type
|
||||
Text(
|
||||
'Types de notification',
|
||||
style: AppTextStyles.labelLarge.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: _getPopularTypes()
|
||||
.map((type) => _buildTypeChip(type))
|
||||
.toList(),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Filtres par statut
|
||||
Text(
|
||||
'Statuts',
|
||||
style: AppTextStyles.labelLarge.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: _getPopularStatuts()
|
||||
.map((statut) => _buildStatutChip(statut))
|
||||
.toList(),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Boutons d'action
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_showFilters = false;
|
||||
});
|
||||
},
|
||||
child: const Text('Fermer'),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 12),
|
||||
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_showFilters = false;
|
||||
});
|
||||
// Les filtres sont déjà appliqués en temps réel
|
||||
},
|
||||
child: const Text('Appliquer'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTypeChip(TypeNotification type) {
|
||||
final isSelected = widget.selectedTypes.contains(type);
|
||||
|
||||
return FilterChip(
|
||||
label: Text(
|
||||
type.libelle,
|
||||
style: AppTextStyles.labelMedium.copyWith(
|
||||
color: isSelected ? AppColors.onPrimary : AppColors.onSurface,
|
||||
),
|
||||
),
|
||||
selected: isSelected,
|
||||
onSelected: (selected) {
|
||||
final newTypes = Set<TypeNotification>.from(widget.selectedTypes);
|
||||
if (selected) {
|
||||
newTypes.add(type);
|
||||
} else {
|
||||
newTypes.remove(type);
|
||||
}
|
||||
widget.onFiltersChanged(types: newTypes);
|
||||
},
|
||||
selectedColor: AppColors.primary,
|
||||
backgroundColor: AppColors.surface,
|
||||
side: BorderSide(
|
||||
color: isSelected
|
||||
? AppColors.primary
|
||||
: AppColors.outline.withOpacity(0.3),
|
||||
),
|
||||
avatar: isSelected
|
||||
? null
|
||||
: Icon(
|
||||
_getTypeIcon(type),
|
||||
size: 16,
|
||||
color: _getTypeColor(type),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatutChip(StatutNotification statut) {
|
||||
final isSelected = widget.selectedStatuts.contains(statut);
|
||||
|
||||
return FilterChip(
|
||||
label: Text(
|
||||
statut.libelle,
|
||||
style: AppTextStyles.labelMedium.copyWith(
|
||||
color: isSelected ? AppColors.onPrimary : AppColors.onSurface,
|
||||
),
|
||||
),
|
||||
selected: isSelected,
|
||||
onSelected: (selected) {
|
||||
final newStatuts = Set<StatutNotification>.from(widget.selectedStatuts);
|
||||
if (selected) {
|
||||
newStatuts.add(statut);
|
||||
} else {
|
||||
newStatuts.remove(statut);
|
||||
}
|
||||
widget.onFiltersChanged(statuts: newStatuts);
|
||||
},
|
||||
selectedColor: AppColors.primary,
|
||||
backgroundColor: AppColors.surface,
|
||||
side: BorderSide(
|
||||
color: isSelected
|
||||
? AppColors.primary
|
||||
: AppColors.outline.withOpacity(0.3),
|
||||
),
|
||||
avatar: isSelected
|
||||
? null
|
||||
: Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: _getStatutColor(statut),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<TypeNotification> _getPopularTypes() {
|
||||
return [
|
||||
TypeNotification.nouvelEvenement,
|
||||
TypeNotification.cotisationDue,
|
||||
TypeNotification.nouvelleDemandeAide,
|
||||
TypeNotification.nouveauMembre,
|
||||
TypeNotification.annonceGenerale,
|
||||
TypeNotification.messagePrive,
|
||||
];
|
||||
}
|
||||
|
||||
List<StatutNotification> _getPopularStatuts() {
|
||||
return [
|
||||
StatutNotification.nonLue,
|
||||
StatutNotification.lue,
|
||||
StatutNotification.marqueeImportante,
|
||||
StatutNotification.archivee,
|
||||
];
|
||||
}
|
||||
|
||||
IconData _getTypeIcon(TypeNotification type) {
|
||||
switch (type.icone) {
|
||||
case 'event':
|
||||
return Icons.event;
|
||||
case 'payment':
|
||||
return Icons.payment;
|
||||
case 'help':
|
||||
return Icons.help;
|
||||
case 'person_add':
|
||||
return Icons.person_add;
|
||||
case 'campaign':
|
||||
return Icons.campaign;
|
||||
case 'mail':
|
||||
return Icons.mail;
|
||||
default:
|
||||
return Icons.notifications;
|
||||
}
|
||||
}
|
||||
|
||||
Color _getTypeColor(TypeNotification type) {
|
||||
try {
|
||||
return Color(int.parse(type.couleur.replaceFirst('#', '0xFF')));
|
||||
} catch (e) {
|
||||
return AppColors.primary;
|
||||
}
|
||||
}
|
||||
|
||||
Color _getStatutColor(StatutNotification statut) {
|
||||
try {
|
||||
return Color(int.parse(statut.couleur.replaceFirst('#', '0xFF')));
|
||||
} catch (e) {
|
||||
return AppColors.primary;
|
||||
}
|
||||
}
|
||||
|
||||
bool _hasActiveFilters() {
|
||||
return widget.selectedTypes.isNotEmpty || widget.selectedStatuts.isNotEmpty;
|
||||
}
|
||||
|
||||
void _clearAllFilters() {
|
||||
widget.onFiltersChanged(
|
||||
types: <TypeNotification>{},
|
||||
statuts: <StatutNotification>{},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,400 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../core/widgets/unified_card.dart';
|
||||
import '../../../../core/theme/app_colors.dart';
|
||||
import '../../../../core/theme/app_text_styles.dart';
|
||||
|
||||
/// Widget d'affichage des statistiques de notifications
|
||||
class NotificationStatsWidget extends StatelessWidget {
|
||||
final int totalCount;
|
||||
final int unreadCount;
|
||||
final int importantCount;
|
||||
|
||||
const NotificationStatsWidget({
|
||||
super.key,
|
||||
required this.totalCount,
|
||||
required this.unreadCount,
|
||||
required this.importantCount,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return UnifiedCard(
|
||||
variant: UnifiedCardVariant.filled,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
children: [
|
||||
// Statistique principale - Non lues
|
||||
Expanded(
|
||||
child: _buildStatItem(
|
||||
icon: Icons.mark_email_unread,
|
||||
label: 'Non lues',
|
||||
value: unreadCount.toString(),
|
||||
color: unreadCount > 0 ? AppColors.primary : AppColors.onSurface.withOpacity(0.6),
|
||||
isHighlighted: unreadCount > 0,
|
||||
),
|
||||
),
|
||||
|
||||
// Séparateur
|
||||
Container(
|
||||
width: 1,
|
||||
height: 40,
|
||||
color: AppColors.outline.withOpacity(0.2),
|
||||
),
|
||||
|
||||
// Statistique secondaire - Importantes
|
||||
Expanded(
|
||||
child: _buildStatItem(
|
||||
icon: Icons.star,
|
||||
label: 'Importantes',
|
||||
value: importantCount.toString(),
|
||||
color: importantCount > 0 ? AppColors.warning : AppColors.onSurface.withOpacity(0.6),
|
||||
isHighlighted: importantCount > 0,
|
||||
),
|
||||
),
|
||||
|
||||
// Séparateur
|
||||
Container(
|
||||
width: 1,
|
||||
height: 40,
|
||||
color: AppColors.outline.withOpacity(0.2),
|
||||
),
|
||||
|
||||
// Statistique tertiaire - Total
|
||||
Expanded(
|
||||
child: _buildStatItem(
|
||||
icon: Icons.notifications,
|
||||
label: 'Total',
|
||||
value: totalCount.toString(),
|
||||
color: AppColors.onSurface.withOpacity(0.8),
|
||||
isHighlighted: false,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatItem({
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required String value,
|
||||
required Color color,
|
||||
required bool isHighlighted,
|
||||
}) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Icône avec badge si mis en évidence
|
||||
Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
color: color,
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
|
||||
if (isHighlighted && value != '0')
|
||||
Positioned(
|
||||
right: -4,
|
||||
top: -4,
|
||||
child: Container(
|
||||
width: 12,
|
||||
height: 12,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: AppColors.surface,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Valeur
|
||||
Text(
|
||||
value,
|
||||
style: AppTextStyles.titleMedium.copyWith(
|
||||
color: color,
|
||||
fontWeight: isHighlighted ? FontWeight.w700 : FontWeight.w600,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 2),
|
||||
|
||||
// Label
|
||||
Text(
|
||||
label,
|
||||
style: AppTextStyles.labelSmall.copyWith(
|
||||
color: AppColors.onSurface.withOpacity(0.7),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Widget d'affichage des statistiques détaillées
|
||||
class DetailedNotificationStatsWidget extends StatelessWidget {
|
||||
final Map<String, dynamic> stats;
|
||||
|
||||
const DetailedNotificationStatsWidget({
|
||||
super.key,
|
||||
required this.stats,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final totalNotifications = stats['total'] ?? 0;
|
||||
final unreadNotifications = stats['unread'] ?? 0;
|
||||
final importantNotifications = stats['important'] ?? 0;
|
||||
final archivedNotifications = stats['archived'] ?? 0;
|
||||
final todayNotifications = stats['today'] ?? 0;
|
||||
final weekNotifications = stats['week'] ?? 0;
|
||||
final engagementRate = stats['engagement_rate'] ?? 0.0;
|
||||
|
||||
return UnifiedCard(
|
||||
variant: UnifiedCardVariant.outlined,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// En-tête
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.analytics,
|
||||
color: AppColors.primary,
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
'Statistiques détaillées',
|
||||
style: AppTextStyles.titleMedium.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Grille de statistiques
|
||||
GridView.count(
|
||||
crossAxisCount: 2,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
childAspectRatio: 2.5,
|
||||
mainAxisSpacing: 16,
|
||||
crossAxisSpacing: 16,
|
||||
children: [
|
||||
_buildDetailedStatCard(
|
||||
'Total',
|
||||
totalNotifications.toString(),
|
||||
Icons.notifications,
|
||||
AppColors.primary,
|
||||
),
|
||||
_buildDetailedStatCard(
|
||||
'Non lues',
|
||||
unreadNotifications.toString(),
|
||||
Icons.mark_email_unread,
|
||||
AppColors.warning,
|
||||
),
|
||||
_buildDetailedStatCard(
|
||||
'Importantes',
|
||||
importantNotifications.toString(),
|
||||
Icons.star,
|
||||
AppColors.error,
|
||||
),
|
||||
_buildDetailedStatCard(
|
||||
'Archivées',
|
||||
archivedNotifications.toString(),
|
||||
Icons.archive,
|
||||
AppColors.onSurface.withOpacity(0.6),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Statistiques temporelles
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildTimeStatCard(
|
||||
'Aujourd\'hui',
|
||||
todayNotifications.toString(),
|
||||
Icons.today,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: _buildTimeStatCard(
|
||||
'Cette semaine',
|
||||
weekNotifications.toString(),
|
||||
Icons.date_range,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Taux d'engagement
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primaryContainer.withOpacity(0.3),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: AppColors.primary.withOpacity(0.2),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.trending_up,
|
||||
color: AppColors.primary,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Taux d\'engagement',
|
||||
style: AppTextStyles.labelMedium.copyWith(
|
||||
color: AppColors.primary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'Pourcentage de notifications ouvertes',
|
||||
style: AppTextStyles.bodySmall.copyWith(
|
||||
color: AppColors.onSurface.withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${engagementRate.toStringAsFixed(1)}%',
|
||||
style: AppTextStyles.titleMedium.copyWith(
|
||||
color: AppColors.primary,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDetailedStatCard(
|
||||
String label,
|
||||
String value,
|
||||
IconData icon,
|
||||
Color color,
|
||||
) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.05),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: color.withOpacity(0.2),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
color: color,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
value,
|
||||
style: AppTextStyles.titleSmall.copyWith(
|
||||
color: color,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
label,
|
||||
style: AppTextStyles.labelSmall.copyWith(
|
||||
color: AppColors.onSurface.withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTimeStatCard(String label, String value, IconData icon) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surfaceVariant.withOpacity(0.5),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: AppColors.outline.withOpacity(0.2),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
color: AppColors.onSurfaceVariant,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
value,
|
||||
style: AppTextStyles.titleSmall.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
label,
|
||||
style: AppTextStyles.labelSmall.copyWith(
|
||||
color: AppColors.onSurface.withOpacity(0.7),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,366 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../../../../core/performance/performance_optimizer.dart';
|
||||
import '../../../../core/performance/smart_cache_service.dart';
|
||||
import '../../../../shared/widgets/performance/optimized_list_view.dart';
|
||||
import '../../../../shared/theme/app_theme.dart';
|
||||
|
||||
/// Page de démonstration des optimisations de performance
|
||||
class PerformanceDemoPage extends StatefulWidget {
|
||||
const PerformanceDemoPage({super.key});
|
||||
|
||||
@override
|
||||
State<PerformanceDemoPage> createState() => _PerformanceDemoPageState();
|
||||
}
|
||||
|
||||
class _PerformanceDemoPageState extends State<PerformanceDemoPage>
|
||||
with TickerProviderStateMixin {
|
||||
|
||||
final _optimizer = PerformanceOptimizer();
|
||||
final _cacheService = SmartCacheService();
|
||||
|
||||
// Données de test pour les démonstrations
|
||||
List<DemoItem> _items = [];
|
||||
bool _isLoading = false;
|
||||
bool _hasMore = true;
|
||||
|
||||
// Contrôleurs d'animation
|
||||
late AnimationController _fadeController;
|
||||
late AnimationController _slideController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Initialiser le service de cache
|
||||
_cacheService.initialize();
|
||||
|
||||
// Initialiser les contrôleurs d'animation optimisés
|
||||
_fadeController = PerformanceOptimizer.createOptimizedController(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
vsync: this,
|
||||
debugLabel: 'FadeController',
|
||||
);
|
||||
|
||||
_slideController = PerformanceOptimizer.createOptimizedController(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
vsync: this,
|
||||
debugLabel: 'SlideController',
|
||||
);
|
||||
|
||||
// Démarrer le monitoring des performances
|
||||
_optimizer.startPerformanceMonitoring();
|
||||
|
||||
// Générer des données initiales
|
||||
_generateInitialData();
|
||||
|
||||
// Démarrer les animations
|
||||
_fadeController.forward();
|
||||
_slideController.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
PerformanceOptimizer.disposeControllers([_fadeController, _slideController]);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _generateInitialData() {
|
||||
_items = List.generate(20, (index) => DemoItem(
|
||||
id: index,
|
||||
title: 'Élément $index',
|
||||
subtitle: 'Description de l\'élément $index',
|
||||
value: (index * 10).toDouble(),
|
||||
));
|
||||
}
|
||||
|
||||
Future<void> _loadMoreItems() async {
|
||||
if (_isLoading || !_hasMore) return;
|
||||
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
_optimizer.startTimer('load_more_items');
|
||||
|
||||
// Simuler un délai de chargement
|
||||
await Future.delayed(const Duration(milliseconds: 800));
|
||||
|
||||
final newItems = List.generate(10, (index) => DemoItem(
|
||||
id: _items.length + index,
|
||||
title: 'Élément ${_items.length + index}',
|
||||
subtitle: 'Description de l\'élément ${_items.length + index}',
|
||||
value: ((_items.length + index) * 10).toDouble(),
|
||||
));
|
||||
|
||||
setState(() {
|
||||
_items.addAll(newItems);
|
||||
_isLoading = false;
|
||||
_hasMore = _items.length < 100; // Limiter à 100 éléments
|
||||
});
|
||||
|
||||
_optimizer.stopTimer('load_more_items');
|
||||
}
|
||||
|
||||
Future<void> _refreshItems() async {
|
||||
_optimizer.startTimer('refresh_items');
|
||||
|
||||
// Simuler un délai de rafraîchissement
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
setState(() {
|
||||
_generateInitialData();
|
||||
_hasMore = true;
|
||||
});
|
||||
|
||||
_optimizer.stopTimer('refresh_items');
|
||||
}
|
||||
|
||||
void _testCachePerformance() async {
|
||||
_optimizer.startTimer('cache_test');
|
||||
|
||||
// Test d'écriture en cache
|
||||
for (int i = 0; i < 100; i++) {
|
||||
await _cacheService.put('test_key_$i', 'test_value_$i');
|
||||
}
|
||||
|
||||
// Test de lecture en cache
|
||||
for (int i = 0; i < 100; i++) {
|
||||
await _cacheService.get<String>('test_key_$i');
|
||||
}
|
||||
|
||||
_optimizer.stopTimer('cache_test');
|
||||
|
||||
final cacheInfo = await _cacheService.getCacheInfo();
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Test de cache terminé: $cacheInfo'),
|
||||
backgroundColor: AppTheme.successColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _showPerformanceStats() {
|
||||
final stats = _optimizer.getPerformanceStats();
|
||||
final cacheStats = _cacheService.getStats();
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Statistiques de Performance'),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text('Optimiseur:', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
...stats.entries.map((e) => Text('${e.key}: ${e.value}')),
|
||||
const SizedBox(height: 16),
|
||||
const Text('Cache:', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
Text(cacheStats.toString()),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
_optimizer.resetStats();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text('Réinitialiser'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Fermer'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _clearAllCaches() {
|
||||
_optimizer.clearAllCaches();
|
||||
_cacheService.clear();
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Tous les caches ont été vidés'),
|
||||
backgroundColor: AppTheme.warningColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Démonstration Performance'),
|
||||
backgroundColor: AppTheme.primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.analytics),
|
||||
onPressed: _showPerformanceStats,
|
||||
tooltip: 'Statistiques',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.clear_all),
|
||||
onPressed: _clearAllCaches,
|
||||
tooltip: 'Vider les caches',
|
||||
),
|
||||
],
|
||||
),
|
||||
body: FadeTransition(
|
||||
opacity: _fadeController,
|
||||
child: Column(
|
||||
children: [
|
||||
// Section des boutons de test
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
onPressed: _testCachePerformance,
|
||||
icon: const Icon(Icons.speed),
|
||||
label: const Text('Test Cache'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
HapticFeedback.lightImpact();
|
||||
PerformanceOptimizer.forceGarbageCollection();
|
||||
},
|
||||
icon: const Icon(Icons.cleaning_services),
|
||||
label: const Text('Force GC'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.warningColor,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _showPerformanceStats,
|
||||
icon: const Icon(Icons.bar_chart),
|
||||
label: const Text('Stats'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.infoColor,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Liste optimisée
|
||||
Expanded(
|
||||
child: SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
begin: const Offset(0, 0.1),
|
||||
end: Offset.zero,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _slideController,
|
||||
curve: Curves.easeOutCubic,
|
||||
)),
|
||||
child: OptimizedListView<DemoItem>(
|
||||
items: _items,
|
||||
itemBuilder: (context, item, index) => _buildDemoItem(item, index),
|
||||
onLoadMore: _loadMoreItems,
|
||||
onRefresh: _refreshItems,
|
||||
hasMore: _hasMore,
|
||||
isLoading: _isLoading,
|
||||
loadMoreThreshold: 5,
|
||||
itemExtent: 80,
|
||||
enableAnimations: true,
|
||||
enableRecycling: true,
|
||||
maxCachedWidgets: 30,
|
||||
emptyWidget: const Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.speed, size: 64, color: Colors.grey),
|
||||
SizedBox(height: 16),
|
||||
Text('Aucun élément de test', style: TextStyle(color: Colors.grey)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDemoItem(DemoItem item, int index) {
|
||||
return PerformanceOptimizer.optimizeWidget(
|
||||
Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: AppTheme.primaryColor,
|
||||
child: Text('${item.id}', style: const TextStyle(color: Colors.white)),
|
||||
),
|
||||
title: Text(item.title),
|
||||
subtitle: Text(item.subtitle),
|
||||
trailing: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'${item.value.toInt()}',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
),
|
||||
const Text('pts', style: TextStyle(fontSize: 12)),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
HapticFeedback.selectionClick();
|
||||
_optimizer.incrementCounter('item_tapped');
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Élément ${item.title} sélectionné'),
|
||||
duration: const Duration(milliseconds: 800),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
key: 'demo_item_${item.id}',
|
||||
forceRepaintBoundary: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Modèle de données pour la démonstration
|
||||
class DemoItem {
|
||||
final int id;
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final double value;
|
||||
|
||||
DemoItem({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.value,
|
||||
});
|
||||
|
||||
@override
|
||||
int get hashCode => id.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is DemoItem && other.id == id;
|
||||
}
|
||||
}
|
||||
@@ -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