Refactoring

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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