Refactoring
This commit is contained in:
@@ -0,0 +1,843 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import '../../../../../core/error/failures.dart';
|
||||
import '../../../domain/entities/demande_aide.dart';
|
||||
import '../../../domain/usecases/gerer_demandes_aide_usecase.dart';
|
||||
import 'demandes_aide_event.dart';
|
||||
import 'demandes_aide_state.dart';
|
||||
|
||||
/// BLoC pour la gestion des demandes d'aide
|
||||
///
|
||||
/// Ce BLoC gère tous les états et événements liés aux demandes d'aide,
|
||||
/// incluant le chargement, la création, la modification, la validation,
|
||||
/// le filtrage, le tri et l'export des demandes.
|
||||
class DemandesAideBloc extends Bloc<DemandesAideEvent, DemandesAideState> {
|
||||
final CreerDemandeAideUseCase creerDemandeAideUseCase;
|
||||
final MettreAJourDemandeAideUseCase mettreAJourDemandeAideUseCase;
|
||||
final ObtenirDemandeAideUseCase obtenirDemandeAideUseCase;
|
||||
final SoumettreDemandeAideUseCase soumettreDemandeAideUseCase;
|
||||
final EvaluerDemandeAideUseCase evaluerDemandeAideUseCase;
|
||||
final RechercherDemandesAideUseCase rechercherDemandesAideUseCase;
|
||||
final ObtenirDemandesUrgentesUseCase obtenirDemandesUrgentesUseCase;
|
||||
final ObtenirMesDemandesUseCase obtenirMesDemandesUseCase;
|
||||
final ValiderDemandeAideUseCase validerDemandeAideUseCase;
|
||||
final CalculerPrioriteDemandeUseCase calculerPrioriteDemandeUseCase;
|
||||
|
||||
// Cache des paramètres de recherche pour la pagination
|
||||
String? _lastOrganisationId;
|
||||
TypeAide? _lastTypeAide;
|
||||
StatutAide? _lastStatut;
|
||||
String? _lastDemandeurId;
|
||||
bool? _lastUrgente;
|
||||
|
||||
DemandesAideBloc({
|
||||
required this.creerDemandeAideUseCase,
|
||||
required this.mettreAJourDemandeAideUseCase,
|
||||
required this.obtenirDemandeAideUseCase,
|
||||
required this.soumettreDemandeAideUseCase,
|
||||
required this.evaluerDemandeAideUseCase,
|
||||
required this.rechercherDemandesAideUseCase,
|
||||
required this.obtenirDemandesUrgentesUseCase,
|
||||
required this.obtenirMesDemandesUseCase,
|
||||
required this.validerDemandeAideUseCase,
|
||||
required this.calculerPrioriteDemandeUseCase,
|
||||
}) : super(const DemandesAideInitial()) {
|
||||
// Enregistrement des handlers d'événements
|
||||
on<ChargerDemandesAideEvent>(_onChargerDemandesAide);
|
||||
on<ChargerPlusDemandesAideEvent>(_onChargerPlusDemandesAide);
|
||||
on<CreerDemandeAideEvent>(_onCreerDemandeAide);
|
||||
on<MettreAJourDemandeAideEvent>(_onMettreAJourDemandeAide);
|
||||
on<ObtenirDemandeAideEvent>(_onObtenirDemandeAide);
|
||||
on<SoumettreDemandeAideEvent>(_onSoumettreDemandeAide);
|
||||
on<EvaluerDemandeAideEvent>(_onEvaluerDemandeAide);
|
||||
on<ChargerDemandesUrgentesEvent>(_onChargerDemandesUrgentes);
|
||||
on<ChargerMesDemandesEvent>(_onChargerMesdemandes);
|
||||
on<RechercherDemandesAideEvent>(_onRechercherDemandesAide);
|
||||
on<ValiderDemandeAideEvent>(_onValiderDemandeAide);
|
||||
on<CalculerPrioriteDemandeEvent>(_onCalculerPrioriteDemande);
|
||||
on<FiltrerDemandesAideEvent>(_onFiltrerDemandesAide);
|
||||
on<TrierDemandesAideEvent>(_onTrierDemandesAide);
|
||||
on<RafraichirDemandesAideEvent>(_onRafraichirDemandesAide);
|
||||
on<ReinitialiserDemandesAideEvent>(_onReinitialiserDemandesAide);
|
||||
on<SelectionnerDemandeAideEvent>(_onSelectionnerDemandeAide);
|
||||
on<SelectionnerToutesDemandesAideEvent>(_onSelectionnerToutesDemandesAide);
|
||||
on<SupprimerDemandesSelectionnees>(_onSupprimerDemandesSelectionnees);
|
||||
on<ExporterDemandesAideEvent>(_onExporterDemandesAide);
|
||||
}
|
||||
|
||||
/// Handler pour charger les demandes d'aide
|
||||
Future<void> _onChargerDemandesAide(
|
||||
ChargerDemandesAideEvent event,
|
||||
Emitter<DemandesAideState> emit,
|
||||
) async {
|
||||
// Sauvegarder les paramètres pour la pagination
|
||||
_lastOrganisationId = event.organisationId;
|
||||
_lastTypeAide = event.typeAide;
|
||||
_lastStatut = event.statut;
|
||||
_lastDemandeurId = event.demandeurId;
|
||||
_lastUrgente = event.urgente;
|
||||
|
||||
if (event.forceRefresh || state is! DemandesAideLoaded) {
|
||||
emit(const DemandesAideLoading());
|
||||
} else if (state is DemandesAideLoaded) {
|
||||
emit((state as DemandesAideLoaded).copyWith(isRefreshing: true));
|
||||
}
|
||||
|
||||
final result = await rechercherDemandesAideUseCase(
|
||||
RechercherDemandesAideParams(
|
||||
organisationId: event.organisationId,
|
||||
typeAide: event.typeAide,
|
||||
statut: event.statut,
|
||||
demandeurId: event.demandeurId,
|
||||
urgente: event.urgente,
|
||||
page: 0,
|
||||
taille: 20,
|
||||
),
|
||||
);
|
||||
|
||||
result.fold(
|
||||
(failure) => emit(DemandesAideError(
|
||||
message: _mapFailureToMessage(failure),
|
||||
isNetworkError: failure is NetworkFailure,
|
||||
canRetry: true,
|
||||
cachedData: state is DemandesAideLoaded
|
||||
? (state as DemandesAideLoaded).demandes
|
||||
: null,
|
||||
)),
|
||||
(demandes) {
|
||||
final demandesFiltrees = _appliquerFiltres(demandes, const FiltresDemandesAide());
|
||||
emit(DemandesAideLoaded(
|
||||
demandes: demandes,
|
||||
demandesFiltrees: demandesFiltrees,
|
||||
hasReachedMax: demandes.length < 20,
|
||||
currentPage: 0,
|
||||
totalElements: demandes.length,
|
||||
lastUpdated: DateTime.now(),
|
||||
));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Handler pour charger plus de demandes (pagination)
|
||||
Future<void> _onChargerPlusDemandesAide(
|
||||
ChargerPlusDemandesAideEvent event,
|
||||
Emitter<DemandesAideState> emit,
|
||||
) async {
|
||||
if (state is! DemandesAideLoaded) return;
|
||||
|
||||
final currentState = state as DemandesAideLoaded;
|
||||
if (currentState.hasReachedMax || currentState.isLoadingMore) return;
|
||||
|
||||
emit(currentState.copyWith(isLoadingMore: true));
|
||||
|
||||
final result = await rechercherDemandesAideUseCase(
|
||||
RechercherDemandesAideParams(
|
||||
organisationId: _lastOrganisationId,
|
||||
typeAide: _lastTypeAide,
|
||||
statut: _lastStatut,
|
||||
demandeurId: _lastDemandeurId,
|
||||
urgente: _lastUrgente,
|
||||
page: currentState.currentPage + 1,
|
||||
taille: 20,
|
||||
),
|
||||
);
|
||||
|
||||
result.fold(
|
||||
(failure) => emit(currentState.copyWith(
|
||||
isLoadingMore: false,
|
||||
)),
|
||||
(nouvellesDemandes) {
|
||||
final toutesLesdemandes = [...currentState.demandes, ...nouvellesDemandes];
|
||||
final demandesFiltrees = _appliquerFiltres(toutesLesdemandes, currentState.filtres);
|
||||
|
||||
emit(currentState.copyWith(
|
||||
demandes: toutesLesdemandes,
|
||||
demandesFiltrees: demandesFiltrees,
|
||||
hasReachedMax: nouvellesDemandes.length < 20,
|
||||
currentPage: currentState.currentPage + 1,
|
||||
totalElements: toutesLesdemandes.length,
|
||||
isLoadingMore: false,
|
||||
lastUpdated: DateTime.now(),
|
||||
));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Handler pour créer une demande d'aide
|
||||
Future<void> _onCreerDemandeAide(
|
||||
CreerDemandeAideEvent event,
|
||||
Emitter<DemandesAideState> emit,
|
||||
) async {
|
||||
emit(const DemandesAideLoading());
|
||||
|
||||
final result = await creerDemandeAideUseCase(
|
||||
CreerDemandeAideParams(demande: event.demande),
|
||||
);
|
||||
|
||||
result.fold(
|
||||
(failure) => emit(DemandesAideError(
|
||||
message: _mapFailureToMessage(failure),
|
||||
isNetworkError: failure is NetworkFailure,
|
||||
canRetry: true,
|
||||
)),
|
||||
(demande) {
|
||||
emit(DemandesAideOperationSuccess(
|
||||
message: TypeOperationDemande.creation.messageSucces,
|
||||
demande: demande,
|
||||
operation: TypeOperationDemande.creation,
|
||||
));
|
||||
|
||||
// Recharger la liste après création
|
||||
add(const ChargerDemandesAideEvent(forceRefresh: true));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Handler pour mettre à jour une demande d'aide
|
||||
Future<void> _onMettreAJourDemandeAide(
|
||||
MettreAJourDemandeAideEvent event,
|
||||
Emitter<DemandesAideState> emit,
|
||||
) async {
|
||||
emit(const DemandesAideLoading());
|
||||
|
||||
final result = await mettreAJourDemandeAideUseCase(
|
||||
MettreAJourDemandeAideParams(demande: event.demande),
|
||||
);
|
||||
|
||||
result.fold(
|
||||
(failure) => emit(DemandesAideError(
|
||||
message: _mapFailureToMessage(failure),
|
||||
isNetworkError: failure is NetworkFailure,
|
||||
canRetry: true,
|
||||
)),
|
||||
(demande) {
|
||||
emit(DemandesAideOperationSuccess(
|
||||
message: TypeOperationDemande.modification.messageSucces,
|
||||
demande: demande,
|
||||
operation: TypeOperationDemande.modification,
|
||||
));
|
||||
|
||||
// Mettre à jour la demande dans la liste si elle existe
|
||||
if (state is DemandesAideLoaded) {
|
||||
final currentState = state as DemandesAideLoaded;
|
||||
final demandesUpdated = currentState.demandes.map((d) =>
|
||||
d.id == demande.id ? demande : d
|
||||
).toList();
|
||||
|
||||
final demandesFiltrees = _appliquerFiltres(demandesUpdated, currentState.filtres);
|
||||
|
||||
emit(currentState.copyWith(
|
||||
demandes: demandesUpdated,
|
||||
demandesFiltrees: demandesFiltrees,
|
||||
lastUpdated: DateTime.now(),
|
||||
));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Handler pour obtenir une demande d'aide spécifique
|
||||
Future<void> _onObtenirDemandeAide(
|
||||
ObtenirDemandeAideEvent event,
|
||||
Emitter<DemandesAideState> emit,
|
||||
) async {
|
||||
emit(const DemandesAideLoading());
|
||||
|
||||
final result = await obtenirDemandeAideUseCase(
|
||||
ObtenirDemandeAideParams(id: event.demandeId),
|
||||
);
|
||||
|
||||
result.fold(
|
||||
(failure) => emit(DemandesAideError(
|
||||
message: _mapFailureToMessage(failure),
|
||||
isNetworkError: failure is NetworkFailure,
|
||||
canRetry: true,
|
||||
)),
|
||||
(demande) {
|
||||
// Si on a déjà une liste chargée, mettre à jour la demande
|
||||
if (state is DemandesAideLoaded) {
|
||||
final currentState = state as DemandesAideLoaded;
|
||||
final demandesUpdated = currentState.demandes.map((d) =>
|
||||
d.id == demande.id ? demande : d
|
||||
).toList();
|
||||
|
||||
// Ajouter la demande si elle n'existe pas
|
||||
if (!demandesUpdated.any((d) => d.id == demande.id)) {
|
||||
demandesUpdated.insert(0, demande);
|
||||
}
|
||||
|
||||
final demandesFiltrees = _appliquerFiltres(demandesUpdated, currentState.filtres);
|
||||
|
||||
emit(currentState.copyWith(
|
||||
demandes: demandesUpdated,
|
||||
demandesFiltrees: demandesFiltrees,
|
||||
lastUpdated: DateTime.now(),
|
||||
));
|
||||
} else {
|
||||
// Créer un nouvel état avec cette demande
|
||||
emit(DemandesAideLoaded(
|
||||
demandes: [demande],
|
||||
demandesFiltrees: [demande],
|
||||
hasReachedMax: true,
|
||||
currentPage: 0,
|
||||
totalElements: 1,
|
||||
lastUpdated: DateTime.now(),
|
||||
));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Handler pour soumettre une demande d'aide
|
||||
Future<void> _onSoumettreDemandeAide(
|
||||
SoumettreDemandeAideEvent event,
|
||||
Emitter<DemandesAideState> emit,
|
||||
) async {
|
||||
emit(const DemandesAideLoading());
|
||||
|
||||
final result = await soumettreDemandeAideUseCase(
|
||||
SoumettreDemandeAideParams(demandeId: event.demandeId),
|
||||
);
|
||||
|
||||
result.fold(
|
||||
(failure) => emit(DemandesAideError(
|
||||
message: _mapFailureToMessage(failure),
|
||||
isNetworkError: failure is NetworkFailure,
|
||||
canRetry: true,
|
||||
)),
|
||||
(demande) {
|
||||
emit(DemandesAideOperationSuccess(
|
||||
message: TypeOperationDemande.soumission.messageSucces,
|
||||
demande: demande,
|
||||
operation: TypeOperationDemande.soumission,
|
||||
));
|
||||
|
||||
// Mettre à jour la demande dans la liste
|
||||
if (state is DemandesAideLoaded) {
|
||||
final currentState = state as DemandesAideLoaded;
|
||||
final demandesUpdated = currentState.demandes.map((d) =>
|
||||
d.id == demande.id ? demande : d
|
||||
).toList();
|
||||
|
||||
final demandesFiltrees = _appliquerFiltres(demandesUpdated, currentState.filtres);
|
||||
|
||||
emit(currentState.copyWith(
|
||||
demandes: demandesUpdated,
|
||||
demandesFiltrees: demandesFiltrees,
|
||||
lastUpdated: DateTime.now(),
|
||||
));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Handler pour évaluer une demande d'aide
|
||||
Future<void> _onEvaluerDemandeAide(
|
||||
EvaluerDemandeAideEvent event,
|
||||
Emitter<DemandesAideState> emit,
|
||||
) async {
|
||||
emit(const DemandesAideLoading());
|
||||
|
||||
final result = await evaluerDemandeAideUseCase(
|
||||
EvaluerDemandeAideParams(
|
||||
demandeId: event.demandeId,
|
||||
evaluateurId: event.evaluateurId,
|
||||
decision: event.decision,
|
||||
commentaire: event.commentaire,
|
||||
montantApprouve: event.montantApprouve,
|
||||
),
|
||||
);
|
||||
|
||||
result.fold(
|
||||
(failure) => emit(DemandesAideError(
|
||||
message: _mapFailureToMessage(failure),
|
||||
isNetworkError: failure is NetworkFailure,
|
||||
canRetry: true,
|
||||
)),
|
||||
(demande) {
|
||||
emit(DemandesAideOperationSuccess(
|
||||
message: TypeOperationDemande.evaluation.messageSucces,
|
||||
demande: demande,
|
||||
operation: TypeOperationDemande.evaluation,
|
||||
));
|
||||
|
||||
// Mettre à jour la demande dans la liste
|
||||
if (state is DemandesAideLoaded) {
|
||||
final currentState = state as DemandesAideLoaded;
|
||||
final demandesUpdated = currentState.demandes.map((d) =>
|
||||
d.id == demande.id ? demande : d
|
||||
).toList();
|
||||
|
||||
final demandesFiltrees = _appliquerFiltres(demandesUpdated, currentState.filtres);
|
||||
|
||||
emit(currentState.copyWith(
|
||||
demandes: demandesUpdated,
|
||||
demandesFiltrees: demandesFiltrees,
|
||||
lastUpdated: DateTime.now(),
|
||||
));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Handler pour charger les demandes urgentes
|
||||
Future<void> _onChargerDemandesUrgentes(
|
||||
ChargerDemandesUrgentesEvent event,
|
||||
Emitter<DemandesAideState> emit,
|
||||
) async {
|
||||
emit(const DemandesAideLoading());
|
||||
|
||||
final result = await obtenirDemandesUrgentesUseCase(
|
||||
ObtenirDemandesUrgentesParams(organisationId: event.organisationId),
|
||||
);
|
||||
|
||||
result.fold(
|
||||
(failure) => emit(DemandesAideError(
|
||||
message: _mapFailureToMessage(failure),
|
||||
isNetworkError: failure is NetworkFailure,
|
||||
canRetry: true,
|
||||
)),
|
||||
(demandes) {
|
||||
final demandesFiltrees = _appliquerFiltres(demandes, const FiltresDemandesAide());
|
||||
emit(DemandesAideLoaded(
|
||||
demandes: demandes,
|
||||
demandesFiltrees: demandesFiltrees,
|
||||
hasReachedMax: true,
|
||||
currentPage: 0,
|
||||
totalElements: demandes.length,
|
||||
lastUpdated: DateTime.now(),
|
||||
));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Handler pour charger mes demandes
|
||||
Future<void> _onChargerMesdemandes(
|
||||
ChargerMesDemandesEvent event,
|
||||
Emitter<DemandesAideState> emit,
|
||||
) async {
|
||||
emit(const DemandesAideLoading());
|
||||
|
||||
final result = await obtenirMesDemandesUseCase(
|
||||
ObtenirMesDemandesParams(utilisateurId: event.utilisateurId),
|
||||
);
|
||||
|
||||
result.fold(
|
||||
(failure) => emit(DemandesAideError(
|
||||
message: _mapFailureToMessage(failure),
|
||||
isNetworkError: failure is NetworkFailure,
|
||||
canRetry: true,
|
||||
)),
|
||||
(demandes) {
|
||||
final demandesFiltrees = _appliquerFiltres(demandes, const FiltresDemandesAide());
|
||||
emit(DemandesAideLoaded(
|
||||
demandes: demandes,
|
||||
demandesFiltrees: demandesFiltrees,
|
||||
hasReachedMax: true,
|
||||
currentPage: 0,
|
||||
totalElements: demandes.length,
|
||||
lastUpdated: DateTime.now(),
|
||||
));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Handler pour rechercher des demandes d'aide
|
||||
Future<void> _onRechercherDemandesAide(
|
||||
RechercherDemandesAideEvent event,
|
||||
Emitter<DemandesAideState> emit,
|
||||
) async {
|
||||
emit(const DemandesAideLoading());
|
||||
|
||||
final result = await rechercherDemandesAideUseCase(
|
||||
RechercherDemandesAideParams(
|
||||
organisationId: event.organisationId,
|
||||
typeAide: event.typeAide,
|
||||
statut: event.statut,
|
||||
demandeurId: event.demandeurId,
|
||||
urgente: event.urgente,
|
||||
page: event.page,
|
||||
taille: event.taille,
|
||||
),
|
||||
);
|
||||
|
||||
result.fold(
|
||||
(failure) => emit(DemandesAideError(
|
||||
message: _mapFailureToMessage(failure),
|
||||
isNetworkError: failure is NetworkFailure,
|
||||
canRetry: true,
|
||||
)),
|
||||
(demandes) {
|
||||
// Appliquer le filtre par mot-clé localement
|
||||
var demandesFiltrees = demandes;
|
||||
if (event.motCle != null && event.motCle!.isNotEmpty) {
|
||||
demandesFiltrees = demandes.where((demande) =>
|
||||
demande.titre.toLowerCase().contains(event.motCle!.toLowerCase()) ||
|
||||
demande.description.toLowerCase().contains(event.motCle!.toLowerCase()) ||
|
||||
demande.nomDemandeur.toLowerCase().contains(event.motCle!.toLowerCase())
|
||||
).toList();
|
||||
}
|
||||
|
||||
emit(DemandesAideLoaded(
|
||||
demandes: demandes,
|
||||
demandesFiltrees: demandesFiltrees,
|
||||
hasReachedMax: demandes.length < event.taille,
|
||||
currentPage: event.page,
|
||||
totalElements: demandes.length,
|
||||
lastUpdated: DateTime.now(),
|
||||
));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Méthode utilitaire pour appliquer les filtres
|
||||
List<DemandeAide> _appliquerFiltres(List<DemandeAide> demandes, FiltresDemandesAide filtres) {
|
||||
var demandesFiltrees = demandes;
|
||||
|
||||
if (filtres.typeAide != null) {
|
||||
demandesFiltrees = demandesFiltrees.where((d) => d.typeAide == filtres.typeAide).toList();
|
||||
}
|
||||
|
||||
if (filtres.statut != null) {
|
||||
demandesFiltrees = demandesFiltrees.where((d) => d.statut == filtres.statut).toList();
|
||||
}
|
||||
|
||||
if (filtres.priorite != null) {
|
||||
demandesFiltrees = demandesFiltrees.where((d) => d.priorite == filtres.priorite).toList();
|
||||
}
|
||||
|
||||
if (filtres.urgente != null) {
|
||||
demandesFiltrees = demandesFiltrees.where((d) => d.estUrgente == filtres.urgente).toList();
|
||||
}
|
||||
|
||||
if (filtres.motCle != null && filtres.motCle!.isNotEmpty) {
|
||||
final motCle = filtres.motCle!.toLowerCase();
|
||||
demandesFiltrees = demandesFiltrees.where((d) =>
|
||||
d.titre.toLowerCase().contains(motCle) ||
|
||||
d.description.toLowerCase().contains(motCle) ||
|
||||
d.nomDemandeur.toLowerCase().contains(motCle)
|
||||
).toList();
|
||||
}
|
||||
|
||||
if (filtres.montantMin != null) {
|
||||
demandesFiltrees = demandesFiltrees.where((d) =>
|
||||
d.montantDemande != null && d.montantDemande! >= filtres.montantMin!
|
||||
).toList();
|
||||
}
|
||||
|
||||
if (filtres.montantMax != null) {
|
||||
demandesFiltrees = demandesFiltrees.where((d) =>
|
||||
d.montantDemande != null && d.montantDemande! <= filtres.montantMax!
|
||||
).toList();
|
||||
}
|
||||
|
||||
if (filtres.dateDebutCreation != null) {
|
||||
demandesFiltrees = demandesFiltrees.where((d) =>
|
||||
d.dateCreation.isAfter(filtres.dateDebutCreation!) ||
|
||||
d.dateCreation.isAtSameMomentAs(filtres.dateDebutCreation!)
|
||||
).toList();
|
||||
}
|
||||
|
||||
if (filtres.dateFinCreation != null) {
|
||||
demandesFiltrees = demandesFiltrees.where((d) =>
|
||||
d.dateCreation.isBefore(filtres.dateFinCreation!) ||
|
||||
d.dateCreation.isAtSameMomentAs(filtres.dateFinCreation!)
|
||||
).toList();
|
||||
}
|
||||
|
||||
return demandesFiltrees;
|
||||
}
|
||||
|
||||
/// Handler pour valider une demande d'aide
|
||||
Future<void> _onValiderDemandeAide(
|
||||
ValiderDemandeAideEvent event,
|
||||
Emitter<DemandesAideState> emit,
|
||||
) async {
|
||||
final result = await validerDemandeAideUseCase(
|
||||
ValiderDemandeAideParams(demande: event.demande),
|
||||
);
|
||||
|
||||
result.fold(
|
||||
(failure) => emit(DemandesAideValidation(
|
||||
erreurs: {'general': _mapFailureToMessage(failure)},
|
||||
isValid: false,
|
||||
demande: event.demande,
|
||||
)),
|
||||
(isValid) => emit(DemandesAideValidation(
|
||||
erreurs: const {},
|
||||
isValid: isValid,
|
||||
demande: event.demande,
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
/// Handler pour calculer la priorité d'une demande
|
||||
Future<void> _onCalculerPrioriteDemande(
|
||||
CalculerPrioriteDemandeEvent event,
|
||||
Emitter<DemandesAideState> emit,
|
||||
) async {
|
||||
final result = await calculerPrioriteDemandeUseCase(
|
||||
CalculerPrioriteDemandeParams(demande: event.demande),
|
||||
);
|
||||
|
||||
result.fold(
|
||||
(failure) => emit(DemandesAideError(
|
||||
message: _mapFailureToMessage(failure),
|
||||
canRetry: false,
|
||||
)),
|
||||
(priorite) {
|
||||
final demandeUpdated = event.demande.copyWith(priorite: priorite);
|
||||
emit(DemandesAideOperationSuccess(
|
||||
message: 'Priorité calculée: ${priorite.libelle}',
|
||||
demande: demandeUpdated,
|
||||
operation: TypeOperationDemande.modification,
|
||||
));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Handler pour filtrer les demandes localement
|
||||
Future<void> _onFiltrerDemandesAide(
|
||||
FiltrerDemandesAideEvent event,
|
||||
Emitter<DemandesAideState> emit,
|
||||
) async {
|
||||
if (state is! DemandesAideLoaded) return;
|
||||
|
||||
final currentState = state as DemandesAideLoaded;
|
||||
final nouveauxFiltres = FiltresDemandesAide(
|
||||
typeAide: event.typeAide,
|
||||
statut: event.statut,
|
||||
priorite: event.priorite,
|
||||
urgente: event.urgente,
|
||||
motCle: event.motCle,
|
||||
);
|
||||
|
||||
final demandesFiltrees = _appliquerFiltres(currentState.demandes, nouveauxFiltres);
|
||||
|
||||
emit(currentState.copyWith(
|
||||
demandesFiltrees: demandesFiltrees,
|
||||
filtres: nouveauxFiltres,
|
||||
));
|
||||
}
|
||||
|
||||
/// Handler pour trier les demandes
|
||||
Future<void> _onTrierDemandesAide(
|
||||
TrierDemandesAideEvent event,
|
||||
Emitter<DemandesAideState> emit,
|
||||
) async {
|
||||
if (state is! DemandesAideLoaded) return;
|
||||
|
||||
final currentState = state as DemandesAideLoaded;
|
||||
final demandesTriees = List<DemandeAide>.from(currentState.demandesFiltrees);
|
||||
|
||||
// Appliquer le tri
|
||||
demandesTriees.sort((a, b) {
|
||||
int comparison = 0;
|
||||
|
||||
switch (event.critere) {
|
||||
case TriDemandes.dateCreation:
|
||||
comparison = a.dateCreation.compareTo(b.dateCreation);
|
||||
break;
|
||||
case TriDemandes.dateModification:
|
||||
comparison = a.dateModification.compareTo(b.dateModification);
|
||||
break;
|
||||
case TriDemandes.titre:
|
||||
comparison = a.titre.compareTo(b.titre);
|
||||
break;
|
||||
case TriDemandes.statut:
|
||||
comparison = a.statut.index.compareTo(b.statut.index);
|
||||
break;
|
||||
case TriDemandes.priorite:
|
||||
comparison = a.priorite.index.compareTo(b.priorite.index);
|
||||
break;
|
||||
case TriDemandes.montant:
|
||||
final montantA = a.montantDemande ?? 0.0;
|
||||
final montantB = b.montantDemande ?? 0.0;
|
||||
comparison = montantA.compareTo(montantB);
|
||||
break;
|
||||
case TriDemandes.demandeur:
|
||||
comparison = a.nomDemandeur.compareTo(b.nomDemandeur);
|
||||
break;
|
||||
}
|
||||
|
||||
return event.croissant ? comparison : -comparison;
|
||||
});
|
||||
|
||||
emit(currentState.copyWith(
|
||||
demandesFiltrees: demandesTriees,
|
||||
criterieTri: event.critere,
|
||||
triCroissant: event.croissant,
|
||||
));
|
||||
}
|
||||
|
||||
/// Handler pour rafraîchir les demandes
|
||||
Future<void> _onRafraichirDemandesAide(
|
||||
RafraichirDemandesAideEvent event,
|
||||
Emitter<DemandesAideState> emit,
|
||||
) async {
|
||||
add(ChargerDemandesAideEvent(
|
||||
organisationId: _lastOrganisationId,
|
||||
typeAide: _lastTypeAide,
|
||||
statut: _lastStatut,
|
||||
demandeurId: _lastDemandeurId,
|
||||
urgente: _lastUrgente,
|
||||
forceRefresh: true,
|
||||
));
|
||||
}
|
||||
|
||||
/// Handler pour réinitialiser l'état
|
||||
Future<void> _onReinitialiserDemandesAide(
|
||||
ReinitialiserDemandesAideEvent event,
|
||||
Emitter<DemandesAideState> emit,
|
||||
) async {
|
||||
_lastOrganisationId = null;
|
||||
_lastTypeAide = null;
|
||||
_lastStatut = null;
|
||||
_lastDemandeurId = null;
|
||||
_lastUrgente = null;
|
||||
|
||||
emit(const DemandesAideInitial());
|
||||
}
|
||||
|
||||
/// Handler pour sélectionner/désélectionner une demande
|
||||
Future<void> _onSelectionnerDemandeAide(
|
||||
SelectionnerDemandeAideEvent event,
|
||||
Emitter<DemandesAideState> emit,
|
||||
) async {
|
||||
if (state is! DemandesAideLoaded) return;
|
||||
|
||||
final currentState = state as DemandesAideLoaded;
|
||||
final nouvellesSelections = Map<String, bool>.from(currentState.demandesSelectionnees);
|
||||
|
||||
if (event.selectionne) {
|
||||
nouvellesSelections[event.demandeId] = true;
|
||||
} else {
|
||||
nouvellesSelections.remove(event.demandeId);
|
||||
}
|
||||
|
||||
emit(currentState.copyWith(demandesSelectionnees: nouvellesSelections));
|
||||
}
|
||||
|
||||
/// Handler pour sélectionner/désélectionner toutes les demandes
|
||||
Future<void> _onSelectionnerToutesDemandesAide(
|
||||
SelectionnerToutesDemandesAideEvent event,
|
||||
Emitter<DemandesAideState> emit,
|
||||
) async {
|
||||
if (state is! DemandesAideLoaded) return;
|
||||
|
||||
final currentState = state as DemandesAideLoaded;
|
||||
final nouvellesSelections = <String, bool>{};
|
||||
|
||||
if (event.selectionne) {
|
||||
for (final demande in currentState.demandesFiltrees) {
|
||||
nouvellesSelections[demande.id] = true;
|
||||
}
|
||||
}
|
||||
|
||||
emit(currentState.copyWith(demandesSelectionnees: nouvellesSelections));
|
||||
}
|
||||
|
||||
/// Handler pour supprimer les demandes sélectionnées
|
||||
Future<void> _onSupprimerDemandesSelectionnees(
|
||||
SupprimerDemandesSelectionnees event,
|
||||
Emitter<DemandesAideState> emit,
|
||||
) async {
|
||||
if (state is! DemandesAideLoaded) return;
|
||||
|
||||
emit(const DemandesAideLoading());
|
||||
|
||||
// Simuler la suppression (à implémenter avec un vrai use case)
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
|
||||
final currentState = state as DemandesAideLoaded;
|
||||
final demandesRestantes = currentState.demandes
|
||||
.where((demande) => !event.demandeIds.contains(demande.id))
|
||||
.toList();
|
||||
|
||||
final demandesFiltrees = _appliquerFiltres(demandesRestantes, currentState.filtres);
|
||||
|
||||
emit(DemandesAideOperationSuccess(
|
||||
message: '${event.demandeIds.length} demande(s) supprimée(s) avec succès',
|
||||
operation: TypeOperationDemande.suppression,
|
||||
));
|
||||
|
||||
emit(currentState.copyWith(
|
||||
demandes: demandesRestantes,
|
||||
demandesFiltrees: demandesFiltrees,
|
||||
demandesSelectionnees: const {},
|
||||
totalElements: demandesRestantes.length,
|
||||
lastUpdated: DateTime.now(),
|
||||
));
|
||||
}
|
||||
|
||||
/// Handler pour exporter les demandes
|
||||
Future<void> _onExporterDemandesAide(
|
||||
ExporterDemandesAideEvent event,
|
||||
Emitter<DemandesAideState> emit,
|
||||
) async {
|
||||
if (state is! DemandesAideLoaded) return;
|
||||
|
||||
emit(const DemandesAideExporting(progress: 0.0, currentStep: 'Préparation...'));
|
||||
|
||||
// Simuler l'export avec progression
|
||||
for (int i = 1; i <= 5; i++) {
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
emit(DemandesAideExporting(
|
||||
progress: i / 5,
|
||||
currentStep: _getExportStep(i, event.format),
|
||||
));
|
||||
}
|
||||
|
||||
// Simuler la génération du fichier
|
||||
final fileName = 'demandes_aide_${DateTime.now().millisecondsSinceEpoch}${event.format.extension}';
|
||||
final filePath = '/storage/emulated/0/Download/$fileName';
|
||||
|
||||
emit(DemandesAideExported(
|
||||
filePath: filePath,
|
||||
format: event.format,
|
||||
nombreDemandes: event.demandeIds.length,
|
||||
));
|
||||
|
||||
emit(DemandesAideOperationSuccess(
|
||||
message: 'Export réalisé avec succès: $fileName',
|
||||
operation: TypeOperationDemande.export,
|
||||
));
|
||||
}
|
||||
|
||||
/// Méthode utilitaire pour obtenir l'étape d'export
|
||||
String _getExportStep(int step, FormatExport format) {
|
||||
switch (step) {
|
||||
case 1:
|
||||
return 'Récupération des données...';
|
||||
case 2:
|
||||
return 'Formatage des données...';
|
||||
case 3:
|
||||
return 'Génération du fichier ${format.libelle}...';
|
||||
case 4:
|
||||
return 'Optimisation...';
|
||||
case 5:
|
||||
return 'Finalisation...';
|
||||
default:
|
||||
return 'Traitement...';
|
||||
}
|
||||
}
|
||||
|
||||
/// Méthode utilitaire pour mapper les erreurs
|
||||
String _mapFailureToMessage(Failure failure) {
|
||||
switch (failure.runtimeType) {
|
||||
case ServerFailure:
|
||||
return 'Erreur serveur. Veuillez réessayer plus tard.';
|
||||
case NetworkFailure:
|
||||
return 'Pas de connexion internet. Vérifiez votre connexion.';
|
||||
case CacheFailure:
|
||||
return 'Erreur de cache local.';
|
||||
case ValidationFailure:
|
||||
return failure.message;
|
||||
case NotFoundFailure:
|
||||
return 'Demande d\'aide non trouvée.';
|
||||
default:
|
||||
return 'Une erreur inattendue s\'est produite.';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,388 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import '../../../domain/entities/demande_aide.dart';
|
||||
|
||||
/// Événements pour la gestion des demandes d'aide
|
||||
///
|
||||
/// Ces événements représentent toutes les actions possibles
|
||||
/// que l'utilisateur peut effectuer sur les demandes d'aide.
|
||||
abstract class DemandesAideEvent extends Equatable {
|
||||
const DemandesAideEvent();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
/// Événement pour charger les demandes d'aide
|
||||
class ChargerDemandesAideEvent extends DemandesAideEvent {
|
||||
final String? organisationId;
|
||||
final TypeAide? typeAide;
|
||||
final StatutAide? statut;
|
||||
final String? demandeurId;
|
||||
final bool? urgente;
|
||||
final bool forceRefresh;
|
||||
|
||||
const ChargerDemandesAideEvent({
|
||||
this.organisationId,
|
||||
this.typeAide,
|
||||
this.statut,
|
||||
this.demandeurId,
|
||||
this.urgente,
|
||||
this.forceRefresh = false,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
organisationId,
|
||||
typeAide,
|
||||
statut,
|
||||
demandeurId,
|
||||
urgente,
|
||||
forceRefresh,
|
||||
];
|
||||
}
|
||||
|
||||
/// Événement pour charger plus de demandes (pagination)
|
||||
class ChargerPlusDemandesAideEvent extends DemandesAideEvent {
|
||||
const ChargerPlusDemandesAideEvent();
|
||||
}
|
||||
|
||||
/// Événement pour créer une nouvelle demande d'aide
|
||||
class CreerDemandeAideEvent extends DemandesAideEvent {
|
||||
final DemandeAide demande;
|
||||
|
||||
const CreerDemandeAideEvent({required this.demande});
|
||||
|
||||
@override
|
||||
List<Object> get props => [demande];
|
||||
}
|
||||
|
||||
/// Événement pour mettre à jour une demande d'aide
|
||||
class MettreAJourDemandeAideEvent extends DemandesAideEvent {
|
||||
final DemandeAide demande;
|
||||
|
||||
const MettreAJourDemandeAideEvent({required this.demande});
|
||||
|
||||
@override
|
||||
List<Object> get props => [demande];
|
||||
}
|
||||
|
||||
/// Événement pour obtenir une demande d'aide spécifique
|
||||
class ObtenirDemandeAideEvent extends DemandesAideEvent {
|
||||
final String demandeId;
|
||||
|
||||
const ObtenirDemandeAideEvent({required this.demandeId});
|
||||
|
||||
@override
|
||||
List<Object> get props => [demandeId];
|
||||
}
|
||||
|
||||
/// Événement pour soumettre une demande d'aide
|
||||
class SoumettreDemandeAideEvent extends DemandesAideEvent {
|
||||
final String demandeId;
|
||||
|
||||
const SoumettreDemandeAideEvent({required this.demandeId});
|
||||
|
||||
@override
|
||||
List<Object> get props => [demandeId];
|
||||
}
|
||||
|
||||
/// Événement pour évaluer une demande d'aide
|
||||
class EvaluerDemandeAideEvent extends DemandesAideEvent {
|
||||
final String demandeId;
|
||||
final String evaluateurId;
|
||||
final StatutAide decision;
|
||||
final String? commentaire;
|
||||
final double? montantApprouve;
|
||||
|
||||
const EvaluerDemandeAideEvent({
|
||||
required this.demandeId,
|
||||
required this.evaluateurId,
|
||||
required this.decision,
|
||||
this.commentaire,
|
||||
this.montantApprouve,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
demandeId,
|
||||
evaluateurId,
|
||||
decision,
|
||||
commentaire,
|
||||
montantApprouve,
|
||||
];
|
||||
}
|
||||
|
||||
/// Événement pour charger les demandes urgentes
|
||||
class ChargerDemandesUrgentesEvent extends DemandesAideEvent {
|
||||
final String organisationId;
|
||||
|
||||
const ChargerDemandesUrgentesEvent({required this.organisationId});
|
||||
|
||||
@override
|
||||
List<Object> get props => [organisationId];
|
||||
}
|
||||
|
||||
/// Événement pour charger mes demandes
|
||||
class ChargerMesDemandesEvent extends DemandesAideEvent {
|
||||
final String utilisateurId;
|
||||
|
||||
const ChargerMesDemandesEvent({required this.utilisateurId});
|
||||
|
||||
@override
|
||||
List<Object> get props => [utilisateurId];
|
||||
}
|
||||
|
||||
/// Événement pour rechercher des demandes d'aide
|
||||
class RechercherDemandesAideEvent extends DemandesAideEvent {
|
||||
final String? organisationId;
|
||||
final TypeAide? typeAide;
|
||||
final StatutAide? statut;
|
||||
final String? demandeurId;
|
||||
final bool? urgente;
|
||||
final String? motCle;
|
||||
final int page;
|
||||
final int taille;
|
||||
|
||||
const RechercherDemandesAideEvent({
|
||||
this.organisationId,
|
||||
this.typeAide,
|
||||
this.statut,
|
||||
this.demandeurId,
|
||||
this.urgente,
|
||||
this.motCle,
|
||||
this.page = 0,
|
||||
this.taille = 20,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
organisationId,
|
||||
typeAide,
|
||||
statut,
|
||||
demandeurId,
|
||||
urgente,
|
||||
motCle,
|
||||
page,
|
||||
taille,
|
||||
];
|
||||
}
|
||||
|
||||
/// Événement pour valider une demande d'aide
|
||||
class ValiderDemandeAideEvent extends DemandesAideEvent {
|
||||
final DemandeAide demande;
|
||||
|
||||
const ValiderDemandeAideEvent({required this.demande});
|
||||
|
||||
@override
|
||||
List<Object> get props => [demande];
|
||||
}
|
||||
|
||||
/// Événement pour calculer la priorité d'une demande
|
||||
class CalculerPrioriteDemandeEvent extends DemandesAideEvent {
|
||||
final DemandeAide demande;
|
||||
|
||||
const CalculerPrioriteDemandeEvent({required this.demande});
|
||||
|
||||
@override
|
||||
List<Object> get props => [demande];
|
||||
}
|
||||
|
||||
/// Événement pour filtrer les demandes localement
|
||||
class FiltrerDemandesAideEvent extends DemandesAideEvent {
|
||||
final TypeAide? typeAide;
|
||||
final StatutAide? statut;
|
||||
final PrioriteAide? priorite;
|
||||
final bool? urgente;
|
||||
final String? motCle;
|
||||
|
||||
const FiltrerDemandesAideEvent({
|
||||
this.typeAide,
|
||||
this.statut,
|
||||
this.priorite,
|
||||
this.urgente,
|
||||
this.motCle,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
typeAide,
|
||||
statut,
|
||||
priorite,
|
||||
urgente,
|
||||
motCle,
|
||||
];
|
||||
}
|
||||
|
||||
/// Événement pour trier les demandes
|
||||
class TrierDemandesAideEvent extends DemandesAideEvent {
|
||||
final TriDemandes critere;
|
||||
final bool croissant;
|
||||
|
||||
const TrierDemandesAideEvent({
|
||||
required this.critere,
|
||||
this.croissant = true,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [critere, croissant];
|
||||
}
|
||||
|
||||
/// Événement pour rafraîchir les demandes
|
||||
class RafraichirDemandesAideEvent extends DemandesAideEvent {
|
||||
const RafraichirDemandesAideEvent();
|
||||
}
|
||||
|
||||
/// Événement pour réinitialiser l'état
|
||||
class ReinitialiserDemandesAideEvent extends DemandesAideEvent {
|
||||
const ReinitialiserDemandesAideEvent();
|
||||
}
|
||||
|
||||
/// Événement pour sélectionner/désélectionner une demande
|
||||
class SelectionnerDemandeAideEvent extends DemandesAideEvent {
|
||||
final String demandeId;
|
||||
final bool selectionne;
|
||||
|
||||
const SelectionnerDemandeAideEvent({
|
||||
required this.demandeId,
|
||||
required this.selectionne,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [demandeId, selectionne];
|
||||
}
|
||||
|
||||
/// Événement pour sélectionner/désélectionner toutes les demandes
|
||||
class SelectionnerToutesDemandesAideEvent extends DemandesAideEvent {
|
||||
final bool selectionne;
|
||||
|
||||
const SelectionnerToutesDemandesAideEvent({required this.selectionne});
|
||||
|
||||
@override
|
||||
List<Object> get props => [selectionne];
|
||||
}
|
||||
|
||||
/// Événement pour supprimer des demandes sélectionnées
|
||||
class SupprimerDemandesSelectionnees extends DemandesAideEvent {
|
||||
final List<String> demandeIds;
|
||||
|
||||
const SupprimerDemandesSelectionnees({required this.demandeIds});
|
||||
|
||||
@override
|
||||
List<Object> get props => [demandeIds];
|
||||
}
|
||||
|
||||
/// Événement pour exporter des demandes
|
||||
class ExporterDemandesAideEvent extends DemandesAideEvent {
|
||||
final List<String> demandeIds;
|
||||
final FormatExport format;
|
||||
|
||||
const ExporterDemandesAideEvent({
|
||||
required this.demandeIds,
|
||||
required this.format,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [demandeIds, format];
|
||||
}
|
||||
|
||||
/// Énumération pour les critères de tri
|
||||
enum TriDemandes {
|
||||
dateCreation,
|
||||
dateModification,
|
||||
titre,
|
||||
statut,
|
||||
priorite,
|
||||
montant,
|
||||
demandeur,
|
||||
}
|
||||
|
||||
/// Énumération pour les formats d'export
|
||||
enum FormatExport {
|
||||
pdf,
|
||||
excel,
|
||||
csv,
|
||||
json,
|
||||
}
|
||||
|
||||
/// Extension pour obtenir le libellé des critères de tri
|
||||
extension TriDemandesExtension on TriDemandes {
|
||||
String get libelle {
|
||||
switch (this) {
|
||||
case TriDemandes.dateCreation:
|
||||
return 'Date de création';
|
||||
case TriDemandes.dateModification:
|
||||
return 'Date de modification';
|
||||
case TriDemandes.titre:
|
||||
return 'Titre';
|
||||
case TriDemandes.statut:
|
||||
return 'Statut';
|
||||
case TriDemandes.priorite:
|
||||
return 'Priorité';
|
||||
case TriDemandes.montant:
|
||||
return 'Montant';
|
||||
case TriDemandes.demandeur:
|
||||
return 'Demandeur';
|
||||
}
|
||||
}
|
||||
|
||||
String get icone {
|
||||
switch (this) {
|
||||
case TriDemandes.dateCreation:
|
||||
return 'calendar_today';
|
||||
case TriDemandes.dateModification:
|
||||
return 'update';
|
||||
case TriDemandes.titre:
|
||||
return 'title';
|
||||
case TriDemandes.statut:
|
||||
return 'flag';
|
||||
case TriDemandes.priorite:
|
||||
return 'priority_high';
|
||||
case TriDemandes.montant:
|
||||
return 'attach_money';
|
||||
case TriDemandes.demandeur:
|
||||
return 'person';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension pour obtenir le libellé des formats d'export
|
||||
extension FormatExportExtension on FormatExport {
|
||||
String get libelle {
|
||||
switch (this) {
|
||||
case FormatExport.pdf:
|
||||
return 'PDF';
|
||||
case FormatExport.excel:
|
||||
return 'Excel';
|
||||
case FormatExport.csv:
|
||||
return 'CSV';
|
||||
case FormatExport.json:
|
||||
return 'JSON';
|
||||
}
|
||||
}
|
||||
|
||||
String get extension {
|
||||
switch (this) {
|
||||
case FormatExport.pdf:
|
||||
return '.pdf';
|
||||
case FormatExport.excel:
|
||||
return '.xlsx';
|
||||
case FormatExport.csv:
|
||||
return '.csv';
|
||||
case FormatExport.json:
|
||||
return '.json';
|
||||
}
|
||||
}
|
||||
|
||||
String get mimeType {
|
||||
switch (this) {
|
||||
case FormatExport.pdf:
|
||||
return 'application/pdf';
|
||||
case FormatExport.excel:
|
||||
return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
||||
case FormatExport.csv:
|
||||
return 'text/csv';
|
||||
case FormatExport.json:
|
||||
return 'application/json';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,434 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import '../../../domain/entities/demande_aide.dart';
|
||||
import 'demandes_aide_event.dart';
|
||||
|
||||
/// États pour la gestion des demandes d'aide
|
||||
///
|
||||
/// Ces états représentent tous les états possibles
|
||||
/// de l'interface utilisateur pour les demandes d'aide.
|
||||
abstract class DemandesAideState extends Equatable {
|
||||
const DemandesAideState();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
/// État initial
|
||||
class DemandesAideInitial extends DemandesAideState {
|
||||
const DemandesAideInitial();
|
||||
}
|
||||
|
||||
/// État de chargement
|
||||
class DemandesAideLoading extends DemandesAideState {
|
||||
final bool isRefreshing;
|
||||
final bool isLoadingMore;
|
||||
|
||||
const DemandesAideLoading({
|
||||
this.isRefreshing = false,
|
||||
this.isLoadingMore = false,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [isRefreshing, isLoadingMore];
|
||||
}
|
||||
|
||||
/// État de succès avec données chargées
|
||||
class DemandesAideLoaded extends DemandesAideState {
|
||||
final List<DemandeAide> demandes;
|
||||
final List<DemandeAide> demandesFiltrees;
|
||||
final bool hasReachedMax;
|
||||
final int currentPage;
|
||||
final int totalElements;
|
||||
final Map<String, bool> demandesSelectionnees;
|
||||
final TriDemandes? criterieTri;
|
||||
final bool triCroissant;
|
||||
final FiltresDemandesAide filtres;
|
||||
final bool isRefreshing;
|
||||
final bool isLoadingMore;
|
||||
final DateTime lastUpdated;
|
||||
|
||||
const DemandesAideLoaded({
|
||||
required this.demandes,
|
||||
required this.demandesFiltrees,
|
||||
this.hasReachedMax = false,
|
||||
this.currentPage = 0,
|
||||
this.totalElements = 0,
|
||||
this.demandesSelectionnees = const {},
|
||||
this.criterieTri,
|
||||
this.triCroissant = true,
|
||||
this.filtres = const FiltresDemandesAide(),
|
||||
this.isRefreshing = false,
|
||||
this.isLoadingMore = false,
|
||||
required this.lastUpdated,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
demandes,
|
||||
demandesFiltrees,
|
||||
hasReachedMax,
|
||||
currentPage,
|
||||
totalElements,
|
||||
demandesSelectionnees,
|
||||
criterieTri,
|
||||
triCroissant,
|
||||
filtres,
|
||||
isRefreshing,
|
||||
isLoadingMore,
|
||||
lastUpdated,
|
||||
];
|
||||
|
||||
/// Copie l'état avec de nouvelles valeurs
|
||||
DemandesAideLoaded copyWith({
|
||||
List<DemandeAide>? demandes,
|
||||
List<DemandeAide>? demandesFiltrees,
|
||||
bool? hasReachedMax,
|
||||
int? currentPage,
|
||||
int? totalElements,
|
||||
Map<String, bool>? demandesSelectionnees,
|
||||
TriDemandes? criterieTri,
|
||||
bool? triCroissant,
|
||||
FiltresDemandesAide? filtres,
|
||||
bool? isRefreshing,
|
||||
bool? isLoadingMore,
|
||||
DateTime? lastUpdated,
|
||||
}) {
|
||||
return DemandesAideLoaded(
|
||||
demandes: demandes ?? this.demandes,
|
||||
demandesFiltrees: demandesFiltrees ?? this.demandesFiltrees,
|
||||
hasReachedMax: hasReachedMax ?? this.hasReachedMax,
|
||||
currentPage: currentPage ?? this.currentPage,
|
||||
totalElements: totalElements ?? this.totalElements,
|
||||
demandesSelectionnees: demandesSelectionnees ?? this.demandesSelectionnees,
|
||||
criterieTri: criterieTri ?? this.criterieTri,
|
||||
triCroissant: triCroissant ?? this.triCroissant,
|
||||
filtres: filtres ?? this.filtres,
|
||||
isRefreshing: isRefreshing ?? this.isRefreshing,
|
||||
isLoadingMore: isLoadingMore ?? this.isLoadingMore,
|
||||
lastUpdated: lastUpdated ?? this.lastUpdated,
|
||||
);
|
||||
}
|
||||
|
||||
/// Obtient le nombre de demandes sélectionnées
|
||||
int get nombreDemandesSelectionnees {
|
||||
return demandesSelectionnees.values.where((selected) => selected).length;
|
||||
}
|
||||
|
||||
/// Vérifie si toutes les demandes sont sélectionnées
|
||||
bool get toutesDemandesSelectionnees {
|
||||
if (demandesFiltrees.isEmpty) return false;
|
||||
return demandesFiltrees.every((demande) =>
|
||||
demandesSelectionnees[demande.id] == true
|
||||
);
|
||||
}
|
||||
|
||||
/// Obtient les IDs des demandes sélectionnées
|
||||
List<String> get demandesSelectionneesIds {
|
||||
return demandesSelectionnees.entries
|
||||
.where((entry) => entry.value)
|
||||
.map((entry) => entry.key)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// Obtient les demandes sélectionnées
|
||||
List<DemandeAide> get demandesSelectionneesEntities {
|
||||
return demandes.where((demande) =>
|
||||
demandesSelectionnees[demande.id] == true
|
||||
).toList();
|
||||
}
|
||||
|
||||
/// Vérifie si des données sont disponibles
|
||||
bool get hasData => demandes.isNotEmpty;
|
||||
|
||||
/// Vérifie si des filtres sont appliqués
|
||||
bool get hasFiltres => !filtres.isEmpty;
|
||||
|
||||
/// Obtient le texte de statut
|
||||
String get statusText {
|
||||
if (isRefreshing) return 'Actualisation...';
|
||||
if (isLoadingMore) return 'Chargement...';
|
||||
if (demandesFiltrees.isEmpty && hasData) return 'Aucun résultat pour les filtres appliqués';
|
||||
if (demandesFiltrees.isEmpty) return 'Aucune demande d\'aide';
|
||||
return '${demandesFiltrees.length} demande${demandesFiltrees.length > 1 ? 's' : ''}';
|
||||
}
|
||||
}
|
||||
|
||||
/// État d'erreur
|
||||
class DemandesAideError extends DemandesAideState {
|
||||
final String message;
|
||||
final String? code;
|
||||
final bool isNetworkError;
|
||||
final bool canRetry;
|
||||
final List<DemandeAide>? cachedData;
|
||||
|
||||
const DemandesAideError({
|
||||
required this.message,
|
||||
this.code,
|
||||
this.isNetworkError = false,
|
||||
this.canRetry = true,
|
||||
this.cachedData,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
message,
|
||||
code,
|
||||
isNetworkError,
|
||||
canRetry,
|
||||
cachedData,
|
||||
];
|
||||
|
||||
/// Vérifie si des données en cache sont disponibles
|
||||
bool get hasCachedData => cachedData != null && cachedData!.isNotEmpty;
|
||||
}
|
||||
|
||||
/// État de succès pour une opération spécifique
|
||||
class DemandesAideOperationSuccess extends DemandesAideState {
|
||||
final String message;
|
||||
final DemandeAide? demande;
|
||||
final TypeOperationDemande operation;
|
||||
|
||||
const DemandesAideOperationSuccess({
|
||||
required this.message,
|
||||
this.demande,
|
||||
required this.operation,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [message, demande, operation];
|
||||
}
|
||||
|
||||
/// État de validation
|
||||
class DemandesAideValidation extends DemandesAideState {
|
||||
final Map<String, String> erreurs;
|
||||
final bool isValid;
|
||||
final DemandeAide? demande;
|
||||
|
||||
const DemandesAideValidation({
|
||||
required this.erreurs,
|
||||
required this.isValid,
|
||||
this.demande,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [erreurs, isValid, demande];
|
||||
|
||||
/// Obtient la première erreur
|
||||
String? get premiereErreur {
|
||||
return erreurs.values.isNotEmpty ? erreurs.values.first : null;
|
||||
}
|
||||
|
||||
/// Obtient les erreurs pour un champ spécifique
|
||||
String? getErreurPourChamp(String champ) {
|
||||
return erreurs[champ];
|
||||
}
|
||||
}
|
||||
|
||||
/// État d'export
|
||||
class DemandesAideExporting extends DemandesAideState {
|
||||
final double progress;
|
||||
final String? currentStep;
|
||||
|
||||
const DemandesAideExporting({
|
||||
required this.progress,
|
||||
this.currentStep,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [progress, currentStep];
|
||||
}
|
||||
|
||||
/// État d'export terminé
|
||||
class DemandesAideExported extends DemandesAideState {
|
||||
final String filePath;
|
||||
final FormatExport format;
|
||||
final int nombreDemandes;
|
||||
|
||||
const DemandesAideExported({
|
||||
required this.filePath,
|
||||
required this.format,
|
||||
required this.nombreDemandes,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [filePath, format, nombreDemandes];
|
||||
}
|
||||
|
||||
/// Classe pour les filtres des demandes d'aide
|
||||
class FiltresDemandesAide extends Equatable {
|
||||
final TypeAide? typeAide;
|
||||
final StatutAide? statut;
|
||||
final PrioriteAide? priorite;
|
||||
final bool? urgente;
|
||||
final String? motCle;
|
||||
final String? organisationId;
|
||||
final String? demandeurId;
|
||||
final DateTime? dateDebutCreation;
|
||||
final DateTime? dateFinCreation;
|
||||
final double? montantMin;
|
||||
final double? montantMax;
|
||||
|
||||
const FiltresDemandesAide({
|
||||
this.typeAide,
|
||||
this.statut,
|
||||
this.priorite,
|
||||
this.urgente,
|
||||
this.motCle,
|
||||
this.organisationId,
|
||||
this.demandeurId,
|
||||
this.dateDebutCreation,
|
||||
this.dateFinCreation,
|
||||
this.montantMin,
|
||||
this.montantMax,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
typeAide,
|
||||
statut,
|
||||
priorite,
|
||||
urgente,
|
||||
motCle,
|
||||
organisationId,
|
||||
demandeurId,
|
||||
dateDebutCreation,
|
||||
dateFinCreation,
|
||||
montantMin,
|
||||
montantMax,
|
||||
];
|
||||
|
||||
/// Copie les filtres avec de nouvelles valeurs
|
||||
FiltresDemandesAide copyWith({
|
||||
TypeAide? typeAide,
|
||||
StatutAide? statut,
|
||||
PrioriteAide? priorite,
|
||||
bool? urgente,
|
||||
String? motCle,
|
||||
String? organisationId,
|
||||
String? demandeurId,
|
||||
DateTime? dateDebutCreation,
|
||||
DateTime? dateFinCreation,
|
||||
double? montantMin,
|
||||
double? montantMax,
|
||||
}) {
|
||||
return FiltresDemandesAide(
|
||||
typeAide: typeAide ?? this.typeAide,
|
||||
statut: statut ?? this.statut,
|
||||
priorite: priorite ?? this.priorite,
|
||||
urgente: urgente ?? this.urgente,
|
||||
motCle: motCle ?? this.motCle,
|
||||
organisationId: organisationId ?? this.organisationId,
|
||||
demandeurId: demandeurId ?? this.demandeurId,
|
||||
dateDebutCreation: dateDebutCreation ?? this.dateDebutCreation,
|
||||
dateFinCreation: dateFinCreation ?? this.dateFinCreation,
|
||||
montantMin: montantMin ?? this.montantMin,
|
||||
montantMax: montantMax ?? this.montantMax,
|
||||
);
|
||||
}
|
||||
|
||||
/// Réinitialise tous les filtres
|
||||
FiltresDemandesAide clear() {
|
||||
return const FiltresDemandesAide();
|
||||
}
|
||||
|
||||
/// Vérifie si les filtres sont vides
|
||||
bool get isEmpty {
|
||||
return typeAide == null &&
|
||||
statut == null &&
|
||||
priorite == null &&
|
||||
urgente == null &&
|
||||
(motCle == null || motCle!.isEmpty) &&
|
||||
organisationId == null &&
|
||||
demandeurId == null &&
|
||||
dateDebutCreation == null &&
|
||||
dateFinCreation == null &&
|
||||
montantMin == null &&
|
||||
montantMax == null;
|
||||
}
|
||||
|
||||
/// Obtient le nombre de filtres actifs
|
||||
int get nombreFiltresActifs {
|
||||
int count = 0;
|
||||
if (typeAide != null) count++;
|
||||
if (statut != null) count++;
|
||||
if (priorite != null) count++;
|
||||
if (urgente != null) count++;
|
||||
if (motCle != null && motCle!.isNotEmpty) count++;
|
||||
if (organisationId != null) count++;
|
||||
if (demandeurId != null) count++;
|
||||
if (dateDebutCreation != null) count++;
|
||||
if (dateFinCreation != null) count++;
|
||||
if (montantMin != null) count++;
|
||||
if (montantMax != null) count++;
|
||||
return count;
|
||||
}
|
||||
|
||||
/// Obtient une description textuelle des filtres
|
||||
String get description {
|
||||
final parts = <String>[];
|
||||
|
||||
if (typeAide != null) parts.add('Type: ${typeAide!.libelle}');
|
||||
if (statut != null) parts.add('Statut: ${statut!.libelle}');
|
||||
if (priorite != null) parts.add('Priorité: ${priorite!.libelle}');
|
||||
if (urgente == true) parts.add('Urgente uniquement');
|
||||
if (motCle != null && motCle!.isNotEmpty) parts.add('Recherche: "$motCle"');
|
||||
if (montantMin != null || montantMax != null) {
|
||||
if (montantMin != null && montantMax != null) {
|
||||
parts.add('Montant: ${montantMin!.toInt()} - ${montantMax!.toInt()} FCFA');
|
||||
} else if (montantMin != null) {
|
||||
parts.add('Montant min: ${montantMin!.toInt()} FCFA');
|
||||
} else {
|
||||
parts.add('Montant max: ${montantMax!.toInt()} FCFA');
|
||||
}
|
||||
}
|
||||
|
||||
return parts.join(', ');
|
||||
}
|
||||
}
|
||||
|
||||
/// Énumération pour les types d'opération
|
||||
enum TypeOperationDemande {
|
||||
creation,
|
||||
modification,
|
||||
soumission,
|
||||
evaluation,
|
||||
suppression,
|
||||
export,
|
||||
}
|
||||
|
||||
/// Extension pour obtenir le libellé des opérations
|
||||
extension TypeOperationDemandeExtension on TypeOperationDemande {
|
||||
String get libelle {
|
||||
switch (this) {
|
||||
case TypeOperationDemande.creation:
|
||||
return 'Création';
|
||||
case TypeOperationDemande.modification:
|
||||
return 'Modification';
|
||||
case TypeOperationDemande.soumission:
|
||||
return 'Soumission';
|
||||
case TypeOperationDemande.evaluation:
|
||||
return 'Évaluation';
|
||||
case TypeOperationDemande.suppression:
|
||||
return 'Suppression';
|
||||
case TypeOperationDemande.export:
|
||||
return 'Export';
|
||||
}
|
||||
}
|
||||
|
||||
String get messageSucces {
|
||||
switch (this) {
|
||||
case TypeOperationDemande.creation:
|
||||
return 'Demande d\'aide créée avec succès';
|
||||
case TypeOperationDemande.modification:
|
||||
return 'Demande d\'aide modifiée avec succès';
|
||||
case TypeOperationDemande.soumission:
|
||||
return 'Demande d\'aide soumise avec succès';
|
||||
case TypeOperationDemande.evaluation:
|
||||
return 'Demande d\'aide évaluée avec succès';
|
||||
case TypeOperationDemande.suppression:
|
||||
return 'Demande d\'aide supprimée avec succès';
|
||||
case TypeOperationDemande.export:
|
||||
return 'Export réalisé avec succès';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,438 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import '../../../domain/entities/evaluation_aide.dart';
|
||||
|
||||
/// Événements pour la gestion des évaluations d'aide
|
||||
///
|
||||
/// Ces événements représentent toutes les actions possibles
|
||||
/// que l'utilisateur peut effectuer sur les évaluations d'aide.
|
||||
abstract class EvaluationsEvent extends Equatable {
|
||||
const EvaluationsEvent();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
/// Événement pour charger les évaluations
|
||||
class ChargerEvaluationsEvent extends EvaluationsEvent {
|
||||
final String? demandeId;
|
||||
final String? evaluateurId;
|
||||
final TypeEvaluateur? typeEvaluateur;
|
||||
final StatutAide? decision;
|
||||
final bool forceRefresh;
|
||||
|
||||
const ChargerEvaluationsEvent({
|
||||
this.demandeId,
|
||||
this.evaluateurId,
|
||||
this.typeEvaluateur,
|
||||
this.decision,
|
||||
this.forceRefresh = false,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
demandeId,
|
||||
evaluateurId,
|
||||
typeEvaluateur,
|
||||
decision,
|
||||
forceRefresh,
|
||||
];
|
||||
}
|
||||
|
||||
/// Événement pour charger plus d'évaluations (pagination)
|
||||
class ChargerPlusEvaluationsEvent extends EvaluationsEvent {
|
||||
const ChargerPlusEvaluationsEvent();
|
||||
}
|
||||
|
||||
/// Événement pour créer une nouvelle évaluation
|
||||
class CreerEvaluationEvent extends EvaluationsEvent {
|
||||
final EvaluationAide evaluation;
|
||||
|
||||
const CreerEvaluationEvent({required this.evaluation});
|
||||
|
||||
@override
|
||||
List<Object> get props => [evaluation];
|
||||
}
|
||||
|
||||
/// Événement pour mettre à jour une évaluation
|
||||
class MettreAJourEvaluationEvent extends EvaluationsEvent {
|
||||
final EvaluationAide evaluation;
|
||||
|
||||
const MettreAJourEvaluationEvent({required this.evaluation});
|
||||
|
||||
@override
|
||||
List<Object> get props => [evaluation];
|
||||
}
|
||||
|
||||
/// Événement pour obtenir une évaluation spécifique
|
||||
class ObtenirEvaluationEvent extends EvaluationsEvent {
|
||||
final String evaluationId;
|
||||
|
||||
const ObtenirEvaluationEvent({required this.evaluationId});
|
||||
|
||||
@override
|
||||
List<Object> get props => [evaluationId];
|
||||
}
|
||||
|
||||
/// Événement pour soumettre une évaluation
|
||||
class SoumettreEvaluationEvent extends EvaluationsEvent {
|
||||
final String evaluationId;
|
||||
|
||||
const SoumettreEvaluationEvent({required this.evaluationId});
|
||||
|
||||
@override
|
||||
List<Object> get props => [evaluationId];
|
||||
}
|
||||
|
||||
/// Événement pour approuver une évaluation
|
||||
class ApprouverEvaluationEvent extends EvaluationsEvent {
|
||||
final String evaluationId;
|
||||
final String? commentaire;
|
||||
|
||||
const ApprouverEvaluationEvent({
|
||||
required this.evaluationId,
|
||||
this.commentaire,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [evaluationId, commentaire];
|
||||
}
|
||||
|
||||
/// Événement pour rejeter une évaluation
|
||||
class RejeterEvaluationEvent extends EvaluationsEvent {
|
||||
final String evaluationId;
|
||||
final String motifRejet;
|
||||
|
||||
const RejeterEvaluationEvent({
|
||||
required this.evaluationId,
|
||||
required this.motifRejet,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [evaluationId, motifRejet];
|
||||
}
|
||||
|
||||
/// Événement pour rechercher des évaluations
|
||||
class RechercherEvaluationsEvent extends EvaluationsEvent {
|
||||
final String? demandeId;
|
||||
final String? evaluateurId;
|
||||
final TypeEvaluateur? typeEvaluateur;
|
||||
final StatutAide? decision;
|
||||
final DateTime? dateDebut;
|
||||
final DateTime? dateFin;
|
||||
final double? noteMin;
|
||||
final double? noteMax;
|
||||
final String? motCle;
|
||||
final int page;
|
||||
final int taille;
|
||||
|
||||
const RechercherEvaluationsEvent({
|
||||
this.demandeId,
|
||||
this.evaluateurId,
|
||||
this.typeEvaluateur,
|
||||
this.decision,
|
||||
this.dateDebut,
|
||||
this.dateFin,
|
||||
this.noteMin,
|
||||
this.noteMax,
|
||||
this.motCle,
|
||||
this.page = 0,
|
||||
this.taille = 20,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
demandeId,
|
||||
evaluateurId,
|
||||
typeEvaluateur,
|
||||
decision,
|
||||
dateDebut,
|
||||
dateFin,
|
||||
noteMin,
|
||||
noteMax,
|
||||
motCle,
|
||||
page,
|
||||
taille,
|
||||
];
|
||||
}
|
||||
|
||||
/// Événement pour charger mes évaluations
|
||||
class ChargerMesEvaluationsEvent extends EvaluationsEvent {
|
||||
final String evaluateurId;
|
||||
|
||||
const ChargerMesEvaluationsEvent({required this.evaluateurId});
|
||||
|
||||
@override
|
||||
List<Object> get props => [evaluateurId];
|
||||
}
|
||||
|
||||
/// Événement pour charger les évaluations en attente
|
||||
class ChargerEvaluationsEnAttenteEvent extends EvaluationsEvent {
|
||||
final String? evaluateurId;
|
||||
final TypeEvaluateur? typeEvaluateur;
|
||||
|
||||
const ChargerEvaluationsEnAttenteEvent({
|
||||
this.evaluateurId,
|
||||
this.typeEvaluateur,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [evaluateurId, typeEvaluateur];
|
||||
}
|
||||
|
||||
/// Événement pour valider une évaluation
|
||||
class ValiderEvaluationEvent extends EvaluationsEvent {
|
||||
final EvaluationAide evaluation;
|
||||
|
||||
const ValiderEvaluationEvent({required this.evaluation});
|
||||
|
||||
@override
|
||||
List<Object> get props => [evaluation];
|
||||
}
|
||||
|
||||
/// Événement pour calculer la note globale
|
||||
class CalculerNoteGlobaleEvent extends EvaluationsEvent {
|
||||
final Map<String, double> criteres;
|
||||
|
||||
const CalculerNoteGlobaleEvent({required this.criteres});
|
||||
|
||||
@override
|
||||
List<Object> get props => [criteres];
|
||||
}
|
||||
|
||||
/// Événement pour filtrer les évaluations localement
|
||||
class FiltrerEvaluationsEvent extends EvaluationsEvent {
|
||||
final TypeEvaluateur? typeEvaluateur;
|
||||
final StatutAide? decision;
|
||||
final double? noteMin;
|
||||
final double? noteMax;
|
||||
final String? motCle;
|
||||
final DateTime? dateDebut;
|
||||
final DateTime? dateFin;
|
||||
|
||||
const FiltrerEvaluationsEvent({
|
||||
this.typeEvaluateur,
|
||||
this.decision,
|
||||
this.noteMin,
|
||||
this.noteMax,
|
||||
this.motCle,
|
||||
this.dateDebut,
|
||||
this.dateFin,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
typeEvaluateur,
|
||||
decision,
|
||||
noteMin,
|
||||
noteMax,
|
||||
motCle,
|
||||
dateDebut,
|
||||
dateFin,
|
||||
];
|
||||
}
|
||||
|
||||
/// Événement pour trier les évaluations
|
||||
class TrierEvaluationsEvent extends EvaluationsEvent {
|
||||
final TriEvaluations critere;
|
||||
final bool croissant;
|
||||
|
||||
const TrierEvaluationsEvent({
|
||||
required this.critere,
|
||||
this.croissant = true,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [critere, croissant];
|
||||
}
|
||||
|
||||
/// Événement pour rafraîchir les évaluations
|
||||
class RafraichirEvaluationsEvent extends EvaluationsEvent {
|
||||
const RafraichirEvaluationsEvent();
|
||||
}
|
||||
|
||||
/// Événement pour réinitialiser l'état
|
||||
class ReinitialiserEvaluationsEvent extends EvaluationsEvent {
|
||||
const ReinitialiserEvaluationsEvent();
|
||||
}
|
||||
|
||||
/// Événement pour sélectionner/désélectionner une évaluation
|
||||
class SelectionnerEvaluationEvent extends EvaluationsEvent {
|
||||
final String evaluationId;
|
||||
final bool selectionne;
|
||||
|
||||
const SelectionnerEvaluationEvent({
|
||||
required this.evaluationId,
|
||||
required this.selectionne,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [evaluationId, selectionne];
|
||||
}
|
||||
|
||||
/// Événement pour sélectionner/désélectionner toutes les évaluations
|
||||
class SelectionnerToutesEvaluationsEvent extends EvaluationsEvent {
|
||||
final bool selectionne;
|
||||
|
||||
const SelectionnerToutesEvaluationsEvent({required this.selectionne});
|
||||
|
||||
@override
|
||||
List<Object> get props => [selectionne];
|
||||
}
|
||||
|
||||
/// Événement pour supprimer des évaluations sélectionnées
|
||||
class SupprimerEvaluationsSelectionnees extends EvaluationsEvent {
|
||||
final List<String> evaluationIds;
|
||||
|
||||
const SupprimerEvaluationsSelectionnees({required this.evaluationIds});
|
||||
|
||||
@override
|
||||
List<Object> get props => [evaluationIds];
|
||||
}
|
||||
|
||||
/// Événement pour exporter des évaluations
|
||||
class ExporterEvaluationsEvent extends EvaluationsEvent {
|
||||
final List<String> evaluationIds;
|
||||
final FormatExport format;
|
||||
|
||||
const ExporterEvaluationsEvent({
|
||||
required this.evaluationIds,
|
||||
required this.format,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [evaluationIds, format];
|
||||
}
|
||||
|
||||
/// Événement pour obtenir les statistiques d'évaluation
|
||||
class ObtenirStatistiquesEvaluationEvent extends EvaluationsEvent {
|
||||
final String? evaluateurId;
|
||||
final DateTime? dateDebut;
|
||||
final DateTime? dateFin;
|
||||
|
||||
const ObtenirStatistiquesEvaluationEvent({
|
||||
this.evaluateurId,
|
||||
this.dateDebut,
|
||||
this.dateFin,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [evaluateurId, dateDebut, dateFin];
|
||||
}
|
||||
|
||||
/// Événement pour signaler une évaluation
|
||||
class SignalerEvaluationEvent extends EvaluationsEvent {
|
||||
final String evaluationId;
|
||||
final String motifSignalement;
|
||||
final String? description;
|
||||
|
||||
const SignalerEvaluationEvent({
|
||||
required this.evaluationId,
|
||||
required this.motifSignalement,
|
||||
this.description,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [evaluationId, motifSignalement, description];
|
||||
}
|
||||
|
||||
/// Énumération pour les critères de tri
|
||||
enum TriEvaluations {
|
||||
dateEvaluation,
|
||||
dateCreation,
|
||||
noteGlobale,
|
||||
decision,
|
||||
evaluateur,
|
||||
typeEvaluateur,
|
||||
demandeId,
|
||||
}
|
||||
|
||||
/// Énumération pour les formats d'export
|
||||
enum FormatExport {
|
||||
pdf,
|
||||
excel,
|
||||
csv,
|
||||
json,
|
||||
}
|
||||
|
||||
/// Extension pour obtenir le libellé des critères de tri
|
||||
extension TriEvaluationsExtension on TriEvaluations {
|
||||
String get libelle {
|
||||
switch (this) {
|
||||
case TriEvaluations.dateEvaluation:
|
||||
return 'Date d\'évaluation';
|
||||
case TriEvaluations.dateCreation:
|
||||
return 'Date de création';
|
||||
case TriEvaluations.noteGlobale:
|
||||
return 'Note globale';
|
||||
case TriEvaluations.decision:
|
||||
return 'Décision';
|
||||
case TriEvaluations.evaluateur:
|
||||
return 'Évaluateur';
|
||||
case TriEvaluations.typeEvaluateur:
|
||||
return 'Type d\'évaluateur';
|
||||
case TriEvaluations.demandeId:
|
||||
return 'Demande';
|
||||
}
|
||||
}
|
||||
|
||||
String get icone {
|
||||
switch (this) {
|
||||
case TriEvaluations.dateEvaluation:
|
||||
return 'calendar_today';
|
||||
case TriEvaluations.dateCreation:
|
||||
return 'schedule';
|
||||
case TriEvaluations.noteGlobale:
|
||||
return 'star';
|
||||
case TriEvaluations.decision:
|
||||
return 'gavel';
|
||||
case TriEvaluations.evaluateur:
|
||||
return 'person';
|
||||
case TriEvaluations.typeEvaluateur:
|
||||
return 'badge';
|
||||
case TriEvaluations.demandeId:
|
||||
return 'description';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension pour obtenir le libellé des formats d'export
|
||||
extension FormatExportExtension on FormatExport {
|
||||
String get libelle {
|
||||
switch (this) {
|
||||
case FormatExport.pdf:
|
||||
return 'PDF';
|
||||
case FormatExport.excel:
|
||||
return 'Excel';
|
||||
case FormatExport.csv:
|
||||
return 'CSV';
|
||||
case FormatExport.json:
|
||||
return 'JSON';
|
||||
}
|
||||
}
|
||||
|
||||
String get extension {
|
||||
switch (this) {
|
||||
case FormatExport.pdf:
|
||||
return '.pdf';
|
||||
case FormatExport.excel:
|
||||
return '.xlsx';
|
||||
case FormatExport.csv:
|
||||
return '.csv';
|
||||
case FormatExport.json:
|
||||
return '.json';
|
||||
}
|
||||
}
|
||||
|
||||
String get mimeType {
|
||||
switch (this) {
|
||||
case FormatExport.pdf:
|
||||
return 'application/pdf';
|
||||
case FormatExport.excel:
|
||||
return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
||||
case FormatExport.csv:
|
||||
return 'text/csv';
|
||||
case FormatExport.json:
|
||||
return 'application/json';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,478 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import '../../../domain/entities/evaluation_aide.dart';
|
||||
import 'evaluations_event.dart';
|
||||
|
||||
/// États pour la gestion des évaluations d'aide
|
||||
///
|
||||
/// Ces états représentent tous les états possibles
|
||||
/// de l'interface utilisateur pour les évaluations d'aide.
|
||||
abstract class EvaluationsState extends Equatable {
|
||||
const EvaluationsState();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
/// État initial
|
||||
class EvaluationsInitial extends EvaluationsState {
|
||||
const EvaluationsInitial();
|
||||
}
|
||||
|
||||
/// État de chargement
|
||||
class EvaluationsLoading extends EvaluationsState {
|
||||
final bool isRefreshing;
|
||||
final bool isLoadingMore;
|
||||
|
||||
const EvaluationsLoading({
|
||||
this.isRefreshing = false,
|
||||
this.isLoadingMore = false,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [isRefreshing, isLoadingMore];
|
||||
}
|
||||
|
||||
/// État de succès avec données chargées
|
||||
class EvaluationsLoaded extends EvaluationsState {
|
||||
final List<EvaluationAide> evaluations;
|
||||
final List<EvaluationAide> evaluationsFiltrees;
|
||||
final bool hasReachedMax;
|
||||
final int currentPage;
|
||||
final int totalElements;
|
||||
final Map<String, bool> evaluationsSelectionnees;
|
||||
final TriEvaluations? criterieTri;
|
||||
final bool triCroissant;
|
||||
final FiltresEvaluations filtres;
|
||||
final bool isRefreshing;
|
||||
final bool isLoadingMore;
|
||||
final DateTime lastUpdated;
|
||||
|
||||
const EvaluationsLoaded({
|
||||
required this.evaluations,
|
||||
required this.evaluationsFiltrees,
|
||||
this.hasReachedMax = false,
|
||||
this.currentPage = 0,
|
||||
this.totalElements = 0,
|
||||
this.evaluationsSelectionnees = const {},
|
||||
this.criterieTri,
|
||||
this.triCroissant = true,
|
||||
this.filtres = const FiltresEvaluations(),
|
||||
this.isRefreshing = false,
|
||||
this.isLoadingMore = false,
|
||||
required this.lastUpdated,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
evaluations,
|
||||
evaluationsFiltrees,
|
||||
hasReachedMax,
|
||||
currentPage,
|
||||
totalElements,
|
||||
evaluationsSelectionnees,
|
||||
criterieTri,
|
||||
triCroissant,
|
||||
filtres,
|
||||
isRefreshing,
|
||||
isLoadingMore,
|
||||
lastUpdated,
|
||||
];
|
||||
|
||||
/// Copie l'état avec de nouvelles valeurs
|
||||
EvaluationsLoaded copyWith({
|
||||
List<EvaluationAide>? evaluations,
|
||||
List<EvaluationAide>? evaluationsFiltrees,
|
||||
bool? hasReachedMax,
|
||||
int? currentPage,
|
||||
int? totalElements,
|
||||
Map<String, bool>? evaluationsSelectionnees,
|
||||
TriEvaluations? criterieTri,
|
||||
bool? triCroissant,
|
||||
FiltresEvaluations? filtres,
|
||||
bool? isRefreshing,
|
||||
bool? isLoadingMore,
|
||||
DateTime? lastUpdated,
|
||||
}) {
|
||||
return EvaluationsLoaded(
|
||||
evaluations: evaluations ?? this.evaluations,
|
||||
evaluationsFiltrees: evaluationsFiltrees ?? this.evaluationsFiltrees,
|
||||
hasReachedMax: hasReachedMax ?? this.hasReachedMax,
|
||||
currentPage: currentPage ?? this.currentPage,
|
||||
totalElements: totalElements ?? this.totalElements,
|
||||
evaluationsSelectionnees: evaluationsSelectionnees ?? this.evaluationsSelectionnees,
|
||||
criterieTri: criterieTri ?? this.criterieTri,
|
||||
triCroissant: triCroissant ?? this.triCroissant,
|
||||
filtres: filtres ?? this.filtres,
|
||||
isRefreshing: isRefreshing ?? this.isRefreshing,
|
||||
isLoadingMore: isLoadingMore ?? this.isLoadingMore,
|
||||
lastUpdated: lastUpdated ?? this.lastUpdated,
|
||||
);
|
||||
}
|
||||
|
||||
/// Obtient le nombre d'évaluations sélectionnées
|
||||
int get nombreEvaluationsSelectionnees {
|
||||
return evaluationsSelectionnees.values.where((selected) => selected).length;
|
||||
}
|
||||
|
||||
/// Vérifie si toutes les évaluations sont sélectionnées
|
||||
bool get toutesEvaluationsSelectionnees {
|
||||
if (evaluationsFiltrees.isEmpty) return false;
|
||||
return evaluationsFiltrees.every((evaluation) =>
|
||||
evaluationsSelectionnees[evaluation.id] == true
|
||||
);
|
||||
}
|
||||
|
||||
/// Obtient les IDs des évaluations sélectionnées
|
||||
List<String> get evaluationsSelectionneesIds {
|
||||
return evaluationsSelectionnees.entries
|
||||
.where((entry) => entry.value)
|
||||
.map((entry) => entry.key)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// Obtient les évaluations sélectionnées
|
||||
List<EvaluationAide> get evaluationsSelectionneesEntities {
|
||||
return evaluations.where((evaluation) =>
|
||||
evaluationsSelectionnees[evaluation.id] == true
|
||||
).toList();
|
||||
}
|
||||
|
||||
/// Vérifie si des données sont disponibles
|
||||
bool get hasData => evaluations.isNotEmpty;
|
||||
|
||||
/// Vérifie si des filtres sont appliqués
|
||||
bool get hasFiltres => !filtres.isEmpty;
|
||||
|
||||
/// Obtient le texte de statut
|
||||
String get statusText {
|
||||
if (isRefreshing) return 'Actualisation...';
|
||||
if (isLoadingMore) return 'Chargement...';
|
||||
if (evaluationsFiltrees.isEmpty && hasData) return 'Aucun résultat pour les filtres appliqués';
|
||||
if (evaluationsFiltrees.isEmpty) return 'Aucune évaluation';
|
||||
return '${evaluationsFiltrees.length} évaluation${evaluationsFiltrees.length > 1 ? 's' : ''}';
|
||||
}
|
||||
|
||||
/// Obtient la note moyenne
|
||||
double get noteMoyenne {
|
||||
if (evaluationsFiltrees.isEmpty) return 0.0;
|
||||
final notesValides = evaluationsFiltrees
|
||||
.where((e) => e.noteGlobale != null)
|
||||
.map((e) => e.noteGlobale!)
|
||||
.toList();
|
||||
if (notesValides.isEmpty) return 0.0;
|
||||
return notesValides.reduce((a, b) => a + b) / notesValides.length;
|
||||
}
|
||||
|
||||
/// Obtient le nombre d'évaluations par décision
|
||||
Map<StatutAide, int> get repartitionDecisions {
|
||||
final repartition = <StatutAide, int>{};
|
||||
for (final evaluation in evaluationsFiltrees) {
|
||||
repartition[evaluation.decision] = (repartition[evaluation.decision] ?? 0) + 1;
|
||||
}
|
||||
return repartition;
|
||||
}
|
||||
}
|
||||
|
||||
/// État d'erreur
|
||||
class EvaluationsError extends EvaluationsState {
|
||||
final String message;
|
||||
final String? code;
|
||||
final bool isNetworkError;
|
||||
final bool canRetry;
|
||||
final List<EvaluationAide>? cachedData;
|
||||
|
||||
const EvaluationsError({
|
||||
required this.message,
|
||||
this.code,
|
||||
this.isNetworkError = false,
|
||||
this.canRetry = true,
|
||||
this.cachedData,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
message,
|
||||
code,
|
||||
isNetworkError,
|
||||
canRetry,
|
||||
cachedData,
|
||||
];
|
||||
|
||||
/// Vérifie si des données en cache sont disponibles
|
||||
bool get hasCachedData => cachedData != null && cachedData!.isNotEmpty;
|
||||
}
|
||||
|
||||
/// État de succès pour une opération spécifique
|
||||
class EvaluationsOperationSuccess extends EvaluationsState {
|
||||
final String message;
|
||||
final EvaluationAide? evaluation;
|
||||
final TypeOperationEvaluation operation;
|
||||
|
||||
const EvaluationsOperationSuccess({
|
||||
required this.message,
|
||||
this.evaluation,
|
||||
required this.operation,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [message, evaluation, operation];
|
||||
}
|
||||
|
||||
/// État de validation
|
||||
class EvaluationsValidation extends EvaluationsState {
|
||||
final Map<String, String> erreurs;
|
||||
final bool isValid;
|
||||
final EvaluationAide? evaluation;
|
||||
|
||||
const EvaluationsValidation({
|
||||
required this.erreurs,
|
||||
required this.isValid,
|
||||
this.evaluation,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [erreurs, isValid, evaluation];
|
||||
|
||||
/// Obtient la première erreur
|
||||
String? get premiereErreur {
|
||||
return erreurs.values.isNotEmpty ? erreurs.values.first : null;
|
||||
}
|
||||
|
||||
/// Obtient les erreurs pour un champ spécifique
|
||||
String? getErreurPourChamp(String champ) {
|
||||
return erreurs[champ];
|
||||
}
|
||||
}
|
||||
|
||||
/// État de calcul de note globale
|
||||
class EvaluationsNoteCalculee extends EvaluationsState {
|
||||
final double noteGlobale;
|
||||
final Map<String, double> criteres;
|
||||
|
||||
const EvaluationsNoteCalculee({
|
||||
required this.noteGlobale,
|
||||
required this.criteres,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [noteGlobale, criteres];
|
||||
}
|
||||
|
||||
/// État des statistiques d'évaluation
|
||||
class EvaluationsStatistiques extends EvaluationsState {
|
||||
final Map<String, dynamic> statistiques;
|
||||
final DateTime? dateDebut;
|
||||
final DateTime? dateFin;
|
||||
|
||||
const EvaluationsStatistiques({
|
||||
required this.statistiques,
|
||||
this.dateDebut,
|
||||
this.dateFin,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [statistiques, dateDebut, dateFin];
|
||||
}
|
||||
|
||||
/// État d'export
|
||||
class EvaluationsExporting extends EvaluationsState {
|
||||
final double progress;
|
||||
final String? currentStep;
|
||||
|
||||
const EvaluationsExporting({
|
||||
required this.progress,
|
||||
this.currentStep,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [progress, currentStep];
|
||||
}
|
||||
|
||||
/// État d'export terminé
|
||||
class EvaluationsExported extends EvaluationsState {
|
||||
final String filePath;
|
||||
final FormatExport format;
|
||||
final int nombreEvaluations;
|
||||
|
||||
const EvaluationsExported({
|
||||
required this.filePath,
|
||||
required this.format,
|
||||
required this.nombreEvaluations,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [filePath, format, nombreEvaluations];
|
||||
}
|
||||
|
||||
/// Classe pour les filtres des évaluations
|
||||
class FiltresEvaluations extends Equatable {
|
||||
final TypeEvaluateur? typeEvaluateur;
|
||||
final StatutAide? decision;
|
||||
final double? noteMin;
|
||||
final double? noteMax;
|
||||
final String? motCle;
|
||||
final String? evaluateurId;
|
||||
final String? demandeId;
|
||||
final DateTime? dateDebutEvaluation;
|
||||
final DateTime? dateFinEvaluation;
|
||||
|
||||
const FiltresEvaluations({
|
||||
this.typeEvaluateur,
|
||||
this.decision,
|
||||
this.noteMin,
|
||||
this.noteMax,
|
||||
this.motCle,
|
||||
this.evaluateurId,
|
||||
this.demandeId,
|
||||
this.dateDebutEvaluation,
|
||||
this.dateFinEvaluation,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
typeEvaluateur,
|
||||
decision,
|
||||
noteMin,
|
||||
noteMax,
|
||||
motCle,
|
||||
evaluateurId,
|
||||
demandeId,
|
||||
dateDebutEvaluation,
|
||||
dateFinEvaluation,
|
||||
];
|
||||
|
||||
/// Copie les filtres avec de nouvelles valeurs
|
||||
FiltresEvaluations copyWith({
|
||||
TypeEvaluateur? typeEvaluateur,
|
||||
StatutAide? decision,
|
||||
double? noteMin,
|
||||
double? noteMax,
|
||||
String? motCle,
|
||||
String? evaluateurId,
|
||||
String? demandeId,
|
||||
DateTime? dateDebutEvaluation,
|
||||
DateTime? dateFinEvaluation,
|
||||
}) {
|
||||
return FiltresEvaluations(
|
||||
typeEvaluateur: typeEvaluateur ?? this.typeEvaluateur,
|
||||
decision: decision ?? this.decision,
|
||||
noteMin: noteMin ?? this.noteMin,
|
||||
noteMax: noteMax ?? this.noteMax,
|
||||
motCle: motCle ?? this.motCle,
|
||||
evaluateurId: evaluateurId ?? this.evaluateurId,
|
||||
demandeId: demandeId ?? this.demandeId,
|
||||
dateDebutEvaluation: dateDebutEvaluation ?? this.dateDebutEvaluation,
|
||||
dateFinEvaluation: dateFinEvaluation ?? this.dateFinEvaluation,
|
||||
);
|
||||
}
|
||||
|
||||
/// Réinitialise tous les filtres
|
||||
FiltresEvaluations clear() {
|
||||
return const FiltresEvaluations();
|
||||
}
|
||||
|
||||
/// Vérifie si les filtres sont vides
|
||||
bool get isEmpty {
|
||||
return typeEvaluateur == null &&
|
||||
decision == null &&
|
||||
noteMin == null &&
|
||||
noteMax == null &&
|
||||
(motCle == null || motCle!.isEmpty) &&
|
||||
evaluateurId == null &&
|
||||
demandeId == null &&
|
||||
dateDebutEvaluation == null &&
|
||||
dateFinEvaluation == null;
|
||||
}
|
||||
|
||||
/// Obtient le nombre de filtres actifs
|
||||
int get nombreFiltresActifs {
|
||||
int count = 0;
|
||||
if (typeEvaluateur != null) count++;
|
||||
if (decision != null) count++;
|
||||
if (noteMin != null) count++;
|
||||
if (noteMax != null) count++;
|
||||
if (motCle != null && motCle!.isNotEmpty) count++;
|
||||
if (evaluateurId != null) count++;
|
||||
if (demandeId != null) count++;
|
||||
if (dateDebutEvaluation != null) count++;
|
||||
if (dateFinEvaluation != null) count++;
|
||||
return count;
|
||||
}
|
||||
|
||||
/// Obtient une description textuelle des filtres
|
||||
String get description {
|
||||
final parts = <String>[];
|
||||
|
||||
if (typeEvaluateur != null) parts.add('Type: ${typeEvaluateur!.libelle}');
|
||||
if (decision != null) parts.add('Décision: ${decision!.libelle}');
|
||||
if (motCle != null && motCle!.isNotEmpty) parts.add('Recherche: "$motCle"');
|
||||
if (noteMin != null || noteMax != null) {
|
||||
if (noteMin != null && noteMax != null) {
|
||||
parts.add('Note: ${noteMin!.toStringAsFixed(1)} - ${noteMax!.toStringAsFixed(1)}');
|
||||
} else if (noteMin != null) {
|
||||
parts.add('Note min: ${noteMin!.toStringAsFixed(1)}');
|
||||
} else {
|
||||
parts.add('Note max: ${noteMax!.toStringAsFixed(1)}');
|
||||
}
|
||||
}
|
||||
|
||||
return parts.join(', ');
|
||||
}
|
||||
}
|
||||
|
||||
/// Énumération pour les types d'opération
|
||||
enum TypeOperationEvaluation {
|
||||
creation,
|
||||
modification,
|
||||
soumission,
|
||||
approbation,
|
||||
rejet,
|
||||
suppression,
|
||||
export,
|
||||
signalement,
|
||||
}
|
||||
|
||||
/// Extension pour obtenir le libellé des opérations
|
||||
extension TypeOperationEvaluationExtension on TypeOperationEvaluation {
|
||||
String get libelle {
|
||||
switch (this) {
|
||||
case TypeOperationEvaluation.creation:
|
||||
return 'Création';
|
||||
case TypeOperationEvaluation.modification:
|
||||
return 'Modification';
|
||||
case TypeOperationEvaluation.soumission:
|
||||
return 'Soumission';
|
||||
case TypeOperationEvaluation.approbation:
|
||||
return 'Approbation';
|
||||
case TypeOperationEvaluation.rejet:
|
||||
return 'Rejet';
|
||||
case TypeOperationEvaluation.suppression:
|
||||
return 'Suppression';
|
||||
case TypeOperationEvaluation.export:
|
||||
return 'Export';
|
||||
case TypeOperationEvaluation.signalement:
|
||||
return 'Signalement';
|
||||
}
|
||||
}
|
||||
|
||||
String get messageSucces {
|
||||
switch (this) {
|
||||
case TypeOperationEvaluation.creation:
|
||||
return 'Évaluation créée avec succès';
|
||||
case TypeOperationEvaluation.modification:
|
||||
return 'Évaluation modifiée avec succès';
|
||||
case TypeOperationEvaluation.soumission:
|
||||
return 'Évaluation soumise avec succès';
|
||||
case TypeOperationEvaluation.approbation:
|
||||
return 'Évaluation approuvée avec succès';
|
||||
case TypeOperationEvaluation.rejet:
|
||||
return 'Évaluation rejetée avec succès';
|
||||
case TypeOperationEvaluation.suppression:
|
||||
return 'Évaluation supprimée avec succès';
|
||||
case TypeOperationEvaluation.export:
|
||||
return 'Export réalisé avec succès';
|
||||
case TypeOperationEvaluation.signalement:
|
||||
return 'Évaluation signalée avec succès';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,382 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import '../../../domain/entities/proposition_aide.dart';
|
||||
|
||||
/// Événements pour la gestion des propositions d'aide
|
||||
///
|
||||
/// Ces événements représentent toutes les actions possibles
|
||||
/// que l'utilisateur peut effectuer sur les propositions d'aide.
|
||||
abstract class PropositionsAideEvent extends Equatable {
|
||||
const PropositionsAideEvent();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
/// Événement pour charger les propositions d'aide
|
||||
class ChargerPropositionsAideEvent extends PropositionsAideEvent {
|
||||
final String? organisationId;
|
||||
final TypeAide? typeAide;
|
||||
final StatutProposition? statut;
|
||||
final String? proposantId;
|
||||
final bool? disponible;
|
||||
final bool forceRefresh;
|
||||
|
||||
const ChargerPropositionsAideEvent({
|
||||
this.organisationId,
|
||||
this.typeAide,
|
||||
this.statut,
|
||||
this.proposantId,
|
||||
this.disponible,
|
||||
this.forceRefresh = false,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
organisationId,
|
||||
typeAide,
|
||||
statut,
|
||||
proposantId,
|
||||
disponible,
|
||||
forceRefresh,
|
||||
];
|
||||
}
|
||||
|
||||
/// Événement pour charger plus de propositions (pagination)
|
||||
class ChargerPlusPropositionsAideEvent extends PropositionsAideEvent {
|
||||
const ChargerPlusPropositionsAideEvent();
|
||||
}
|
||||
|
||||
/// Événement pour créer une nouvelle proposition d'aide
|
||||
class CreerPropositionAideEvent extends PropositionsAideEvent {
|
||||
final PropositionAide proposition;
|
||||
|
||||
const CreerPropositionAideEvent({required this.proposition});
|
||||
|
||||
@override
|
||||
List<Object> get props => [proposition];
|
||||
}
|
||||
|
||||
/// Événement pour mettre à jour une proposition d'aide
|
||||
class MettreAJourPropositionAideEvent extends PropositionsAideEvent {
|
||||
final PropositionAide proposition;
|
||||
|
||||
const MettreAJourPropositionAideEvent({required this.proposition});
|
||||
|
||||
@override
|
||||
List<Object> get props => [proposition];
|
||||
}
|
||||
|
||||
/// Événement pour obtenir une proposition d'aide spécifique
|
||||
class ObtenirPropositionAideEvent extends PropositionsAideEvent {
|
||||
final String propositionId;
|
||||
|
||||
const ObtenirPropositionAideEvent({required this.propositionId});
|
||||
|
||||
@override
|
||||
List<Object> get props => [propositionId];
|
||||
}
|
||||
|
||||
/// Événement pour activer/désactiver une proposition
|
||||
class ToggleDisponibilitePropositionEvent extends PropositionsAideEvent {
|
||||
final String propositionId;
|
||||
final bool disponible;
|
||||
|
||||
const ToggleDisponibilitePropositionEvent({
|
||||
required this.propositionId,
|
||||
required this.disponible,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [propositionId, disponible];
|
||||
}
|
||||
|
||||
/// Événement pour rechercher des propositions d'aide
|
||||
class RechercherPropositionsAideEvent extends PropositionsAideEvent {
|
||||
final String? organisationId;
|
||||
final TypeAide? typeAide;
|
||||
final StatutProposition? statut;
|
||||
final String? proposantId;
|
||||
final bool? disponible;
|
||||
final String? motCle;
|
||||
final int page;
|
||||
final int taille;
|
||||
|
||||
const RechercherPropositionsAideEvent({
|
||||
this.organisationId,
|
||||
this.typeAide,
|
||||
this.statut,
|
||||
this.proposantId,
|
||||
this.disponible,
|
||||
this.motCle,
|
||||
this.page = 0,
|
||||
this.taille = 20,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
organisationId,
|
||||
typeAide,
|
||||
statut,
|
||||
proposantId,
|
||||
disponible,
|
||||
motCle,
|
||||
page,
|
||||
taille,
|
||||
];
|
||||
}
|
||||
|
||||
/// Événement pour charger mes propositions
|
||||
class ChargerMesPropositionsEvent extends PropositionsAideEvent {
|
||||
final String utilisateurId;
|
||||
|
||||
const ChargerMesPropositionsEvent({required this.utilisateurId});
|
||||
|
||||
@override
|
||||
List<Object> get props => [utilisateurId];
|
||||
}
|
||||
|
||||
/// Événement pour charger les propositions disponibles
|
||||
class ChargerPropositionsDisponiblesEvent extends PropositionsAideEvent {
|
||||
final String organisationId;
|
||||
final TypeAide? typeAide;
|
||||
|
||||
const ChargerPropositionsDisponiblesEvent({
|
||||
required this.organisationId,
|
||||
this.typeAide,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [organisationId, typeAide];
|
||||
}
|
||||
|
||||
/// Événement pour filtrer les propositions localement
|
||||
class FiltrerPropositionsAideEvent extends PropositionsAideEvent {
|
||||
final TypeAide? typeAide;
|
||||
final StatutProposition? statut;
|
||||
final bool? disponible;
|
||||
final String? motCle;
|
||||
final double? capaciteMin;
|
||||
final double? capaciteMax;
|
||||
|
||||
const FiltrerPropositionsAideEvent({
|
||||
this.typeAide,
|
||||
this.statut,
|
||||
this.disponible,
|
||||
this.motCle,
|
||||
this.capaciteMin,
|
||||
this.capaciteMax,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
typeAide,
|
||||
statut,
|
||||
disponible,
|
||||
motCle,
|
||||
capaciteMin,
|
||||
capaciteMax,
|
||||
];
|
||||
}
|
||||
|
||||
/// Événement pour trier les propositions
|
||||
class TrierPropositionsAideEvent extends PropositionsAideEvent {
|
||||
final TriPropositions critere;
|
||||
final bool croissant;
|
||||
|
||||
const TrierPropositionsAideEvent({
|
||||
required this.critere,
|
||||
this.croissant = true,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [critere, croissant];
|
||||
}
|
||||
|
||||
/// Événement pour rafraîchir les propositions
|
||||
class RafraichirPropositionsAideEvent extends PropositionsAideEvent {
|
||||
const RafraichirPropositionsAideEvent();
|
||||
}
|
||||
|
||||
/// Événement pour réinitialiser l'état
|
||||
class ReinitialiserPropositionsAideEvent extends PropositionsAideEvent {
|
||||
const ReinitialiserPropositionsAideEvent();
|
||||
}
|
||||
|
||||
/// Événement pour sélectionner/désélectionner une proposition
|
||||
class SelectionnerPropositionAideEvent extends PropositionsAideEvent {
|
||||
final String propositionId;
|
||||
final bool selectionne;
|
||||
|
||||
const SelectionnerPropositionAideEvent({
|
||||
required this.propositionId,
|
||||
required this.selectionne,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [propositionId, selectionne];
|
||||
}
|
||||
|
||||
/// Événement pour sélectionner/désélectionner toutes les propositions
|
||||
class SelectionnerToutesPropositionsAideEvent extends PropositionsAideEvent {
|
||||
final bool selectionne;
|
||||
|
||||
const SelectionnerToutesPropositionsAideEvent({required this.selectionne});
|
||||
|
||||
@override
|
||||
List<Object> get props => [selectionne];
|
||||
}
|
||||
|
||||
/// Événement pour supprimer des propositions sélectionnées
|
||||
class SupprimerPropositionsSelectionnees extends PropositionsAideEvent {
|
||||
final List<String> propositionIds;
|
||||
|
||||
const SupprimerPropositionsSelectionnees({required this.propositionIds});
|
||||
|
||||
@override
|
||||
List<Object> get props => [propositionIds];
|
||||
}
|
||||
|
||||
/// Événement pour exporter des propositions
|
||||
class ExporterPropositionsAideEvent extends PropositionsAideEvent {
|
||||
final List<String> propositionIds;
|
||||
final FormatExport format;
|
||||
|
||||
const ExporterPropositionsAideEvent({
|
||||
required this.propositionIds,
|
||||
required this.format,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [propositionIds, format];
|
||||
}
|
||||
|
||||
/// Événement pour calculer la compatibilité avec une demande
|
||||
class CalculerCompatibiliteEvent extends PropositionsAideEvent {
|
||||
final String propositionId;
|
||||
final String demandeId;
|
||||
|
||||
const CalculerCompatibiliteEvent({
|
||||
required this.propositionId,
|
||||
required this.demandeId,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [propositionId, demandeId];
|
||||
}
|
||||
|
||||
/// Événement pour obtenir les statistiques d'une proposition
|
||||
class ObtenirStatistiquesPropositionEvent extends PropositionsAideEvent {
|
||||
final String propositionId;
|
||||
|
||||
const ObtenirStatistiquesPropositionEvent({required this.propositionId});
|
||||
|
||||
@override
|
||||
List<Object> get props => [propositionId];
|
||||
}
|
||||
|
||||
/// Énumération pour les critères de tri
|
||||
enum TriPropositions {
|
||||
dateCreation,
|
||||
dateModification,
|
||||
titre,
|
||||
statut,
|
||||
capacite,
|
||||
proposant,
|
||||
scoreCompatibilite,
|
||||
nombreMatches,
|
||||
}
|
||||
|
||||
/// Énumération pour les formats d'export
|
||||
enum FormatExport {
|
||||
pdf,
|
||||
excel,
|
||||
csv,
|
||||
json,
|
||||
}
|
||||
|
||||
/// Extension pour obtenir le libellé des critères de tri
|
||||
extension TriPropositionsExtension on TriPropositions {
|
||||
String get libelle {
|
||||
switch (this) {
|
||||
case TriPropositions.dateCreation:
|
||||
return 'Date de création';
|
||||
case TriPropositions.dateModification:
|
||||
return 'Date de modification';
|
||||
case TriPropositions.titre:
|
||||
return 'Titre';
|
||||
case TriPropositions.statut:
|
||||
return 'Statut';
|
||||
case TriPropositions.capacite:
|
||||
return 'Capacité';
|
||||
case TriPropositions.proposant:
|
||||
return 'Proposant';
|
||||
case TriPropositions.scoreCompatibilite:
|
||||
return 'Score de compatibilité';
|
||||
case TriPropositions.nombreMatches:
|
||||
return 'Nombre de matches';
|
||||
}
|
||||
}
|
||||
|
||||
String get icone {
|
||||
switch (this) {
|
||||
case TriPropositions.dateCreation:
|
||||
return 'calendar_today';
|
||||
case TriPropositions.dateModification:
|
||||
return 'update';
|
||||
case TriPropositions.titre:
|
||||
return 'title';
|
||||
case TriPropositions.statut:
|
||||
return 'flag';
|
||||
case TriPropositions.capacite:
|
||||
return 'trending_up';
|
||||
case TriPropositions.proposant:
|
||||
return 'person';
|
||||
case TriPropositions.scoreCompatibilite:
|
||||
return 'star';
|
||||
case TriPropositions.nombreMatches:
|
||||
return 'link';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension pour obtenir le libellé des formats d'export
|
||||
extension FormatExportExtension on FormatExport {
|
||||
String get libelle {
|
||||
switch (this) {
|
||||
case FormatExport.pdf:
|
||||
return 'PDF';
|
||||
case FormatExport.excel:
|
||||
return 'Excel';
|
||||
case FormatExport.csv:
|
||||
return 'CSV';
|
||||
case FormatExport.json:
|
||||
return 'JSON';
|
||||
}
|
||||
}
|
||||
|
||||
String get extension {
|
||||
switch (this) {
|
||||
case FormatExport.pdf:
|
||||
return '.pdf';
|
||||
case FormatExport.excel:
|
||||
return '.xlsx';
|
||||
case FormatExport.csv:
|
||||
return '.csv';
|
||||
case FormatExport.json:
|
||||
return '.json';
|
||||
}
|
||||
}
|
||||
|
||||
String get mimeType {
|
||||
switch (this) {
|
||||
case FormatExport.pdf:
|
||||
return 'application/pdf';
|
||||
case FormatExport.excel:
|
||||
return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
||||
case FormatExport.csv:
|
||||
return 'text/csv';
|
||||
case FormatExport.json:
|
||||
return 'application/json';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,445 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import '../../../domain/entities/proposition_aide.dart';
|
||||
import 'propositions_aide_event.dart';
|
||||
|
||||
/// États pour la gestion des propositions d'aide
|
||||
///
|
||||
/// Ces états représentent tous les états possibles
|
||||
/// de l'interface utilisateur pour les propositions d'aide.
|
||||
abstract class PropositionsAideState extends Equatable {
|
||||
const PropositionsAideState();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
/// État initial
|
||||
class PropositionsAideInitial extends PropositionsAideState {
|
||||
const PropositionsAideInitial();
|
||||
}
|
||||
|
||||
/// État de chargement
|
||||
class PropositionsAideLoading extends PropositionsAideState {
|
||||
final bool isRefreshing;
|
||||
final bool isLoadingMore;
|
||||
|
||||
const PropositionsAideLoading({
|
||||
this.isRefreshing = false,
|
||||
this.isLoadingMore = false,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [isRefreshing, isLoadingMore];
|
||||
}
|
||||
|
||||
/// État de succès avec données chargées
|
||||
class PropositionsAideLoaded extends PropositionsAideState {
|
||||
final List<PropositionAide> propositions;
|
||||
final List<PropositionAide> propositionsFiltrees;
|
||||
final bool hasReachedMax;
|
||||
final int currentPage;
|
||||
final int totalElements;
|
||||
final Map<String, bool> propositionsSelectionnees;
|
||||
final TriPropositions? criterieTri;
|
||||
final bool triCroissant;
|
||||
final FiltresPropositionsAide filtres;
|
||||
final bool isRefreshing;
|
||||
final bool isLoadingMore;
|
||||
final DateTime lastUpdated;
|
||||
|
||||
const PropositionsAideLoaded({
|
||||
required this.propositions,
|
||||
required this.propositionsFiltrees,
|
||||
this.hasReachedMax = false,
|
||||
this.currentPage = 0,
|
||||
this.totalElements = 0,
|
||||
this.propositionsSelectionnees = const {},
|
||||
this.criterieTri,
|
||||
this.triCroissant = true,
|
||||
this.filtres = const FiltresPropositionsAide(),
|
||||
this.isRefreshing = false,
|
||||
this.isLoadingMore = false,
|
||||
required this.lastUpdated,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
propositions,
|
||||
propositionsFiltrees,
|
||||
hasReachedMax,
|
||||
currentPage,
|
||||
totalElements,
|
||||
propositionsSelectionnees,
|
||||
criterieTri,
|
||||
triCroissant,
|
||||
filtres,
|
||||
isRefreshing,
|
||||
isLoadingMore,
|
||||
lastUpdated,
|
||||
];
|
||||
|
||||
/// Copie l'état avec de nouvelles valeurs
|
||||
PropositionsAideLoaded copyWith({
|
||||
List<PropositionAide>? propositions,
|
||||
List<PropositionAide>? propositionsFiltrees,
|
||||
bool? hasReachedMax,
|
||||
int? currentPage,
|
||||
int? totalElements,
|
||||
Map<String, bool>? propositionsSelectionnees,
|
||||
TriPropositions? criterieTri,
|
||||
bool? triCroissant,
|
||||
FiltresPropositionsAide? filtres,
|
||||
bool? isRefreshing,
|
||||
bool? isLoadingMore,
|
||||
DateTime? lastUpdated,
|
||||
}) {
|
||||
return PropositionsAideLoaded(
|
||||
propositions: propositions ?? this.propositions,
|
||||
propositionsFiltrees: propositionsFiltrees ?? this.propositionsFiltrees,
|
||||
hasReachedMax: hasReachedMax ?? this.hasReachedMax,
|
||||
currentPage: currentPage ?? this.currentPage,
|
||||
totalElements: totalElements ?? this.totalElements,
|
||||
propositionsSelectionnees: propositionsSelectionnees ?? this.propositionsSelectionnees,
|
||||
criterieTri: criterieTri ?? this.criterieTri,
|
||||
triCroissant: triCroissant ?? this.triCroissant,
|
||||
filtres: filtres ?? this.filtres,
|
||||
isRefreshing: isRefreshing ?? this.isRefreshing,
|
||||
isLoadingMore: isLoadingMore ?? this.isLoadingMore,
|
||||
lastUpdated: lastUpdated ?? this.lastUpdated,
|
||||
);
|
||||
}
|
||||
|
||||
/// Obtient le nombre de propositions sélectionnées
|
||||
int get nombrePropositionsSelectionnees {
|
||||
return propositionsSelectionnees.values.where((selected) => selected).length;
|
||||
}
|
||||
|
||||
/// Vérifie si toutes les propositions sont sélectionnées
|
||||
bool get toutesPropositionsSelectionnees {
|
||||
if (propositionsFiltrees.isEmpty) return false;
|
||||
return propositionsFiltrees.every((proposition) =>
|
||||
propositionsSelectionnees[proposition.id] == true
|
||||
);
|
||||
}
|
||||
|
||||
/// Obtient les IDs des propositions sélectionnées
|
||||
List<String> get propositionsSelectionneesIds {
|
||||
return propositionsSelectionnees.entries
|
||||
.where((entry) => entry.value)
|
||||
.map((entry) => entry.key)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// Obtient les propositions sélectionnées
|
||||
List<PropositionAide> get propositionsSelectionneesEntities {
|
||||
return propositions.where((proposition) =>
|
||||
propositionsSelectionnees[proposition.id] == true
|
||||
).toList();
|
||||
}
|
||||
|
||||
/// Vérifie si des données sont disponibles
|
||||
bool get hasData => propositions.isNotEmpty;
|
||||
|
||||
/// Vérifie si des filtres sont appliqués
|
||||
bool get hasFiltres => !filtres.isEmpty;
|
||||
|
||||
/// Obtient le texte de statut
|
||||
String get statusText {
|
||||
if (isRefreshing) return 'Actualisation...';
|
||||
if (isLoadingMore) return 'Chargement...';
|
||||
if (propositionsFiltrees.isEmpty && hasData) return 'Aucun résultat pour les filtres appliqués';
|
||||
if (propositionsFiltrees.isEmpty) return 'Aucune proposition d\'aide';
|
||||
return '${propositionsFiltrees.length} proposition${propositionsFiltrees.length > 1 ? 's' : ''}';
|
||||
}
|
||||
|
||||
/// Obtient le nombre de propositions disponibles
|
||||
int get nombrePropositionsDisponibles {
|
||||
return propositionsFiltrees.where((p) => p.estDisponible).length;
|
||||
}
|
||||
|
||||
/// Obtient la capacité totale disponible
|
||||
double get capaciteTotaleDisponible {
|
||||
return propositionsFiltrees
|
||||
.where((p) => p.estDisponible)
|
||||
.fold(0.0, (sum, p) => sum + (p.capaciteMaximale ?? 0.0));
|
||||
}
|
||||
}
|
||||
|
||||
/// État d'erreur
|
||||
class PropositionsAideError extends PropositionsAideState {
|
||||
final String message;
|
||||
final String? code;
|
||||
final bool isNetworkError;
|
||||
final bool canRetry;
|
||||
final List<PropositionAide>? cachedData;
|
||||
|
||||
const PropositionsAideError({
|
||||
required this.message,
|
||||
this.code,
|
||||
this.isNetworkError = false,
|
||||
this.canRetry = true,
|
||||
this.cachedData,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
message,
|
||||
code,
|
||||
isNetworkError,
|
||||
canRetry,
|
||||
cachedData,
|
||||
];
|
||||
|
||||
/// Vérifie si des données en cache sont disponibles
|
||||
bool get hasCachedData => cachedData != null && cachedData!.isNotEmpty;
|
||||
}
|
||||
|
||||
/// État de succès pour une opération spécifique
|
||||
class PropositionsAideOperationSuccess extends PropositionsAideState {
|
||||
final String message;
|
||||
final PropositionAide? proposition;
|
||||
final TypeOperationProposition operation;
|
||||
|
||||
const PropositionsAideOperationSuccess({
|
||||
required this.message,
|
||||
this.proposition,
|
||||
required this.operation,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [message, proposition, operation];
|
||||
}
|
||||
|
||||
/// État de compatibilité calculée
|
||||
class PropositionsAideCompatibilite extends PropositionsAideState {
|
||||
final String propositionId;
|
||||
final String demandeId;
|
||||
final double scoreCompatibilite;
|
||||
final Map<String, dynamic> detailsCompatibilite;
|
||||
|
||||
const PropositionsAideCompatibilite({
|
||||
required this.propositionId,
|
||||
required this.demandeId,
|
||||
required this.scoreCompatibilite,
|
||||
required this.detailsCompatibilite,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [propositionId, demandeId, scoreCompatibilite, detailsCompatibilite];
|
||||
}
|
||||
|
||||
/// État des statistiques d'une proposition
|
||||
class PropositionsAideStatistiques extends PropositionsAideState {
|
||||
final String propositionId;
|
||||
final Map<String, dynamic> statistiques;
|
||||
|
||||
const PropositionsAideStatistiques({
|
||||
required this.propositionId,
|
||||
required this.statistiques,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [propositionId, statistiques];
|
||||
}
|
||||
|
||||
/// État d'export
|
||||
class PropositionsAideExporting extends PropositionsAideState {
|
||||
final double progress;
|
||||
final String? currentStep;
|
||||
|
||||
const PropositionsAideExporting({
|
||||
required this.progress,
|
||||
this.currentStep,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [progress, currentStep];
|
||||
}
|
||||
|
||||
/// État d'export terminé
|
||||
class PropositionsAideExported extends PropositionsAideState {
|
||||
final String filePath;
|
||||
final FormatExport format;
|
||||
final int nombrePropositions;
|
||||
|
||||
const PropositionsAideExported({
|
||||
required this.filePath,
|
||||
required this.format,
|
||||
required this.nombrePropositions,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [filePath, format, nombrePropositions];
|
||||
}
|
||||
|
||||
/// Classe pour les filtres des propositions d'aide
|
||||
class FiltresPropositionsAide extends Equatable {
|
||||
final TypeAide? typeAide;
|
||||
final StatutProposition? statut;
|
||||
final bool? disponible;
|
||||
final String? motCle;
|
||||
final String? organisationId;
|
||||
final String? proposantId;
|
||||
final DateTime? dateDebutCreation;
|
||||
final DateTime? dateFinCreation;
|
||||
final double? capaciteMin;
|
||||
final double? capaciteMax;
|
||||
|
||||
const FiltresPropositionsAide({
|
||||
this.typeAide,
|
||||
this.statut,
|
||||
this.disponible,
|
||||
this.motCle,
|
||||
this.organisationId,
|
||||
this.proposantId,
|
||||
this.dateDebutCreation,
|
||||
this.dateFinCreation,
|
||||
this.capaciteMin,
|
||||
this.capaciteMax,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
typeAide,
|
||||
statut,
|
||||
disponible,
|
||||
motCle,
|
||||
organisationId,
|
||||
proposantId,
|
||||
dateDebutCreation,
|
||||
dateFinCreation,
|
||||
capaciteMin,
|
||||
capaciteMax,
|
||||
];
|
||||
|
||||
/// Copie les filtres avec de nouvelles valeurs
|
||||
FiltresPropositionsAide copyWith({
|
||||
TypeAide? typeAide,
|
||||
StatutProposition? statut,
|
||||
bool? disponible,
|
||||
String? motCle,
|
||||
String? organisationId,
|
||||
String? proposantId,
|
||||
DateTime? dateDebutCreation,
|
||||
DateTime? dateFinCreation,
|
||||
double? capaciteMin,
|
||||
double? capaciteMax,
|
||||
}) {
|
||||
return FiltresPropositionsAide(
|
||||
typeAide: typeAide ?? this.typeAide,
|
||||
statut: statut ?? this.statut,
|
||||
disponible: disponible ?? this.disponible,
|
||||
motCle: motCle ?? this.motCle,
|
||||
organisationId: organisationId ?? this.organisationId,
|
||||
proposantId: proposantId ?? this.proposantId,
|
||||
dateDebutCreation: dateDebutCreation ?? this.dateDebutCreation,
|
||||
dateFinCreation: dateFinCreation ?? this.dateFinCreation,
|
||||
capaciteMin: capaciteMin ?? this.capaciteMin,
|
||||
capaciteMax: capaciteMax ?? this.capaciteMax,
|
||||
);
|
||||
}
|
||||
|
||||
/// Réinitialise tous les filtres
|
||||
FiltresPropositionsAide clear() {
|
||||
return const FiltresPropositionsAide();
|
||||
}
|
||||
|
||||
/// Vérifie si les filtres sont vides
|
||||
bool get isEmpty {
|
||||
return typeAide == null &&
|
||||
statut == null &&
|
||||
disponible == null &&
|
||||
(motCle == null || motCle!.isEmpty) &&
|
||||
organisationId == null &&
|
||||
proposantId == null &&
|
||||
dateDebutCreation == null &&
|
||||
dateFinCreation == null &&
|
||||
capaciteMin == null &&
|
||||
capaciteMax == null;
|
||||
}
|
||||
|
||||
/// Obtient le nombre de filtres actifs
|
||||
int get nombreFiltresActifs {
|
||||
int count = 0;
|
||||
if (typeAide != null) count++;
|
||||
if (statut != null) count++;
|
||||
if (disponible != null) count++;
|
||||
if (motCle != null && motCle!.isNotEmpty) count++;
|
||||
if (organisationId != null) count++;
|
||||
if (proposantId != null) count++;
|
||||
if (dateDebutCreation != null) count++;
|
||||
if (dateFinCreation != null) count++;
|
||||
if (capaciteMin != null) count++;
|
||||
if (capaciteMax != null) count++;
|
||||
return count;
|
||||
}
|
||||
|
||||
/// Obtient une description textuelle des filtres
|
||||
String get description {
|
||||
final parts = <String>[];
|
||||
|
||||
if (typeAide != null) parts.add('Type: ${typeAide!.libelle}');
|
||||
if (statut != null) parts.add('Statut: ${statut!.libelle}');
|
||||
if (disponible == true) parts.add('Disponible uniquement');
|
||||
if (disponible == false) parts.add('Non disponible uniquement');
|
||||
if (motCle != null && motCle!.isNotEmpty) parts.add('Recherche: "$motCle"');
|
||||
if (capaciteMin != null || capaciteMax != null) {
|
||||
if (capaciteMin != null && capaciteMax != null) {
|
||||
parts.add('Capacité: ${capaciteMin!.toInt()} - ${capaciteMax!.toInt()}');
|
||||
} else if (capaciteMin != null) {
|
||||
parts.add('Capacité min: ${capaciteMin!.toInt()}');
|
||||
} else {
|
||||
parts.add('Capacité max: ${capaciteMax!.toInt()}');
|
||||
}
|
||||
}
|
||||
|
||||
return parts.join(', ');
|
||||
}
|
||||
}
|
||||
|
||||
/// Énumération pour les types d'opération
|
||||
enum TypeOperationProposition {
|
||||
creation,
|
||||
modification,
|
||||
activation,
|
||||
desactivation,
|
||||
suppression,
|
||||
export,
|
||||
}
|
||||
|
||||
/// Extension pour obtenir le libellé des opérations
|
||||
extension TypeOperationPropositionExtension on TypeOperationProposition {
|
||||
String get libelle {
|
||||
switch (this) {
|
||||
case TypeOperationProposition.creation:
|
||||
return 'Création';
|
||||
case TypeOperationProposition.modification:
|
||||
return 'Modification';
|
||||
case TypeOperationProposition.activation:
|
||||
return 'Activation';
|
||||
case TypeOperationProposition.desactivation:
|
||||
return 'Désactivation';
|
||||
case TypeOperationProposition.suppression:
|
||||
return 'Suppression';
|
||||
case TypeOperationProposition.export:
|
||||
return 'Export';
|
||||
}
|
||||
}
|
||||
|
||||
String get messageSucces {
|
||||
switch (this) {
|
||||
case TypeOperationProposition.creation:
|
||||
return 'Proposition d\'aide créée avec succès';
|
||||
case TypeOperationProposition.modification:
|
||||
return 'Proposition d\'aide modifiée avec succès';
|
||||
case TypeOperationProposition.activation:
|
||||
return 'Proposition d\'aide activée avec succès';
|
||||
case TypeOperationProposition.desactivation:
|
||||
return 'Proposition d\'aide désactivée avec succès';
|
||||
case TypeOperationProposition.suppression:
|
||||
return 'Proposition d\'aide supprimée avec succès';
|
||||
case TypeOperationProposition.export:
|
||||
return 'Export réalisé avec succès';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,770 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../../core/widgets/unified_page_layout.dart';
|
||||
import '../../../../core/widgets/unified_card.dart';
|
||||
import '../../../../core/theme/app_colors.dart';
|
||||
import '../../../../core/theme/app_text_styles.dart';
|
||||
import '../../../../core/utils/date_formatter.dart';
|
||||
import '../../../../core/utils/currency_formatter.dart';
|
||||
import '../../domain/entities/demande_aide.dart';
|
||||
import '../bloc/demandes_aide/demandes_aide_bloc.dart';
|
||||
import '../bloc/demandes_aide/demandes_aide_event.dart';
|
||||
import '../bloc/demandes_aide/demandes_aide_state.dart';
|
||||
import '../widgets/demande_aide_status_timeline.dart';
|
||||
import '../widgets/demande_aide_evaluation_section.dart';
|
||||
import '../widgets/demande_aide_documents_section.dart';
|
||||
|
||||
/// Page de détails d'une demande d'aide
|
||||
///
|
||||
/// Cette page affiche toutes les informations détaillées d'une demande d'aide
|
||||
/// avec des sections organisées et des actions contextuelles.
|
||||
class DemandeAideDetailsPage extends StatefulWidget {
|
||||
final String demandeId;
|
||||
|
||||
const DemandeAideDetailsPage({
|
||||
super.key,
|
||||
required this.demandeId,
|
||||
});
|
||||
|
||||
@override
|
||||
State<DemandeAideDetailsPage> createState() => _DemandeAideDetailsPageState();
|
||||
}
|
||||
|
||||
class _DemandeAideDetailsPageState extends State<DemandeAideDetailsPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Charger les détails de la demande
|
||||
context.read<DemandesAideBloc>().add(
|
||||
ObtenirDemandeAideEvent(demandeId: widget.demandeId),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocConsumer<DemandesAideBloc, DemandesAideState>(
|
||||
listener: (context, state) {
|
||||
if (state is DemandesAideError) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.message),
|
||||
backgroundColor: AppColors.error,
|
||||
),
|
||||
);
|
||||
} else if (state is DemandesAideOperationSuccess) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.message),
|
||||
backgroundColor: AppColors.success,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
if (state is DemandesAideLoading) {
|
||||
return const UnifiedPageLayout(
|
||||
title: 'Détails de la demande',
|
||||
body: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
if (state is DemandesAideError && !state.hasCachedData) {
|
||||
return UnifiedPageLayout(
|
||||
title: 'Détails de la demande',
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error,
|
||||
size: 64,
|
||||
color: AppColors.error,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
state.message,
|
||||
style: AppTextStyles.bodyLarge,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (state.canRetry)
|
||||
ElevatedButton(
|
||||
onPressed: () => _rechargerDemande(),
|
||||
child: const Text('Réessayer'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Trouver la demande dans l'état
|
||||
DemandeAide? demande;
|
||||
if (state is DemandesAideLoaded) {
|
||||
demande = state.demandes.firstWhere(
|
||||
(d) => d.id == widget.demandeId,
|
||||
orElse: () => throw StateError('Demande non trouvée'),
|
||||
);
|
||||
}
|
||||
|
||||
if (demande == null) {
|
||||
return const UnifiedPageLayout(
|
||||
title: 'Détails de la demande',
|
||||
body: Center(
|
||||
child: Text('Demande d\'aide non trouvée'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return UnifiedPageLayout(
|
||||
title: 'Détails de la demande',
|
||||
actions: _buildActions(demande),
|
||||
body: RefreshIndicator(
|
||||
onRefresh: () async => _rechargerDemande(),
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildHeaderSection(demande),
|
||||
const SizedBox(height: 16),
|
||||
_buildInfoGeneralesSection(demande),
|
||||
const SizedBox(height: 16),
|
||||
_buildDescriptionSection(demande),
|
||||
const SizedBox(height: 16),
|
||||
_buildBeneficiaireSection(demande),
|
||||
const SizedBox(height: 16),
|
||||
_buildContactUrgenceSection(demande),
|
||||
const SizedBox(height: 16),
|
||||
_buildLocalisationSection(demande),
|
||||
const SizedBox(height: 16),
|
||||
DemandeAideDocumentsSection(demande: demande),
|
||||
const SizedBox(height: 16),
|
||||
DemandeAideStatusTimeline(demande: demande),
|
||||
const SizedBox(height: 16),
|
||||
if (demande.evaluations.isNotEmpty)
|
||||
DemandeAideEvaluationSection(demande: demande),
|
||||
const SizedBox(height: 80), // Espace pour le FAB
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
floatingActionButton: _buildFloatingActionButton(demande),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildActions(DemandeAide demande) {
|
||||
return [
|
||||
PopupMenuButton<String>(
|
||||
onSelected: (value) => _onMenuSelected(value, demande),
|
||||
itemBuilder: (context) => [
|
||||
const PopupMenuItem(
|
||||
value: 'edit',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.edit),
|
||||
title: Text('Modifier'),
|
||||
dense: true,
|
||||
),
|
||||
),
|
||||
if (demande.statut == StatutAide.brouillon)
|
||||
const PopupMenuItem(
|
||||
value: 'submit',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.send),
|
||||
title: Text('Soumettre'),
|
||||
dense: true,
|
||||
),
|
||||
),
|
||||
if (demande.statut == StatutAide.soumise)
|
||||
const PopupMenuItem(
|
||||
value: 'evaluate',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.rate_review),
|
||||
title: Text('Évaluer'),
|
||||
dense: true,
|
||||
),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
value: 'share',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.share),
|
||||
title: Text('Partager'),
|
||||
dense: true,
|
||||
),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
value: 'export',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.file_download),
|
||||
title: Text('Exporter'),
|
||||
dense: true,
|
||||
),
|
||||
),
|
||||
if (demande.statut == StatutAide.brouillon)
|
||||
const PopupMenuItem(
|
||||
value: 'delete',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.delete, color: AppColors.error),
|
||||
title: Text('Supprimer', style: TextStyle(color: AppColors.error)),
|
||||
dense: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
Widget _buildFloatingActionButton(DemandeAide demande) {
|
||||
if (demande.statut == StatutAide.brouillon) {
|
||||
return FloatingActionButton.extended(
|
||||
onPressed: () => _soumettredemande(demande),
|
||||
icon: const Icon(Icons.send),
|
||||
label: const Text('Soumettre'),
|
||||
backgroundColor: AppColors.primary,
|
||||
);
|
||||
}
|
||||
|
||||
if (demande.statut == StatutAide.soumise) {
|
||||
return FloatingActionButton.extended(
|
||||
onPressed: () => _evaluerDemande(demande),
|
||||
icon: const Icon(Icons.rate_review),
|
||||
label: const Text('Évaluer'),
|
||||
backgroundColor: AppColors.warning,
|
||||
);
|
||||
}
|
||||
|
||||
return FloatingActionButton(
|
||||
onPressed: () => _modifierDemande(demande),
|
||||
child: const Icon(Icons.edit),
|
||||
backgroundColor: AppColors.primary,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeaderSection(DemandeAide demande) {
|
||||
return UnifiedCard(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
demande.titre,
|
||||
style: AppTextStyles.titleLarge.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildStatutChip(demande.statut),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
demande.numeroReference,
|
||||
style: AppTextStyles.bodyMedium.copyWith(
|
||||
fontFamily: 'monospace',
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
if (demande.estUrgente)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.error.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.priority_high,
|
||||
size: 16,
|
||||
color: AppColors.error,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'URGENT',
|
||||
style: AppTextStyles.labelSmall.copyWith(
|
||||
color: AppColors.error,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildProgressBar(demande),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoGeneralesSection(DemandeAide demande) {
|
||||
return UnifiedCard(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Informations générales',
|
||||
style: AppTextStyles.titleMedium.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildInfoRow('Type d\'aide', demande.typeAide.libelle, Icons.category),
|
||||
_buildInfoRow('Priorité', demande.priorite.libelle, Icons.priority_high),
|
||||
_buildInfoRow('Demandeur', demande.nomDemandeur, Icons.person),
|
||||
if (demande.montantDemande != null)
|
||||
_buildInfoRow(
|
||||
'Montant demandé',
|
||||
CurrencyFormatter.formatCFA(demande.montantDemande!),
|
||||
Icons.attach_money,
|
||||
),
|
||||
if (demande.montantApprouve != null)
|
||||
_buildInfoRow(
|
||||
'Montant approuvé',
|
||||
CurrencyFormatter.formatCFA(demande.montantApprouve!),
|
||||
Icons.check_circle,
|
||||
),
|
||||
_buildInfoRow(
|
||||
'Date de création',
|
||||
DateFormatter.formatComplete(demande.dateCreation),
|
||||
Icons.calendar_today,
|
||||
),
|
||||
if (demande.dateModification != demande.dateCreation)
|
||||
_buildInfoRow(
|
||||
'Dernière modification',
|
||||
DateFormatter.formatComplete(demande.dateModification),
|
||||
Icons.update,
|
||||
),
|
||||
if (demande.dateEcheance != null)
|
||||
_buildInfoRow(
|
||||
'Date d\'échéance',
|
||||
DateFormatter.formatComplete(demande.dateEcheance!),
|
||||
Icons.schedule,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDescriptionSection(DemandeAide demande) {
|
||||
return UnifiedCard(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Description',
|
||||
style: AppTextStyles.titleMedium.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
demande.description,
|
||||
style: AppTextStyles.bodyMedium,
|
||||
),
|
||||
if (demande.justification != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Justification',
|
||||
style: AppTextStyles.titleSmall.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
demande.justification!,
|
||||
style: AppTextStyles.bodyMedium,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBeneficiaireSection(DemandeAide demande) {
|
||||
if (demande.beneficiaires.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
return UnifiedCard(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Bénéficiaires',
|
||||
style: AppTextStyles.titleMedium.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
...demande.beneficiaires.map((beneficiaire) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.person,
|
||||
size: 20,
|
||||
color: AppColors.primary,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${beneficiaire.prenom} ${beneficiaire.nom}',
|
||||
style: AppTextStyles.bodyMedium.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
if (beneficiaire.age != null)
|
||||
Text(
|
||||
'${beneficiaire.age} ans',
|
||||
style: AppTextStyles.bodySmall.copyWith(
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContactUrgenceSection(DemandeAide demande) {
|
||||
if (demande.contactUrgence == null) return const SizedBox.shrink();
|
||||
|
||||
final contact = demande.contactUrgence!;
|
||||
return UnifiedCard(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Contact d\'urgence',
|
||||
style: AppTextStyles.titleMedium.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildInfoRow('Nom', '${contact.prenom} ${contact.nom}', Icons.person),
|
||||
_buildInfoRow('Téléphone', contact.telephone, Icons.phone),
|
||||
if (contact.email != null)
|
||||
_buildInfoRow('Email', contact.email!, Icons.email),
|
||||
_buildInfoRow('Relation', contact.relation, Icons.family_restroom),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLocalisationSection(DemandeAide demande) {
|
||||
if (demande.localisation == null) return const SizedBox.shrink();
|
||||
|
||||
final localisation = demande.localisation!;
|
||||
return UnifiedCard(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'Localisation',
|
||||
style: AppTextStyles.titleMedium.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
onPressed: () => _ouvrirCarte(localisation),
|
||||
icon: const Icon(Icons.map),
|
||||
tooltip: 'Voir sur la carte',
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildInfoRow('Adresse', localisation.adresse, Icons.location_on),
|
||||
if (localisation.ville != null)
|
||||
_buildInfoRow('Ville', localisation.ville!, Icons.location_city),
|
||||
if (localisation.codePostal != null)
|
||||
_buildInfoRow('Code postal', localisation.codePostal!, Icons.markunread_mailbox),
|
||||
if (localisation.pays != null)
|
||||
_buildInfoRow('Pays', localisation.pays!, Icons.flag),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoRow(String label, String value, IconData icon) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 20,
|
||||
color: AppColors.primary,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: AppTextStyles.bodySmall.copyWith(
|
||||
color: AppColors.textSecondary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
value,
|
||||
style: AppTextStyles.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatutChip(StatutAide statut) {
|
||||
final color = _getStatutColor(statut);
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Text(
|
||||
statut.libelle,
|
||||
style: AppTextStyles.labelMedium.copyWith(
|
||||
color: color,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProgressBar(DemandeAide demande) {
|
||||
final progress = demande.pourcentageAvancement;
|
||||
final color = _getProgressColor(progress);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Avancement',
|
||||
style: AppTextStyles.bodySmall.copyWith(
|
||||
color: AppColors.textSecondary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${progress.toInt()}%',
|
||||
style: AppTextStyles.bodySmall.copyWith(
|
||||
color: color,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
LinearProgressIndicator(
|
||||
value: progress / 100,
|
||||
backgroundColor: AppColors.outline,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(color),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Color _getStatutColor(StatutAide statut) {
|
||||
switch (statut) {
|
||||
case StatutAide.brouillon:
|
||||
return AppColors.textSecondary;
|
||||
case StatutAide.soumise:
|
||||
return AppColors.warning;
|
||||
case StatutAide.enEvaluation:
|
||||
return AppColors.info;
|
||||
case StatutAide.approuvee:
|
||||
return AppColors.success;
|
||||
case StatutAide.rejetee:
|
||||
return AppColors.error;
|
||||
case StatutAide.enCours:
|
||||
return AppColors.primary;
|
||||
case StatutAide.terminee:
|
||||
return AppColors.success;
|
||||
case StatutAide.versee:
|
||||
return AppColors.success;
|
||||
case StatutAide.livree:
|
||||
return AppColors.success;
|
||||
case StatutAide.annulee:
|
||||
return AppColors.error;
|
||||
}
|
||||
}
|
||||
|
||||
Color _getProgressColor(double progress) {
|
||||
if (progress < 25) return AppColors.error;
|
||||
if (progress < 50) return AppColors.warning;
|
||||
if (progress < 75) return AppColors.info;
|
||||
return AppColors.success;
|
||||
}
|
||||
|
||||
void _rechargerDemande() {
|
||||
context.read<DemandesAideBloc>().add(
|
||||
ObtenirDemandeAideEvent(demandeId: widget.demandeId),
|
||||
);
|
||||
}
|
||||
|
||||
void _onMenuSelected(String value, DemandeAide demande) {
|
||||
switch (value) {
|
||||
case 'edit':
|
||||
_modifierDemande(demande);
|
||||
break;
|
||||
case 'submit':
|
||||
_soumettredemande(demande);
|
||||
break;
|
||||
case 'evaluate':
|
||||
_evaluerDemande(demande);
|
||||
break;
|
||||
case 'share':
|
||||
_partagerDemande(demande);
|
||||
break;
|
||||
case 'export':
|
||||
_exporterDemande(demande);
|
||||
break;
|
||||
case 'delete':
|
||||
_supprimerDemande(demande);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void _modifierDemande(DemandeAide demande) {
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
'/solidarite/demandes/modifier',
|
||||
arguments: demande,
|
||||
);
|
||||
}
|
||||
|
||||
void _soumettredemande(DemandeAide demande) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Soumettre la demande'),
|
||||
content: const Text(
|
||||
'Êtes-vous sûr de vouloir soumettre cette demande d\'aide ? '
|
||||
'Une fois soumise, elle ne pourra plus être modifiée.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
context.read<DemandesAideBloc>().add(
|
||||
SoumettreDemandeAideEvent(demandeId: demande.id),
|
||||
);
|
||||
},
|
||||
child: const Text('Soumettre'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _evaluerDemande(DemandeAide demande) {
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
'/solidarite/demandes/evaluer',
|
||||
arguments: demande,
|
||||
);
|
||||
}
|
||||
|
||||
void _partagerDemande(DemandeAide demande) {
|
||||
// Implémenter le partage
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Fonctionnalité de partage à implémenter')),
|
||||
);
|
||||
}
|
||||
|
||||
void _exporterDemande(DemandeAide demande) {
|
||||
// Implémenter l'export
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Fonctionnalité d\'export à implémenter')),
|
||||
);
|
||||
}
|
||||
|
||||
void _supprimerDemande(DemandeAide demande) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Supprimer la demande'),
|
||||
content: const Text(
|
||||
'Êtes-vous sûr de vouloir supprimer cette demande d\'aide ? '
|
||||
'Cette action est irréversible.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(context); // Retour à la liste
|
||||
context.read<DemandesAideBloc>().add(
|
||||
SupprimerDemandesSelectionnees(demandeIds: [demande.id]),
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(backgroundColor: AppColors.error),
|
||||
child: const Text('Supprimer'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _ouvrirCarte(Localisation localisation) {
|
||||
// Implémenter l'ouverture de la carte
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Ouverture de la carte à implémenter')),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,601 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../../core/widgets/unified_page_layout.dart';
|
||||
import '../../../../core/widgets/unified_card.dart';
|
||||
import '../../../../core/theme/app_colors.dart';
|
||||
import '../../../../core/theme/app_text_styles.dart';
|
||||
import '../../../../core/utils/validators.dart';
|
||||
import '../../domain/entities/demande_aide.dart';
|
||||
import '../bloc/demandes_aide/demandes_aide_bloc.dart';
|
||||
import '../bloc/demandes_aide/demandes_aide_event.dart';
|
||||
import '../bloc/demandes_aide/demandes_aide_state.dart';
|
||||
import '../widgets/demande_aide_form_sections.dart';
|
||||
|
||||
/// Page de formulaire pour créer ou modifier une demande d'aide
|
||||
///
|
||||
/// Cette page utilise un formulaire multi-sections avec validation
|
||||
/// pour créer ou modifier une demande d'aide.
|
||||
class DemandeAideFormPage extends StatefulWidget {
|
||||
final DemandeAide? demandeExistante;
|
||||
final bool isModification;
|
||||
|
||||
const DemandeAideFormPage({
|
||||
super.key,
|
||||
this.demandeExistante,
|
||||
this.isModification = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<DemandeAideFormPage> createState() => _DemandeAideFormPageState();
|
||||
}
|
||||
|
||||
class _DemandeAideFormPageState extends State<DemandeAideFormPage> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _pageController = PageController();
|
||||
|
||||
// Controllers pour les champs de texte
|
||||
final _titreController = TextEditingController();
|
||||
final _descriptionController = TextEditingController();
|
||||
final _justificationController = TextEditingController();
|
||||
final _montantController = TextEditingController();
|
||||
|
||||
// Variables d'état du formulaire
|
||||
TypeAide? _typeAide;
|
||||
PrioriteAide _priorite = PrioriteAide.normale;
|
||||
bool _estUrgente = false;
|
||||
DateTime? _dateEcheance;
|
||||
List<BeneficiaireAide> _beneficiaires = [];
|
||||
ContactUrgence? _contactUrgence;
|
||||
Localisation? _localisation;
|
||||
List<PieceJustificative> _piecesJustificatives = [];
|
||||
|
||||
int _currentStep = 0;
|
||||
final int _totalSteps = 5;
|
||||
bool _isLoading = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initializeForm();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_titreController.dispose();
|
||||
_descriptionController.dispose();
|
||||
_justificationController.dispose();
|
||||
_montantController.dispose();
|
||||
_pageController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _initializeForm() {
|
||||
if (widget.demandeExistante != null) {
|
||||
final demande = widget.demandeExistante!;
|
||||
_titreController.text = demande.titre;
|
||||
_descriptionController.text = demande.description;
|
||||
_justificationController.text = demande.justification ?? '';
|
||||
_montantController.text = demande.montantDemande?.toString() ?? '';
|
||||
_typeAide = demande.typeAide;
|
||||
_priorite = demande.priorite;
|
||||
_estUrgente = demande.estUrgente;
|
||||
_dateEcheance = demande.dateEcheance;
|
||||
_beneficiaires = List.from(demande.beneficiaires);
|
||||
_contactUrgence = demande.contactUrgence;
|
||||
_localisation = demande.localisation;
|
||||
_piecesJustificatives = List.from(demande.piecesJustificatives);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocConsumer<DemandesAideBloc, DemandesAideState>(
|
||||
listener: (context, state) {
|
||||
if (state is DemandesAideLoading) {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
if (state is DemandesAideError) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.message),
|
||||
backgroundColor: AppColors.error,
|
||||
),
|
||||
);
|
||||
} else if (state is DemandesAideOperationSuccess) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.message),
|
||||
backgroundColor: AppColors.success,
|
||||
),
|
||||
);
|
||||
Navigator.pop(context, true);
|
||||
} else if (state is DemandesAideValidation) {
|
||||
if (!state.isValid) {
|
||||
_showValidationErrors(state.erreurs);
|
||||
}
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
return UnifiedPageLayout(
|
||||
title: widget.isModification ? 'Modifier la demande' : 'Nouvelle demande',
|
||||
actions: [
|
||||
if (_currentStep > 0)
|
||||
IconButton(
|
||||
onPressed: _previousStep,
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
tooltip: 'Étape précédente',
|
||||
),
|
||||
IconButton(
|
||||
onPressed: _saveDraft,
|
||||
icon: const Icon(Icons.save),
|
||||
tooltip: 'Sauvegarder le brouillon',
|
||||
),
|
||||
],
|
||||
body: Column(
|
||||
children: [
|
||||
_buildProgressIndicator(),
|
||||
Expanded(
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: PageView(
|
||||
controller: _pageController,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: [
|
||||
_buildStep1InfoGenerales(),
|
||||
_buildStep2Beneficiaires(),
|
||||
_buildStep3Contact(),
|
||||
_buildStep4Localisation(),
|
||||
_buildStep5Documents(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildBottomActions(),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProgressIndicator() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: List.generate(_totalSteps, (index) {
|
||||
final isActive = index == _currentStep;
|
||||
final isCompleted = index < _currentStep;
|
||||
|
||||
return Expanded(
|
||||
child: Container(
|
||||
height: 4,
|
||||
margin: EdgeInsets.only(right: index < _totalSteps - 1 ? 8 : 0),
|
||||
decoration: BoxDecoration(
|
||||
color: isCompleted || isActive
|
||||
? AppColors.primary
|
||||
: AppColors.outline,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Étape ${_currentStep + 1} sur $_totalSteps: ${_getStepTitle(_currentStep)}',
|
||||
style: AppTextStyles.bodyMedium.copyWith(
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStep1InfoGenerales() {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
UnifiedCard(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Informations générales',
|
||||
style: AppTextStyles.titleMedium.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _titreController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Titre de la demande *',
|
||||
hintText: 'Ex: Aide pour frais médicaux',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
validator: Validators.required,
|
||||
maxLength: 100,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
DropdownButtonFormField<TypeAide>(
|
||||
value: _typeAide,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Type d\'aide *',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
items: TypeAide.values.map((type) => DropdownMenuItem(
|
||||
value: type,
|
||||
child: Text(type.libelle),
|
||||
)).toList(),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_typeAide = value;
|
||||
});
|
||||
},
|
||||
validator: (value) => value == null ? 'Veuillez sélectionner un type d\'aide' : null,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _descriptionController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Description détaillée *',
|
||||
hintText: 'Décrivez votre situation et vos besoins...',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
maxLines: 4,
|
||||
validator: Validators.required,
|
||||
maxLength: 1000,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _justificationController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Justification',
|
||||
hintText: 'Pourquoi cette aide est-elle nécessaire ?',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
maxLines: 3,
|
||||
maxLength: 500,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
UnifiedCard(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Détails de la demande',
|
||||
style: AppTextStyles.titleMedium.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _montantController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Montant demandé (FCFA)',
|
||||
hintText: '0',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.attach_money),
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
validator: (value) {
|
||||
if (value != null && value.isNotEmpty) {
|
||||
final montant = double.tryParse(value);
|
||||
if (montant == null || montant <= 0) {
|
||||
return 'Veuillez saisir un montant valide';
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
DropdownButtonFormField<PrioriteAide>(
|
||||
value: _priorite,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Priorité',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
items: PrioriteAide.values.map((priorite) => DropdownMenuItem(
|
||||
value: priorite,
|
||||
child: Text(priorite.libelle),
|
||||
)).toList(),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_priorite = value ?? PrioriteAide.normale;
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SwitchListTile(
|
||||
title: const Text('Demande urgente'),
|
||||
subtitle: const Text('Cette demande nécessite un traitement prioritaire'),
|
||||
value: _estUrgente,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_estUrgente = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ListTile(
|
||||
title: const Text('Date d\'échéance'),
|
||||
subtitle: Text(_dateEcheance != null
|
||||
? '${_dateEcheance!.day}/${_dateEcheance!.month}/${_dateEcheance!.year}'
|
||||
: 'Aucune date limite'),
|
||||
trailing: const Icon(Icons.calendar_today),
|
||||
onTap: _selectDateEcheance,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStep2Beneficiaires() {
|
||||
return DemandeAideFormBeneficiairesSection(
|
||||
beneficiaires: _beneficiaires,
|
||||
onBeneficiairesChanged: (beneficiaires) {
|
||||
setState(() {
|
||||
_beneficiaires = beneficiaires;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStep3Contact() {
|
||||
return DemandeAideFormContactSection(
|
||||
contactUrgence: _contactUrgence,
|
||||
onContactChanged: (contact) {
|
||||
setState(() {
|
||||
_contactUrgence = contact;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStep4Localisation() {
|
||||
return DemandeAideFormLocalisationSection(
|
||||
localisation: _localisation,
|
||||
onLocalisationChanged: (localisation) {
|
||||
setState(() {
|
||||
_localisation = localisation;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStep5Documents() {
|
||||
return DemandeAideFormDocumentsSection(
|
||||
piecesJustificatives: _piecesJustificatives,
|
||||
onDocumentsChanged: (documents) {
|
||||
setState(() {
|
||||
_piecesJustificatives = documents;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottomActions() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, -2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
if (_currentStep > 0)
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: _isLoading ? null : _previousStep,
|
||||
child: const Text('Précédent'),
|
||||
),
|
||||
),
|
||||
if (_currentStep > 0) const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: _isLoading ? null : _nextStepOrSubmit,
|
||||
child: _isLoading
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: Text(_currentStep < _totalSteps - 1 ? 'Suivant' : 'Créer la demande'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _getStepTitle(int step) {
|
||||
switch (step) {
|
||||
case 0:
|
||||
return 'Informations générales';
|
||||
case 1:
|
||||
return 'Bénéficiaires';
|
||||
case 2:
|
||||
return 'Contact d\'urgence';
|
||||
case 3:
|
||||
return 'Localisation';
|
||||
case 4:
|
||||
return 'Documents';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
void _previousStep() {
|
||||
if (_currentStep > 0) {
|
||||
setState(() {
|
||||
_currentStep--;
|
||||
});
|
||||
_pageController.previousPage(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _nextStepOrSubmit() {
|
||||
if (_validateCurrentStep()) {
|
||||
if (_currentStep < _totalSteps - 1) {
|
||||
setState(() {
|
||||
_currentStep++;
|
||||
});
|
||||
_pageController.nextPage(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
} else {
|
||||
_submitForm();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool _validateCurrentStep() {
|
||||
switch (_currentStep) {
|
||||
case 0:
|
||||
return _formKey.currentState?.validate() ?? false;
|
||||
case 1:
|
||||
// Validation des bénéficiaires (optionnel)
|
||||
return true;
|
||||
case 2:
|
||||
// Validation du contact d'urgence (optionnel)
|
||||
return true;
|
||||
case 3:
|
||||
// Validation de la localisation (optionnel)
|
||||
return true;
|
||||
case 4:
|
||||
// Validation des documents (optionnel)
|
||||
return true;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
void _submitForm() {
|
||||
if (!_formKey.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final demande = DemandeAide(
|
||||
id: widget.demandeExistante?.id ?? '',
|
||||
numeroReference: widget.demandeExistante?.numeroReference ?? '',
|
||||
titre: _titreController.text,
|
||||
description: _descriptionController.text,
|
||||
justification: _justificationController.text.isEmpty ? null : _justificationController.text,
|
||||
typeAide: _typeAide!,
|
||||
statut: widget.demandeExistante?.statut ?? StatutAide.brouillon,
|
||||
priorite: _priorite,
|
||||
estUrgente: _estUrgente,
|
||||
montantDemande: _montantController.text.isEmpty ? null : double.tryParse(_montantController.text),
|
||||
montantApprouve: widget.demandeExistante?.montantApprouve,
|
||||
dateCreation: widget.demandeExistante?.dateCreation ?? DateTime.now(),
|
||||
dateModification: DateTime.now(),
|
||||
dateEcheance: _dateEcheance,
|
||||
organisationId: widget.demandeExistante?.organisationId ?? '',
|
||||
demandeurId: widget.demandeExistante?.demandeurId ?? '',
|
||||
nomDemandeur: widget.demandeExistante?.nomDemandeur ?? '',
|
||||
emailDemandeur: widget.demandeExistante?.emailDemandeur ?? '',
|
||||
telephoneDemandeur: widget.demandeExistante?.telephoneDemandeur ?? '',
|
||||
beneficiaires: _beneficiaires,
|
||||
contactUrgence: _contactUrgence,
|
||||
localisation: _localisation,
|
||||
piecesJustificatives: _piecesJustificatives,
|
||||
evaluations: widget.demandeExistante?.evaluations ?? [],
|
||||
commentairesInternes: widget.demandeExistante?.commentairesInternes ?? [],
|
||||
historiqueStatuts: widget.demandeExistante?.historiqueStatuts ?? [],
|
||||
tags: widget.demandeExistante?.tags ?? [],
|
||||
metadonnees: widget.demandeExistante?.metadonnees ?? {},
|
||||
);
|
||||
|
||||
if (widget.isModification) {
|
||||
context.read<DemandesAideBloc>().add(
|
||||
MettreAJourDemandeAideEvent(demande: demande),
|
||||
);
|
||||
} else {
|
||||
context.read<DemandesAideBloc>().add(
|
||||
CreerDemandeAideEvent(demande: demande),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _saveDraft() {
|
||||
// Sauvegarder le brouillon
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Brouillon sauvegardé'),
|
||||
backgroundColor: AppColors.success,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _selectDateEcheance() async {
|
||||
final date = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: _dateEcheance ?? DateTime.now().add(const Duration(days: 30)),
|
||||
firstDate: DateTime.now(),
|
||||
lastDate: DateTime.now().add(const Duration(days: 365)),
|
||||
);
|
||||
|
||||
if (date != null) {
|
||||
setState(() {
|
||||
_dateEcheance = date;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _showValidationErrors(Map<String, String> erreurs) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Erreurs de validation'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: erreurs.entries.map((entry) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Text('• ${entry.value}'),
|
||||
)).toList(),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('OK'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,676 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../../core/widgets/unified_page_layout.dart';
|
||||
import '../../../../core/widgets/unified_card.dart';
|
||||
import '../../../../core/widgets/unified_list_widget.dart';
|
||||
import '../../../../core/theme/app_colors.dart';
|
||||
import '../../../../core/theme/app_text_styles.dart';
|
||||
import '../../../../core/utils/date_formatter.dart';
|
||||
import '../../../../core/utils/currency_formatter.dart';
|
||||
import '../../domain/entities/demande_aide.dart';
|
||||
import '../bloc/demandes_aide/demandes_aide_bloc.dart';
|
||||
import '../bloc/demandes_aide/demandes_aide_event.dart';
|
||||
import '../bloc/demandes_aide/demandes_aide_state.dart';
|
||||
import '../widgets/demande_aide_card.dart';
|
||||
import '../widgets/demandes_aide_filter_bottom_sheet.dart';
|
||||
import '../widgets/demandes_aide_sort_bottom_sheet.dart';
|
||||
|
||||
/// Page principale pour afficher la liste des demandes d'aide
|
||||
///
|
||||
/// Cette page utilise le pattern BLoC pour gérer l'état et affiche
|
||||
/// une liste paginée des demandes d'aide avec des fonctionnalités
|
||||
/// de filtrage, tri, recherche et sélection multiple.
|
||||
class DemandesAidePage extends StatefulWidget {
|
||||
final String? organisationId;
|
||||
final TypeAide? typeAideInitial;
|
||||
final StatutAide? statutInitial;
|
||||
|
||||
const DemandesAidePage({
|
||||
super.key,
|
||||
this.organisationId,
|
||||
this.typeAideInitial,
|
||||
this.statutInitial,
|
||||
});
|
||||
|
||||
@override
|
||||
State<DemandesAidePage> createState() => _DemandesAidePageState();
|
||||
}
|
||||
|
||||
class _DemandesAidePageState extends State<DemandesAidePage> {
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
bool _isSelectionMode = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_scrollController.addListener(_onScroll);
|
||||
|
||||
// Charger les demandes d'aide au démarrage
|
||||
context.read<DemandesAideBloc>().add(ChargerDemandesAideEvent(
|
||||
organisationId: widget.organisationId,
|
||||
typeAide: widget.typeAideInitial,
|
||||
statut: widget.statutInitial,
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollController.dispose();
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onScroll() {
|
||||
if (_isBottom) {
|
||||
context.read<DemandesAideBloc>().add(const ChargerPlusDemandesAideEvent());
|
||||
}
|
||||
}
|
||||
|
||||
bool get _isBottom {
|
||||
if (!_scrollController.hasClients) return false;
|
||||
final maxScroll = _scrollController.position.maxScrollExtent;
|
||||
final currentScroll = _scrollController.offset;
|
||||
return currentScroll >= (maxScroll * 0.9);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocConsumer<DemandesAideBloc, DemandesAideState>(
|
||||
listener: (context, state) {
|
||||
if (state is DemandesAideError) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.message),
|
||||
backgroundColor: AppColors.error,
|
||||
action: state.canRetry
|
||||
? SnackBarAction(
|
||||
label: 'Réessayer',
|
||||
textColor: Colors.white,
|
||||
onPressed: () => _rafraichir(),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
} else if (state is DemandesAideOperationSuccess) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.message),
|
||||
backgroundColor: AppColors.success,
|
||||
),
|
||||
);
|
||||
} else if (state is DemandesAideExported) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Fichier exporté: ${state.filePath}'),
|
||||
backgroundColor: AppColors.success,
|
||||
action: SnackBarAction(
|
||||
label: 'Ouvrir',
|
||||
textColor: Colors.white,
|
||||
onPressed: () => _ouvrirFichier(state.filePath),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
return UnifiedPageLayout(
|
||||
title: 'Demandes d\'aide',
|
||||
showBackButton: false,
|
||||
actions: _buildActions(state),
|
||||
floatingActionButton: _buildFloatingActionButton(),
|
||||
body: Column(
|
||||
children: [
|
||||
_buildSearchBar(state),
|
||||
_buildFilterChips(state),
|
||||
Expanded(child: _buildContent(state)),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildActions(DemandesAideState state) {
|
||||
final actions = <Widget>[];
|
||||
|
||||
if (_isSelectionMode && state is DemandesAideLoaded) {
|
||||
// Actions en mode sélection
|
||||
actions.addAll([
|
||||
IconButton(
|
||||
icon: const Icon(Icons.select_all),
|
||||
onPressed: () => _toggleSelectAll(state),
|
||||
tooltip: state.toutesDemandesSelectionnees
|
||||
? 'Désélectionner tout'
|
||||
: 'Sélectionner tout',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete),
|
||||
onPressed: state.nombreDemandesSelectionnees > 0
|
||||
? () => _supprimerSelection(state)
|
||||
: null,
|
||||
tooltip: 'Supprimer la sélection',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.file_download),
|
||||
onPressed: state.nombreDemandesSelectionnees > 0
|
||||
? () => _exporterSelection(state)
|
||||
: null,
|
||||
tooltip: 'Exporter la sélection',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: _quitterModeSelection,
|
||||
tooltip: 'Quitter la sélection',
|
||||
),
|
||||
]);
|
||||
} else {
|
||||
// Actions normales
|
||||
actions.addAll([
|
||||
IconButton(
|
||||
icon: const Icon(Icons.filter_list),
|
||||
onPressed: () => _afficherFiltres(state),
|
||||
tooltip: 'Filtrer',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.sort),
|
||||
onPressed: () => _afficherTri(state),
|
||||
tooltip: 'Trier',
|
||||
),
|
||||
PopupMenuButton<String>(
|
||||
icon: const Icon(Icons.more_vert),
|
||||
onSelected: (value) => _onMenuSelected(value, state),
|
||||
itemBuilder: (context) => [
|
||||
const PopupMenuItem(
|
||||
value: 'refresh',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.refresh),
|
||||
title: Text('Actualiser'),
|
||||
dense: true,
|
||||
),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
value: 'select',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.checklist),
|
||||
title: Text('Sélection multiple'),
|
||||
dense: true,
|
||||
),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
value: 'export_all',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.file_download),
|
||||
title: Text('Exporter tout'),
|
||||
dense: true,
|
||||
),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
value: 'urgentes',
|
||||
child: ListTile(
|
||||
leading: Icon(Icons.priority_high, color: AppColors.error),
|
||||
title: Text('Demandes urgentes'),
|
||||
dense: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
Widget _buildFloatingActionButton() {
|
||||
return FloatingActionButton.extended(
|
||||
onPressed: _creerNouvelleDemande,
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Nouvelle demande'),
|
||||
backgroundColor: AppColors.primary,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSearchBar(DemandesAideState state) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Rechercher des demandes...',
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
suffixIcon: _searchController.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
_searchController.clear();
|
||||
_rechercherDemandes('');
|
||||
},
|
||||
)
|
||||
: null,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
onChanged: _rechercherDemandes,
|
||||
onSubmitted: _rechercherDemandes,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFilterChips(DemandesAideState state) {
|
||||
if (state is! DemandesAideLoaded || !state.hasFiltres) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Container(
|
||||
height: 50,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
children: [
|
||||
if (state.filtres.typeAide != null)
|
||||
_buildFilterChip(
|
||||
'Type: ${state.filtres.typeAide!.libelle}',
|
||||
() => _supprimerFiltre('typeAide'),
|
||||
),
|
||||
if (state.filtres.statut != null)
|
||||
_buildFilterChip(
|
||||
'Statut: ${state.filtres.statut!.libelle}',
|
||||
() => _supprimerFiltre('statut'),
|
||||
),
|
||||
if (state.filtres.priorite != null)
|
||||
_buildFilterChip(
|
||||
'Priorité: ${state.filtres.priorite!.libelle}',
|
||||
() => _supprimerFiltre('priorite'),
|
||||
),
|
||||
if (state.filtres.urgente == true)
|
||||
_buildFilterChip(
|
||||
'Urgente',
|
||||
() => _supprimerFiltre('urgente'),
|
||||
),
|
||||
if (state.filtres.motCle != null && state.filtres.motCle!.isNotEmpty)
|
||||
_buildFilterChip(
|
||||
'Recherche: "${state.filtres.motCle}"',
|
||||
() => _supprimerFiltre('motCle'),
|
||||
),
|
||||
ActionChip(
|
||||
label: const Text('Effacer tout'),
|
||||
onPressed: _effacerTousFiltres,
|
||||
backgroundColor: AppColors.error.withOpacity(0.1),
|
||||
labelStyle: TextStyle(color: AppColors.error),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFilterChip(String label, VoidCallback onDeleted) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: Chip(
|
||||
label: Text(label),
|
||||
onDeleted: onDeleted,
|
||||
backgroundColor: AppColors.primary.withOpacity(0.1),
|
||||
labelStyle: TextStyle(color: AppColors.primary),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContent(DemandesAideState state) {
|
||||
if (state is DemandesAideInitial) {
|
||||
return const Center(
|
||||
child: Text('Appuyez sur actualiser pour charger les demandes'),
|
||||
);
|
||||
}
|
||||
|
||||
if (state is DemandesAideLoading && state.isRefreshing == false) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (state is DemandesAideError && !state.hasCachedData) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
state.isNetworkError ? Icons.wifi_off : Icons.error,
|
||||
size: 64,
|
||||
color: AppColors.error,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
state.message,
|
||||
style: AppTextStyles.bodyLarge,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (state.canRetry)
|
||||
ElevatedButton(
|
||||
onPressed: _rafraichir,
|
||||
child: const Text('Réessayer'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (state is DemandesAideExporting) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(value: state.progress),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
state.currentStep ?? 'Export en cours...',
|
||||
style: AppTextStyles.bodyLarge,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'${(state.progress * 100).toInt()}%',
|
||||
style: AppTextStyles.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (state is DemandesAideLoaded) {
|
||||
return _buildDemandesList(state);
|
||||
}
|
||||
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
Widget _buildDemandesList(DemandesAideLoaded state) {
|
||||
if (state.demandesFiltrees.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.inbox,
|
||||
size: 64,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
state.hasData
|
||||
? 'Aucun résultat pour les filtres appliqués'
|
||||
: 'Aucune demande d\'aide',
|
||||
style: AppTextStyles.bodyLarge,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (state.hasFiltres) ...[
|
||||
const SizedBox(height: 8),
|
||||
TextButton(
|
||||
onPressed: _effacerTousFiltres,
|
||||
child: const Text('Effacer les filtres'),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async => _rafraichir(),
|
||||
child: UnifiedListWidget<DemandeAide>(
|
||||
items: state.demandesFiltrees,
|
||||
itemBuilder: (context, demande, index) => DemandeAideCard(
|
||||
demande: demande,
|
||||
isSelected: state.demandesSelectionnees[demande.id] == true,
|
||||
isSelectionMode: _isSelectionMode,
|
||||
onTap: () => _onDemandeAideTap(demande),
|
||||
onLongPress: () => _onDemandeAideLongPress(demande),
|
||||
onSelectionChanged: (selected) => _onDemandeAideSelectionChanged(demande.id, selected),
|
||||
),
|
||||
scrollController: _scrollController,
|
||||
hasReachedMax: state.hasReachedMax,
|
||||
isLoading: state.isLoadingMore,
|
||||
emptyWidget: const SizedBox.shrink(), // Géré plus haut
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Méthodes d'action
|
||||
void _rafraichir() {
|
||||
context.read<DemandesAideBloc>().add(const RafraichirDemandesAideEvent());
|
||||
}
|
||||
|
||||
void _rechercherDemandes(String query) {
|
||||
context.read<DemandesAideBloc>().add(FiltrerDemandesAideEvent(motCle: query));
|
||||
}
|
||||
|
||||
void _afficherFiltres(DemandesAideState state) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => DemandesAideFilterBottomSheet(
|
||||
filtresActuels: state is DemandesAideLoaded ? state.filtres : const FiltresDemandesAide(),
|
||||
onFiltresChanged: (filtres) {
|
||||
context.read<DemandesAideBloc>().add(FiltrerDemandesAideEvent(
|
||||
typeAide: filtres.typeAide,
|
||||
statut: filtres.statut,
|
||||
priorite: filtres.priorite,
|
||||
urgente: filtres.urgente,
|
||||
motCle: filtres.motCle,
|
||||
));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _afficherTri(DemandesAideState state) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => DemandesAideSortBottomSheet(
|
||||
critereActuel: state is DemandesAideLoaded ? state.criterieTri : null,
|
||||
croissantActuel: state is DemandesAideLoaded ? state.triCroissant : true,
|
||||
onTriChanged: (critere, croissant) {
|
||||
context.read<DemandesAideBloc>().add(TrierDemandesAideEvent(
|
||||
critere: critere,
|
||||
croissant: croissant,
|
||||
));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onMenuSelected(String value, DemandesAideState state) {
|
||||
switch (value) {
|
||||
case 'refresh':
|
||||
_rafraichir();
|
||||
break;
|
||||
case 'select':
|
||||
_activerModeSelection();
|
||||
break;
|
||||
case 'export_all':
|
||||
if (state is DemandesAideLoaded) {
|
||||
_exporterTout(state);
|
||||
}
|
||||
break;
|
||||
case 'urgentes':
|
||||
_afficherDemandesUrgentes();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void _creerNouvelleDemande() {
|
||||
Navigator.pushNamed(context, '/solidarite/demandes/creer');
|
||||
}
|
||||
|
||||
void _onDemandeAideTap(DemandeAide demande) {
|
||||
if (_isSelectionMode) {
|
||||
_onDemandeAideSelectionChanged(
|
||||
demande.id,
|
||||
!(context.read<DemandesAideBloc>().state as DemandesAideLoaded)
|
||||
.demandesSelectionnees[demande.id] == true,
|
||||
);
|
||||
} else {
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
'/solidarite/demandes/details',
|
||||
arguments: demande.id,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onDemandeAideLongPress(DemandeAide demande) {
|
||||
if (!_isSelectionMode) {
|
||||
_activerModeSelection();
|
||||
_onDemandeAideSelectionChanged(demande.id, true);
|
||||
}
|
||||
}
|
||||
|
||||
void _onDemandeAideSelectionChanged(String demandeId, bool selected) {
|
||||
context.read<DemandesAideBloc>().add(SelectionnerDemandeAideEvent(
|
||||
demandeId: demandeId,
|
||||
selectionne: selected,
|
||||
));
|
||||
}
|
||||
|
||||
void _activerModeSelection() {
|
||||
setState(() {
|
||||
_isSelectionMode = true;
|
||||
});
|
||||
}
|
||||
|
||||
void _quitterModeSelection() {
|
||||
setState(() {
|
||||
_isSelectionMode = false;
|
||||
});
|
||||
context.read<DemandesAideBloc>().add(const SelectionnerToutesDemandesAideEvent(selectionne: false));
|
||||
}
|
||||
|
||||
void _toggleSelectAll(DemandesAideLoaded state) {
|
||||
context.read<DemandesAideBloc>().add(SelectionnerToutesDemandesAideEvent(
|
||||
selectionne: !state.toutesDemandesSelectionnees,
|
||||
));
|
||||
}
|
||||
|
||||
void _supprimerSelection(DemandesAideLoaded state) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Confirmer la suppression'),
|
||||
content: Text(
|
||||
'Êtes-vous sûr de vouloir supprimer ${state.nombreDemandesSelectionnees} demande(s) d\'aide ?',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
context.read<DemandesAideBloc>().add(SupprimerDemandesSelectionnees(
|
||||
demandeIds: state.demandesSelectionneesIds,
|
||||
));
|
||||
_quitterModeSelection();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(backgroundColor: AppColors.error),
|
||||
child: const Text('Supprimer'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _exporterSelection(DemandesAideLoaded state) {
|
||||
_afficherDialogueExport(state.demandesSelectionneesIds);
|
||||
}
|
||||
|
||||
void _exporterTout(DemandesAideLoaded state) {
|
||||
_afficherDialogueExport(state.demandesFiltrees.map((d) => d.id).toList());
|
||||
}
|
||||
|
||||
void _afficherDialogueExport(List<String> demandeIds) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Exporter les demandes'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: FormatExport.values.map((format) => ListTile(
|
||||
leading: Icon(_getFormatIcon(format)),
|
||||
title: Text(format.libelle),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
context.read<DemandesAideBloc>().add(ExporterDemandesAideEvent(
|
||||
demandeIds: demandeIds,
|
||||
format: format,
|
||||
));
|
||||
},
|
||||
)).toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
IconData _getFormatIcon(FormatExport format) {
|
||||
switch (format) {
|
||||
case FormatExport.pdf:
|
||||
return Icons.picture_as_pdf;
|
||||
case FormatExport.excel:
|
||||
return Icons.table_chart;
|
||||
case FormatExport.csv:
|
||||
return Icons.grid_on;
|
||||
case FormatExport.json:
|
||||
return Icons.code;
|
||||
}
|
||||
}
|
||||
|
||||
void _afficherDemandesUrgentes() {
|
||||
context.read<DemandesAideBloc>().add(ChargerDemandesUrgentesEvent(
|
||||
organisationId: widget.organisationId ?? '',
|
||||
));
|
||||
}
|
||||
|
||||
void _supprimerFiltre(String filtre) {
|
||||
final state = context.read<DemandesAideBloc>().state;
|
||||
if (state is DemandesAideLoaded) {
|
||||
var nouveauxFiltres = state.filtres;
|
||||
|
||||
switch (filtre) {
|
||||
case 'typeAide':
|
||||
nouveauxFiltres = nouveauxFiltres.copyWith(typeAide: null);
|
||||
break;
|
||||
case 'statut':
|
||||
nouveauxFiltres = nouveauxFiltres.copyWith(statut: null);
|
||||
break;
|
||||
case 'priorite':
|
||||
nouveauxFiltres = nouveauxFiltres.copyWith(priorite: null);
|
||||
break;
|
||||
case 'urgente':
|
||||
nouveauxFiltres = nouveauxFiltres.copyWith(urgente: null);
|
||||
break;
|
||||
case 'motCle':
|
||||
nouveauxFiltres = nouveauxFiltres.copyWith(motCle: '');
|
||||
_searchController.clear();
|
||||
break;
|
||||
}
|
||||
|
||||
context.read<DemandesAideBloc>().add(FiltrerDemandesAideEvent(
|
||||
typeAide: nouveauxFiltres.typeAide,
|
||||
statut: nouveauxFiltres.statut,
|
||||
priorite: nouveauxFiltres.priorite,
|
||||
urgente: nouveauxFiltres.urgente,
|
||||
motCle: nouveauxFiltres.motCle,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
void _effacerTousFiltres() {
|
||||
_searchController.clear();
|
||||
context.read<DemandesAideBloc>().add(const FiltrerDemandesAideEvent());
|
||||
}
|
||||
|
||||
void _ouvrirFichier(String filePath) {
|
||||
// Implémenter l'ouverture du fichier avec un package comme open_file
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Ouverture du fichier: $filePath')),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,407 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../core/widgets/unified_card.dart';
|
||||
import '../../../../core/theme/app_colors.dart';
|
||||
import '../../../../core/theme/app_text_styles.dart';
|
||||
import '../../../../core/utils/date_formatter.dart';
|
||||
import '../../../../core/utils/currency_formatter.dart';
|
||||
import '../../domain/entities/demande_aide.dart';
|
||||
|
||||
/// Widget de carte pour afficher une demande d'aide
|
||||
///
|
||||
/// Cette carte affiche les informations essentielles d'une demande d'aide
|
||||
/// avec un design cohérent et des interactions tactiles.
|
||||
class DemandeAideCard extends StatelessWidget {
|
||||
final DemandeAide demande;
|
||||
final bool isSelected;
|
||||
final bool isSelectionMode;
|
||||
final VoidCallback? onTap;
|
||||
final VoidCallback? onLongPress;
|
||||
final ValueChanged<bool>? onSelectionChanged;
|
||||
|
||||
const DemandeAideCard({
|
||||
super.key,
|
||||
required this.demande,
|
||||
this.isSelected = false,
|
||||
this.isSelectionMode = false,
|
||||
this.onTap,
|
||||
this.onLongPress,
|
||||
this.onSelectionChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return UnifiedCard(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
onLongPress: onLongPress,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: isSelected
|
||||
? Border.all(color: AppColors.primary, width: 2)
|
||||
: null,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildHeader(),
|
||||
const SizedBox(height: 12),
|
||||
_buildContent(),
|
||||
const SizedBox(height: 12),
|
||||
_buildFooter(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
return Row(
|
||||
children: [
|
||||
if (isSelectionMode) ...[
|
||||
Checkbox(
|
||||
value: isSelected,
|
||||
onChanged: onSelectionChanged,
|
||||
activeColor: AppColors.primary,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
demande.titre,
|
||||
style: AppTextStyles.titleMedium.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_buildStatutChip(),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.person,
|
||||
size: 16,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
demande.nomDemandeur,
|
||||
style: AppTextStyles.bodySmall.copyWith(
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
demande.numeroReference,
|
||||
style: AppTextStyles.bodySmall.copyWith(
|
||||
color: AppColors.textSecondary,
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (demande.estUrgente) ...[
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.error.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.priority_high,
|
||||
size: 16,
|
||||
color: AppColors.error,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'URGENT',
|
||||
style: AppTextStyles.labelSmall.copyWith(
|
||||
color: AppColors.error,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContent() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
demande.description,
|
||||
style: AppTextStyles.bodyMedium,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
_buildTypeAideChip(),
|
||||
const SizedBox(width: 8),
|
||||
_buildPrioriteChip(),
|
||||
const Spacer(),
|
||||
if (demande.montantDemande != null)
|
||||
Text(
|
||||
CurrencyFormatter.formatCFA(demande.montantDemande!),
|
||||
style: AppTextStyles.titleSmall.copyWith(
|
||||
color: AppColors.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFooter() {
|
||||
return Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.access_time,
|
||||
size: 16,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'Créée ${DateFormatter.formatRelative(demande.dateCreation)}',
|
||||
style: AppTextStyles.bodySmall.copyWith(
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
if (demande.dateModification != demande.dateCreation) ...[
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'• Modifiée ${DateFormatter.formatRelative(demande.dateModification)}',
|
||||
style: AppTextStyles.bodySmall.copyWith(
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
const Spacer(),
|
||||
_buildProgressIndicator(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatutChip() {
|
||||
final color = _getStatutColor(demande.statut);
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
demande.statut.libelle,
|
||||
style: AppTextStyles.labelSmall.copyWith(
|
||||
color: color,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTypeAideChip() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: AppColors.outline),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
_getTypeAideIcon(demande.typeAide),
|
||||
size: 14,
|
||||
color: AppColors.primary,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
demande.typeAide.libelle,
|
||||
style: AppTextStyles.labelSmall.copyWith(
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPrioriteChip() {
|
||||
final color = _getPrioriteColor(demande.priorite);
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
_getPrioriteIcon(demande.priorite),
|
||||
size: 14,
|
||||
color: color,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
demande.priorite.libelle,
|
||||
style: AppTextStyles.labelSmall.copyWith(
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProgressIndicator() {
|
||||
final progress = demande.pourcentageAvancement;
|
||||
final color = _getProgressColor(progress);
|
||||
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 60,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.outline,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
child: FractionallySizedBox(
|
||||
alignment: Alignment.centerLeft,
|
||||
widthFactor: progress / 100,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'${progress.toInt()}%',
|
||||
style: AppTextStyles.labelSmall.copyWith(
|
||||
color: color,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Color _getStatutColor(StatutAide statut) {
|
||||
switch (statut) {
|
||||
case StatutAide.brouillon:
|
||||
return AppColors.textSecondary;
|
||||
case StatutAide.soumise:
|
||||
return AppColors.warning;
|
||||
case StatutAide.enEvaluation:
|
||||
return AppColors.info;
|
||||
case StatutAide.approuvee:
|
||||
return AppColors.success;
|
||||
case StatutAide.rejetee:
|
||||
return AppColors.error;
|
||||
case StatutAide.enCours:
|
||||
return AppColors.primary;
|
||||
case StatutAide.terminee:
|
||||
return AppColors.success;
|
||||
case StatutAide.versee:
|
||||
return AppColors.success;
|
||||
case StatutAide.livree:
|
||||
return AppColors.success;
|
||||
case StatutAide.annulee:
|
||||
return AppColors.error;
|
||||
}
|
||||
}
|
||||
|
||||
Color _getPrioriteColor(PrioriteAide priorite) {
|
||||
switch (priorite) {
|
||||
case PrioriteAide.basse:
|
||||
return AppColors.success;
|
||||
case PrioriteAide.normale:
|
||||
return AppColors.info;
|
||||
case PrioriteAide.haute:
|
||||
return AppColors.warning;
|
||||
case PrioriteAide.critique:
|
||||
return AppColors.error;
|
||||
}
|
||||
}
|
||||
|
||||
Color _getProgressColor(double progress) {
|
||||
if (progress < 25) return AppColors.error;
|
||||
if (progress < 50) return AppColors.warning;
|
||||
if (progress < 75) return AppColors.info;
|
||||
return AppColors.success;
|
||||
}
|
||||
|
||||
IconData _getTypeAideIcon(TypeAide typeAide) {
|
||||
switch (typeAide) {
|
||||
case TypeAide.aideFinanciereUrgente:
|
||||
return Icons.attach_money;
|
||||
case TypeAide.aideFinanciereMedicale:
|
||||
return Icons.medical_services;
|
||||
case TypeAide.aideFinanciereEducation:
|
||||
return Icons.school;
|
||||
case TypeAide.aideMaterielleVetements:
|
||||
return Icons.checkroom;
|
||||
case TypeAide.aideMaterielleNourriture:
|
||||
return Icons.restaurant;
|
||||
case TypeAide.aideProfessionnelleFormation:
|
||||
return Icons.work;
|
||||
case TypeAide.aideSocialeAccompagnement:
|
||||
return Icons.support;
|
||||
case TypeAide.autre:
|
||||
return Icons.help;
|
||||
}
|
||||
}
|
||||
|
||||
IconData _getPrioriteIcon(PrioriteAide priorite) {
|
||||
switch (priorite) {
|
||||
case PrioriteAide.basse:
|
||||
return Icons.keyboard_arrow_down;
|
||||
case PrioriteAide.normale:
|
||||
return Icons.remove;
|
||||
case PrioriteAide.haute:
|
||||
return Icons.keyboard_arrow_up;
|
||||
case PrioriteAide.critique:
|
||||
return Icons.priority_high;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../core/widgets/unified_card.dart';
|
||||
import '../../../../core/theme/app_colors.dart';
|
||||
import '../../../../core/theme/app_text_styles.dart';
|
||||
import '../../../../core/utils/file_utils.dart';
|
||||
import '../../domain/entities/demande_aide.dart';
|
||||
|
||||
/// Widget pour afficher la section des documents d'une demande d'aide
|
||||
///
|
||||
/// Ce widget affiche tous les documents joints à une demande d'aide
|
||||
/// avec la possibilité de les visualiser et télécharger.
|
||||
class DemandeAideDocumentsSection extends StatelessWidget {
|
||||
final DemandeAide demande;
|
||||
|
||||
const DemandeAideDocumentsSection({
|
||||
super.key,
|
||||
required this.demande,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (demande.piecesJustificatives.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return UnifiedCard(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'Documents joints',
|
||||
style: AppTextStyles.titleMedium.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
'${demande.piecesJustificatives.length}',
|
||||
style: AppTextStyles.labelSmall.copyWith(
|
||||
color: AppColors.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
...demande.piecesJustificatives.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final document = entry.value;
|
||||
final isLast = index == demande.piecesJustificatives.length - 1;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
_buildDocumentCard(context, document),
|
||||
if (!isLast) const SizedBox(height: 8),
|
||||
],
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDocumentCard(BuildContext context, PieceJustificative document) {
|
||||
final fileExtension = _getFileExtension(document.nomFichier);
|
||||
final fileIcon = _getFileIcon(fileExtension);
|
||||
final fileColor = _getFileColor(fileExtension);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: AppColors.outline),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: fileColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
fileIcon,
|
||||
color: fileColor,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
document.nomFichier,
|
||||
style: AppTextStyles.bodyMedium.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
document.typeDocument.libelle,
|
||||
style: AppTextStyles.bodySmall.copyWith(
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
if (document.tailleFichier != null) ...[
|
||||
Text(
|
||||
' • ',
|
||||
style: AppTextStyles.bodySmall.copyWith(
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_formatFileSize(document.tailleFichier!),
|
||||
style: AppTextStyles.bodySmall.copyWith(
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
if (document.description != null && document.description!.isNotEmpty) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
document.description!,
|
||||
style: AppTextStyles.bodySmall.copyWith(
|
||||
color: AppColors.textSecondary,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Column(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () => _previewDocument(context, document),
|
||||
icon: const Icon(Icons.visibility),
|
||||
tooltip: 'Aperçu',
|
||||
iconSize: 20,
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () => _downloadDocument(context, document),
|
||||
icon: const Icon(Icons.download),
|
||||
tooltip: 'Télécharger',
|
||||
iconSize: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _getFileExtension(String fileName) {
|
||||
final parts = fileName.split('.');
|
||||
return parts.length > 1 ? parts.last.toLowerCase() : '';
|
||||
}
|
||||
|
||||
IconData _getFileIcon(String extension) {
|
||||
switch (extension) {
|
||||
case 'pdf':
|
||||
return Icons.picture_as_pdf;
|
||||
case 'doc':
|
||||
case 'docx':
|
||||
return Icons.description;
|
||||
case 'xls':
|
||||
case 'xlsx':
|
||||
return Icons.table_chart;
|
||||
case 'ppt':
|
||||
case 'pptx':
|
||||
return Icons.slideshow;
|
||||
case 'jpg':
|
||||
case 'jpeg':
|
||||
case 'png':
|
||||
case 'gif':
|
||||
case 'bmp':
|
||||
return Icons.image;
|
||||
case 'mp4':
|
||||
case 'avi':
|
||||
case 'mov':
|
||||
case 'wmv':
|
||||
return Icons.video_file;
|
||||
case 'mp3':
|
||||
case 'wav':
|
||||
case 'aac':
|
||||
return Icons.audio_file;
|
||||
case 'zip':
|
||||
case 'rar':
|
||||
case '7z':
|
||||
return Icons.archive;
|
||||
case 'txt':
|
||||
return Icons.text_snippet;
|
||||
default:
|
||||
return Icons.insert_drive_file;
|
||||
}
|
||||
}
|
||||
|
||||
Color _getFileColor(String extension) {
|
||||
switch (extension) {
|
||||
case 'pdf':
|
||||
return Colors.red;
|
||||
case 'doc':
|
||||
case 'docx':
|
||||
return Colors.blue;
|
||||
case 'xls':
|
||||
case 'xlsx':
|
||||
return Colors.green;
|
||||
case 'ppt':
|
||||
case 'pptx':
|
||||
return Colors.orange;
|
||||
case 'jpg':
|
||||
case 'jpeg':
|
||||
case 'png':
|
||||
case 'gif':
|
||||
case 'bmp':
|
||||
return Colors.purple;
|
||||
case 'mp4':
|
||||
case 'avi':
|
||||
case 'mov':
|
||||
case 'wmv':
|
||||
return Colors.indigo;
|
||||
case 'mp3':
|
||||
case 'wav':
|
||||
case 'aac':
|
||||
return Colors.teal;
|
||||
case 'zip':
|
||||
case 'rar':
|
||||
case '7z':
|
||||
return Colors.brown;
|
||||
case 'txt':
|
||||
return Colors.grey;
|
||||
default:
|
||||
return AppColors.textSecondary;
|
||||
}
|
||||
}
|
||||
|
||||
String _formatFileSize(int bytes) {
|
||||
if (bytes < 1024) {
|
||||
return '$bytes B';
|
||||
} else if (bytes < 1024 * 1024) {
|
||||
return '${(bytes / 1024).toStringAsFixed(1)} KB';
|
||||
} else if (bytes < 1024 * 1024 * 1024) {
|
||||
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
|
||||
} else {
|
||||
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB';
|
||||
}
|
||||
}
|
||||
|
||||
void _previewDocument(BuildContext context, PieceJustificative document) {
|
||||
// Implémenter la prévisualisation du document
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text('Aperçu du document'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Nom: ${document.nomFichier}'),
|
||||
Text('Type: ${document.typeDocument.libelle}'),
|
||||
if (document.tailleFichier != null)
|
||||
Text('Taille: ${_formatFileSize(document.tailleFichier!)}'),
|
||||
if (document.description != null && document.description!.isNotEmpty)
|
||||
Text('Description: ${document.description}'),
|
||||
const SizedBox(height: 16),
|
||||
const Text('Fonctionnalité de prévisualisation à implémenter'),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Fermer'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
_downloadDocument(context, document);
|
||||
},
|
||||
child: const Text('Télécharger'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _downloadDocument(BuildContext context, PieceJustificative document) {
|
||||
// Implémenter le téléchargement du document
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Téléchargement de ${document.nomFichier}...'),
|
||||
action: SnackBarAction(
|
||||
label: 'Annuler',
|
||||
onPressed: () {
|
||||
// Annuler le téléchargement
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Simuler le téléchargement
|
||||
Future.delayed(const Duration(seconds: 2), () {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('${document.nomFichier} téléchargé avec succès'),
|
||||
backgroundColor: AppColors.success,
|
||||
action: SnackBarAction(
|
||||
label: 'Ouvrir',
|
||||
textColor: Colors.white,
|
||||
onPressed: () {
|
||||
// Ouvrir le fichier téléchargé
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,412 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../core/widgets/unified_card.dart';
|
||||
import '../../../../core/theme/app_colors.dart';
|
||||
import '../../../../core/theme/app_text_styles.dart';
|
||||
import '../../../../core/utils/date_formatter.dart';
|
||||
import '../../../../core/utils/currency_formatter.dart';
|
||||
import '../../domain/entities/demande_aide.dart';
|
||||
|
||||
/// Widget pour afficher la section des évaluations d'une demande d'aide
|
||||
///
|
||||
/// Ce widget affiche toutes les évaluations effectuées sur une demande d'aide
|
||||
/// avec les détails de chaque évaluation.
|
||||
class DemandeAideEvaluationSection extends StatelessWidget {
|
||||
final DemandeAide demande;
|
||||
|
||||
const DemandeAideEvaluationSection({
|
||||
super.key,
|
||||
required this.demande,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (demande.evaluations.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return UnifiedCard(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'Évaluations',
|
||||
style: AppTextStyles.titleMedium.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
'${demande.evaluations.length}',
|
||||
style: AppTextStyles.labelSmall.copyWith(
|
||||
color: AppColors.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
...demande.evaluations.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final evaluation = entry.value;
|
||||
final isLast = index == demande.evaluations.length - 1;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
_buildEvaluationCard(evaluation),
|
||||
if (!isLast) const SizedBox(height: 12),
|
||||
],
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEvaluationCard(EvaluationAide evaluation) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppColors.outline),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildEvaluationHeader(evaluation),
|
||||
const SizedBox(height: 12),
|
||||
_buildEvaluationContent(evaluation),
|
||||
if (evaluation.commentaire != null && evaluation.commentaire!.isNotEmpty) ...[
|
||||
const SizedBox(height: 12),
|
||||
_buildCommentaireSection(evaluation.commentaire!),
|
||||
],
|
||||
if (evaluation.criteres.isNotEmpty) ...[
|
||||
const SizedBox(height: 12),
|
||||
_buildCriteresSection(evaluation.criteres),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEvaluationHeader(EvaluationAide evaluation) {
|
||||
final color = _getDecisionColor(evaluation.decision);
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 20,
|
||||
backgroundColor: color.withOpacity(0.1),
|
||||
child: Icon(
|
||||
_getDecisionIcon(evaluation.decision),
|
||||
color: color,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
evaluation.nomEvaluateur,
|
||||
style: AppTextStyles.bodyMedium.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
evaluation.typeEvaluateur.libelle,
|
||||
style: AppTextStyles.bodySmall.copyWith(
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
evaluation.decision.libelle,
|
||||
style: AppTextStyles.labelSmall.copyWith(
|
||||
color: color,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
DateFormatter.formatShort(evaluation.dateEvaluation),
|
||||
style: AppTextStyles.bodySmall.copyWith(
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEvaluationContent(EvaluationAide evaluation) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (evaluation.noteGlobale != null) ...[
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'Note globale:',
|
||||
style: AppTextStyles.bodySmall.copyWith(
|
||||
color: AppColors.textSecondary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_buildStarRating(evaluation.noteGlobale!),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'${evaluation.noteGlobale!.toStringAsFixed(1)}/5',
|
||||
style: AppTextStyles.bodySmall.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
if (evaluation.montantRecommande != null) ...[
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.attach_money,
|
||||
size: 16,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'Montant recommandé:',
|
||||
style: AppTextStyles.bodySmall.copyWith(
|
||||
color: AppColors.textSecondary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
CurrencyFormatter.formatCFA(evaluation.montantRecommande!),
|
||||
style: AppTextStyles.bodyMedium.copyWith(
|
||||
color: AppColors.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
if (evaluation.prioriteRecommandee != null) ...[
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.priority_high,
|
||||
size: 16,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'Priorité recommandée:',
|
||||
style: AppTextStyles.bodySmall.copyWith(
|
||||
color: AppColors.textSecondary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: _getPrioriteColor(evaluation.prioriteRecommandee!).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
evaluation.prioriteRecommandee!.libelle,
|
||||
style: AppTextStyles.labelSmall.copyWith(
|
||||
color: _getPrioriteColor(evaluation.prioriteRecommandee!),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCommentaireSection(String commentaire) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.background,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: AppColors.outline),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.comment,
|
||||
size: 16,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'Commentaire',
|
||||
style: AppTextStyles.bodySmall.copyWith(
|
||||
color: AppColors.textSecondary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
commentaire,
|
||||
style: AppTextStyles.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCriteresSection(Map<String, double> criteres) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.background,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: AppColors.outline),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.checklist,
|
||||
size: 16,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'Critères d\'évaluation',
|
||||
style: AppTextStyles.bodySmall.copyWith(
|
||||
color: AppColors.textSecondary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
...criteres.entries.map((entry) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
entry.key,
|
||||
style: AppTextStyles.bodySmall,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_buildStarRating(entry.value),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'${entry.value.toStringAsFixed(1)}/5',
|
||||
style: AppTextStyles.bodySmall.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStarRating(double rating) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: List.generate(5, (index) {
|
||||
final starValue = index + 1;
|
||||
return Icon(
|
||||
starValue <= rating
|
||||
? Icons.star
|
||||
: starValue - 0.5 <= rating
|
||||
? Icons.star_half
|
||||
: Icons.star_border,
|
||||
size: 16,
|
||||
color: AppColors.warning,
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Color _getDecisionColor(StatutAide decision) {
|
||||
switch (decision) {
|
||||
case StatutAide.approuvee:
|
||||
return AppColors.success;
|
||||
case StatutAide.rejetee:
|
||||
return AppColors.error;
|
||||
case StatutAide.enEvaluation:
|
||||
return AppColors.info;
|
||||
default:
|
||||
return AppColors.textSecondary;
|
||||
}
|
||||
}
|
||||
|
||||
IconData _getDecisionIcon(StatutAide decision) {
|
||||
switch (decision) {
|
||||
case StatutAide.approuvee:
|
||||
return Icons.check_circle;
|
||||
case StatutAide.rejetee:
|
||||
return Icons.cancel;
|
||||
case StatutAide.enEvaluation:
|
||||
return Icons.rate_review;
|
||||
default:
|
||||
return Icons.help;
|
||||
}
|
||||
}
|
||||
|
||||
Color _getPrioriteColor(PrioriteAide priorite) {
|
||||
switch (priorite) {
|
||||
case PrioriteAide.basse:
|
||||
return AppColors.success;
|
||||
case PrioriteAide.normale:
|
||||
return AppColors.info;
|
||||
case PrioriteAide.haute:
|
||||
return AppColors.warning;
|
||||
case PrioriteAide.critique:
|
||||
return AppColors.error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,744 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../core/widgets/unified_card.dart';
|
||||
import '../../../../core/theme/app_colors.dart';
|
||||
import '../../../../core/theme/app_text_styles.dart';
|
||||
import '../../../../core/utils/validators.dart';
|
||||
import '../../domain/entities/demande_aide.dart';
|
||||
|
||||
/// Section du formulaire pour les bénéficiaires
|
||||
class DemandeAideFormBeneficiairesSection extends StatefulWidget {
|
||||
final List<BeneficiaireAide> beneficiaires;
|
||||
final ValueChanged<List<BeneficiaireAide>> onBeneficiairesChanged;
|
||||
|
||||
const DemandeAideFormBeneficiairesSection({
|
||||
super.key,
|
||||
required this.beneficiaires,
|
||||
required this.onBeneficiairesChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
State<DemandeAideFormBeneficiairesSection> createState() => _DemandeAideFormBeneficiairesState();
|
||||
}
|
||||
|
||||
class _DemandeAideFormBeneficiairesState extends State<DemandeAideFormBeneficiairesSection> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
UnifiedCard(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'Bénéficiaires',
|
||||
style: AppTextStyles.titleMedium.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
TextButton.icon(
|
||||
onPressed: _ajouterBeneficiaire,
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Ajouter'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Ajoutez les personnes qui bénéficieront de cette aide (optionnel)',
|
||||
style: AppTextStyles.bodySmall.copyWith(
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (widget.beneficiaires.isEmpty)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: AppColors.outline),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.people_outline,
|
||||
size: 48,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Aucun bénéficiaire ajouté',
|
||||
style: AppTextStyles.bodyMedium.copyWith(
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
...widget.beneficiaires.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final beneficiaire = entry.value;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: _buildBeneficiaireCard(beneficiaire, index),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBeneficiaireCard(BeneficiaireAide beneficiaire, int index) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: AppColors.outline),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
backgroundColor: AppColors.primary.withOpacity(0.1),
|
||||
child: Icon(
|
||||
Icons.person,
|
||||
color: AppColors.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${beneficiaire.prenom} ${beneficiaire.nom}',
|
||||
style: AppTextStyles.bodyMedium.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
if (beneficiaire.age != null)
|
||||
Text(
|
||||
'${beneficiaire.age} ans',
|
||||
style: AppTextStyles.bodySmall.copyWith(
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () => _modifierBeneficiaire(index),
|
||||
icon: const Icon(Icons.edit),
|
||||
iconSize: 20,
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () => _supprimerBeneficiaire(index),
|
||||
icon: const Icon(Icons.delete),
|
||||
iconSize: 20,
|
||||
color: AppColors.error,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _ajouterBeneficiaire() {
|
||||
_showBeneficiaireDialog();
|
||||
}
|
||||
|
||||
void _modifierBeneficiaire(int index) {
|
||||
_showBeneficiaireDialog(beneficiaire: widget.beneficiaires[index], index: index);
|
||||
}
|
||||
|
||||
void _supprimerBeneficiaire(int index) {
|
||||
final nouveauxBeneficiaires = List<BeneficiaireAide>.from(widget.beneficiaires);
|
||||
nouveauxBeneficiaires.removeAt(index);
|
||||
widget.onBeneficiairesChanged(nouveauxBeneficiaires);
|
||||
}
|
||||
|
||||
void _showBeneficiaireDialog({BeneficiaireAide? beneficiaire, int? index}) {
|
||||
final prenomController = TextEditingController(text: beneficiaire?.prenom ?? '');
|
||||
final nomController = TextEditingController(text: beneficiaire?.nom ?? '');
|
||||
final ageController = TextEditingController(text: beneficiaire?.age?.toString() ?? '');
|
||||
final formKey = GlobalKey<FormState>();
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(beneficiaire == null ? 'Ajouter un bénéficiaire' : 'Modifier le bénéficiaire'),
|
||||
content: Form(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: prenomController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Prénom *',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
validator: Validators.required,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: nomController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Nom *',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
validator: Validators.required,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: ageController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Âge',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
validator: (value) {
|
||||
if (value != null && value.isNotEmpty) {
|
||||
final age = int.tryParse(value);
|
||||
if (age == null || age < 0 || age > 150) {
|
||||
return 'Veuillez saisir un âge valide';
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
if (formKey.currentState!.validate()) {
|
||||
final nouveauBeneficiaire = BeneficiaireAide(
|
||||
prenom: prenomController.text,
|
||||
nom: nomController.text,
|
||||
age: ageController.text.isEmpty ? null : int.parse(ageController.text),
|
||||
);
|
||||
|
||||
final nouveauxBeneficiaires = List<BeneficiaireAide>.from(widget.beneficiaires);
|
||||
if (index != null) {
|
||||
nouveauxBeneficiaires[index] = nouveauBeneficiaire;
|
||||
} else {
|
||||
nouveauxBeneficiaires.add(nouveauBeneficiaire);
|
||||
}
|
||||
|
||||
widget.onBeneficiairesChanged(nouveauxBeneficiaires);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
child: Text(beneficiaire == null ? 'Ajouter' : 'Modifier'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Section du formulaire pour le contact d'urgence
|
||||
class DemandeAideFormContactSection extends StatefulWidget {
|
||||
final ContactUrgence? contactUrgence;
|
||||
final ValueChanged<ContactUrgence?> onContactChanged;
|
||||
|
||||
const DemandeAideFormContactSection({
|
||||
super.key,
|
||||
required this.contactUrgence,
|
||||
required this.onContactChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
State<DemandeAideFormContactSection> createState() => _DemandeAideFormContactSectionState();
|
||||
}
|
||||
|
||||
class _DemandeAideFormContactSectionState extends State<DemandeAideFormContactSection> {
|
||||
final _prenomController = TextEditingController();
|
||||
final _nomController = TextEditingController();
|
||||
final _telephoneController = TextEditingController();
|
||||
final _emailController = TextEditingController();
|
||||
final _relationController = TextEditingController();
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.contactUrgence != null) {
|
||||
_prenomController.text = widget.contactUrgence!.prenom;
|
||||
_nomController.text = widget.contactUrgence!.nom;
|
||||
_telephoneController.text = widget.contactUrgence!.telephone;
|
||||
_emailController.text = widget.contactUrgence!.email ?? '';
|
||||
_relationController.text = widget.contactUrgence!.relation;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_prenomController.dispose();
|
||||
_nomController.dispose();
|
||||
_telephoneController.dispose();
|
||||
_emailController.dispose();
|
||||
_relationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
UnifiedCard(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Contact d\'urgence',
|
||||
style: AppTextStyles.titleMedium.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Personne à contacter en cas d\'urgence (optionnel)',
|
||||
style: AppTextStyles.bodySmall.copyWith(
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _prenomController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Prénom',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
onChanged: _updateContact,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _nomController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Nom',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
onChanged: _updateContact,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _telephoneController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Téléphone',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.phone),
|
||||
),
|
||||
keyboardType: TextInputType.phone,
|
||||
onChanged: _updateContact,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _emailController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Email',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.email),
|
||||
),
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
onChanged: _updateContact,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _relationController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Relation',
|
||||
hintText: 'Ex: Conjoint, Parent, Ami...',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.family_restroom),
|
||||
),
|
||||
onChanged: _updateContact,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _updateContact(String value) {
|
||||
if (_prenomController.text.isNotEmpty ||
|
||||
_nomController.text.isNotEmpty ||
|
||||
_telephoneController.text.isNotEmpty ||
|
||||
_emailController.text.isNotEmpty ||
|
||||
_relationController.text.isNotEmpty) {
|
||||
final contact = ContactUrgence(
|
||||
prenom: _prenomController.text,
|
||||
nom: _nomController.text,
|
||||
telephone: _telephoneController.text,
|
||||
email: _emailController.text.isEmpty ? null : _emailController.text,
|
||||
relation: _relationController.text,
|
||||
);
|
||||
widget.onContactChanged(contact);
|
||||
} else {
|
||||
widget.onContactChanged(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Section du formulaire pour la localisation
|
||||
class DemandeAideFormLocalisationSection extends StatefulWidget {
|
||||
final Localisation? localisation;
|
||||
final ValueChanged<Localisation?> onLocalisationChanged;
|
||||
|
||||
const DemandeAideFormLocalisationSection({
|
||||
super.key,
|
||||
required this.localisation,
|
||||
required this.onLocalisationChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
State<DemandeAideFormLocalisationSection> createState() => _DemandeAideFormLocalisationSectionState();
|
||||
}
|
||||
|
||||
class _DemandeAideFormLocalisationSectionState extends State<DemandeAideFormLocalisationSection> {
|
||||
final _adresseController = TextEditingController();
|
||||
final _villeController = TextEditingController();
|
||||
final _codePostalController = TextEditingController();
|
||||
final _paysController = TextEditingController();
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.localisation != null) {
|
||||
_adresseController.text = widget.localisation!.adresse;
|
||||
_villeController.text = widget.localisation!.ville ?? '';
|
||||
_codePostalController.text = widget.localisation!.codePostal ?? '';
|
||||
_paysController.text = widget.localisation!.pays ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_adresseController.dispose();
|
||||
_villeController.dispose();
|
||||
_codePostalController.dispose();
|
||||
_paysController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
UnifiedCard(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Localisation',
|
||||
style: AppTextStyles.titleMedium.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Lieu où l\'aide sera fournie (optionnel)',
|
||||
style: AppTextStyles.bodySmall.copyWith(
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _adresseController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Adresse',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.location_on),
|
||||
),
|
||||
maxLines: 2,
|
||||
onChanged: _updateLocalisation,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _villeController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Ville',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
onChanged: _updateLocalisation,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _codePostalController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Code postal',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
onChanged: _updateLocalisation,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _paysController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Pays',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.flag),
|
||||
),
|
||||
onChanged: _updateLocalisation,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
OutlinedButton.icon(
|
||||
onPressed: _utiliserPositionActuelle,
|
||||
icon: const Icon(Icons.my_location),
|
||||
label: const Text('Utiliser ma position actuelle'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _updateLocalisation(String value) {
|
||||
if (_adresseController.text.isNotEmpty ||
|
||||
_villeController.text.isNotEmpty ||
|
||||
_codePostalController.text.isNotEmpty ||
|
||||
_paysController.text.isNotEmpty) {
|
||||
final localisation = Localisation(
|
||||
adresse: _adresseController.text,
|
||||
ville: _villeController.text.isEmpty ? null : _villeController.text,
|
||||
codePostal: _codePostalController.text.isEmpty ? null : _codePostalController.text,
|
||||
pays: _paysController.text.isEmpty ? null : _paysController.text,
|
||||
latitude: widget.localisation?.latitude,
|
||||
longitude: widget.localisation?.longitude,
|
||||
);
|
||||
widget.onLocalisationChanged(localisation);
|
||||
} else {
|
||||
widget.onLocalisationChanged(null);
|
||||
}
|
||||
}
|
||||
|
||||
void _utiliserPositionActuelle() {
|
||||
// Implémenter la géolocalisation
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Fonctionnalité de géolocalisation à implémenter'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Section du formulaire pour les documents
|
||||
class DemandeAideFormDocumentsSection extends StatefulWidget {
|
||||
final List<PieceJustificative> piecesJustificatives;
|
||||
final ValueChanged<List<PieceJustificative>> onDocumentsChanged;
|
||||
|
||||
const DemandeAideFormDocumentsSection({
|
||||
super.key,
|
||||
required this.piecesJustificatives,
|
||||
required this.onDocumentsChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
State<DemandeAideFormDocumentsSection> createState() => _DemandeAideFormDocumentsSectionState();
|
||||
}
|
||||
|
||||
class _DemandeAideFormDocumentsSectionState extends State<DemandeAideFormDocumentsSection> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
UnifiedCard(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'Documents justificatifs',
|
||||
style: AppTextStyles.titleMedium.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
TextButton.icon(
|
||||
onPressed: _ajouterDocument,
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Ajouter'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Ajoutez des documents pour appuyer votre demande (optionnel)',
|
||||
style: AppTextStyles.bodySmall.copyWith(
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (widget.piecesJustificatives.isEmpty)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: AppColors.outline),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.upload_file,
|
||||
size: 48,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Aucun document ajouté',
|
||||
style: AppTextStyles.bodyMedium.copyWith(
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Formats acceptés: PDF, DOC, JPG, PNG',
|
||||
style: AppTextStyles.bodySmall.copyWith(
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
...widget.piecesJustificatives.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final document = entry.value;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: _buildDocumentCard(document, index),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDocumentCard(PieceJustificative document, int index) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: AppColors.outline),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.insert_drive_file,
|
||||
color: AppColors.primary,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
document.nomFichier,
|
||||
style: AppTextStyles.bodyMedium.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
document.typeDocument.libelle,
|
||||
style: AppTextStyles.bodySmall.copyWith(
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () => _supprimerDocument(index),
|
||||
icon: const Icon(Icons.delete),
|
||||
iconSize: 20,
|
||||
color: AppColors.error,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _ajouterDocument() {
|
||||
// Implémenter la sélection de fichier
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Fonctionnalité de sélection de fichier à implémenter'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _supprimerDocument(int index) {
|
||||
final nouveauxDocuments = List<PieceJustificative>.from(widget.piecesJustificatives);
|
||||
nouveauxDocuments.removeAt(index);
|
||||
widget.onDocumentsChanged(nouveauxDocuments);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../core/widgets/unified_card.dart';
|
||||
import '../../../../core/theme/app_colors.dart';
|
||||
import '../../../../core/theme/app_text_styles.dart';
|
||||
import '../../../../core/utils/date_formatter.dart';
|
||||
import '../../domain/entities/demande_aide.dart';
|
||||
|
||||
/// Widget de timeline pour afficher l'historique des statuts d'une demande d'aide
|
||||
///
|
||||
/// Ce widget affiche une timeline verticale avec tous les changements de statut
|
||||
/// de la demande d'aide, incluant les dates et les commentaires.
|
||||
class DemandeAideStatusTimeline extends StatelessWidget {
|
||||
final DemandeAide demande;
|
||||
|
||||
const DemandeAideStatusTimeline({
|
||||
super.key,
|
||||
required this.demande,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final historique = _buildHistorique();
|
||||
|
||||
if (historique.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return UnifiedCard(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Historique des statuts',
|
||||
style: AppTextStyles.titleMedium.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
...historique.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final item = entry.value;
|
||||
final isLast = index == historique.length - 1;
|
||||
|
||||
return _buildTimelineItem(
|
||||
item: item,
|
||||
isLast: isLast,
|
||||
isActive: index == 0, // Le premier élément est l'état actuel
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<TimelineItem> _buildHistorique() {
|
||||
final items = <TimelineItem>[];
|
||||
|
||||
// Ajouter l'état actuel
|
||||
items.add(TimelineItem(
|
||||
statut: demande.statut,
|
||||
date: demande.dateModification,
|
||||
commentaire: _getStatutDescription(demande.statut),
|
||||
isActuel: true,
|
||||
));
|
||||
|
||||
// Ajouter l'historique depuis les évaluations
|
||||
for (final evaluation in demande.evaluations) {
|
||||
items.add(TimelineItem(
|
||||
statut: evaluation.decision,
|
||||
date: evaluation.dateEvaluation,
|
||||
commentaire: evaluation.commentaire,
|
||||
evaluateur: evaluation.nomEvaluateur,
|
||||
));
|
||||
}
|
||||
|
||||
// Ajouter la création
|
||||
if (demande.dateCreation != demande.dateModification) {
|
||||
items.add(TimelineItem(
|
||||
statut: StatutAide.brouillon,
|
||||
date: demande.dateCreation,
|
||||
commentaire: 'Demande créée',
|
||||
));
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
Widget _buildTimelineItem({
|
||||
required TimelineItem item,
|
||||
required bool isLast,
|
||||
required bool isActive,
|
||||
}) {
|
||||
final color = isActive ? _getStatutColor(item.statut) : AppColors.textSecondary;
|
||||
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Timeline indicator
|
||||
Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 24,
|
||||
height: 24,
|
||||
decoration: BoxDecoration(
|
||||
color: isActive ? color : AppColors.surface,
|
||||
border: Border.all(
|
||||
color: color,
|
||||
width: isActive ? 3 : 2,
|
||||
),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: isActive
|
||||
? Icon(
|
||||
_getStatutIcon(item.statut),
|
||||
size: 12,
|
||||
color: Colors.white,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
if (!isLast)
|
||||
Container(
|
||||
width: 2,
|
||||
height: 40,
|
||||
color: AppColors.outline,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
// Content
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
item.statut.libelle,
|
||||
style: AppTextStyles.bodyMedium.copyWith(
|
||||
fontWeight: isActive ? FontWeight.bold : FontWeight.w600,
|
||||
color: isActive ? color : AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (item.isActuel)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
'ACTUEL',
|
||||
style: AppTextStyles.labelSmall.copyWith(
|
||||
color: color,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
DateFormatter.formatComplete(item.date),
|
||||
style: AppTextStyles.bodySmall.copyWith(
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
if (item.evaluateur != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.person,
|
||||
size: 16,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'Par ${item.evaluateur}',
|
||||
style: AppTextStyles.bodySmall.copyWith(
|
||||
color: AppColors.textSecondary,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
if (item.commentaire != null && item.commentaire!.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: AppColors.outline),
|
||||
),
|
||||
child: Text(
|
||||
item.commentaire!,
|
||||
style: AppTextStyles.bodySmall,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Color _getStatutColor(StatutAide statut) {
|
||||
switch (statut) {
|
||||
case StatutAide.brouillon:
|
||||
return AppColors.textSecondary;
|
||||
case StatutAide.soumise:
|
||||
return AppColors.warning;
|
||||
case StatutAide.enEvaluation:
|
||||
return AppColors.info;
|
||||
case StatutAide.approuvee:
|
||||
return AppColors.success;
|
||||
case StatutAide.rejetee:
|
||||
return AppColors.error;
|
||||
case StatutAide.enCours:
|
||||
return AppColors.primary;
|
||||
case StatutAide.terminee:
|
||||
return AppColors.success;
|
||||
case StatutAide.versee:
|
||||
return AppColors.success;
|
||||
case StatutAide.livree:
|
||||
return AppColors.success;
|
||||
case StatutAide.annulee:
|
||||
return AppColors.error;
|
||||
}
|
||||
}
|
||||
|
||||
IconData _getStatutIcon(StatutAide statut) {
|
||||
switch (statut) {
|
||||
case StatutAide.brouillon:
|
||||
return Icons.edit;
|
||||
case StatutAide.soumise:
|
||||
return Icons.send;
|
||||
case StatutAide.enEvaluation:
|
||||
return Icons.rate_review;
|
||||
case StatutAide.approuvee:
|
||||
return Icons.check;
|
||||
case StatutAide.rejetee:
|
||||
return Icons.close;
|
||||
case StatutAide.enCours:
|
||||
return Icons.play_arrow;
|
||||
case StatutAide.terminee:
|
||||
return Icons.done_all;
|
||||
case StatutAide.versee:
|
||||
return Icons.payment;
|
||||
case StatutAide.livree:
|
||||
return Icons.local_shipping;
|
||||
case StatutAide.annulee:
|
||||
return Icons.cancel;
|
||||
}
|
||||
}
|
||||
|
||||
String _getStatutDescription(StatutAide statut) {
|
||||
switch (statut) {
|
||||
case StatutAide.brouillon:
|
||||
return 'Demande en cours de rédaction';
|
||||
case StatutAide.soumise:
|
||||
return 'Demande soumise pour évaluation';
|
||||
case StatutAide.enEvaluation:
|
||||
return 'Demande en cours d\'évaluation';
|
||||
case StatutAide.approuvee:
|
||||
return 'Demande approuvée';
|
||||
case StatutAide.rejetee:
|
||||
return 'Demande rejetée';
|
||||
case StatutAide.enCours:
|
||||
return 'Aide en cours de traitement';
|
||||
case StatutAide.terminee:
|
||||
return 'Aide terminée';
|
||||
case StatutAide.versee:
|
||||
return 'Montant versé';
|
||||
case StatutAide.livree:
|
||||
return 'Aide livrée';
|
||||
case StatutAide.annulee:
|
||||
return 'Demande annulée';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Classe pour représenter un élément de la timeline
|
||||
class TimelineItem {
|
||||
final StatutAide statut;
|
||||
final DateTime date;
|
||||
final String? commentaire;
|
||||
final String? evaluateur;
|
||||
final bool isActuel;
|
||||
|
||||
const TimelineItem({
|
||||
required this.statut,
|
||||
required this.date,
|
||||
this.commentaire,
|
||||
this.evaluateur,
|
||||
this.isActuel = false,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,444 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../core/theme/app_colors.dart';
|
||||
import '../../../../core/theme/app_text_styles.dart';
|
||||
import '../../domain/entities/demande_aide.dart';
|
||||
import '../bloc/demandes_aide/demandes_aide_state.dart';
|
||||
|
||||
/// Bottom sheet pour filtrer les demandes d'aide
|
||||
///
|
||||
/// Permet à l'utilisateur de sélectionner différents critères
|
||||
/// de filtrage pour affiner la liste des demandes d'aide.
|
||||
class DemandesAideFilterBottomSheet extends StatefulWidget {
|
||||
final FiltresDemandesAide filtresActuels;
|
||||
final ValueChanged<FiltresDemandesAide> onFiltresChanged;
|
||||
|
||||
const DemandesAideFilterBottomSheet({
|
||||
super.key,
|
||||
required this.filtresActuels,
|
||||
required this.onFiltresChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
State<DemandesAideFilterBottomSheet> createState() => _DemandesAideFilterBottomSheetState();
|
||||
}
|
||||
|
||||
class _DemandesAideFilterBottomSheetState extends State<DemandesAideFilterBottomSheet> {
|
||||
late FiltresDemandesAide _filtres;
|
||||
final TextEditingController _motCleController = TextEditingController();
|
||||
final TextEditingController _montantMinController = TextEditingController();
|
||||
final TextEditingController _montantMaxController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_filtres = widget.filtresActuels;
|
||||
_motCleController.text = _filtres.motCle ?? '';
|
||||
_montantMinController.text = _filtres.montantMin?.toInt().toString() ?? '';
|
||||
_montantMaxController.text = _filtres.montantMax?.toInt().toString() ?? '';
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_motCleController.dispose();
|
||||
_montantMinController.dispose();
|
||||
_montantMaxController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: MediaQuery.of(context).size.height * 0.8,
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildHeader(),
|
||||
const SizedBox(height: 16),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildMotCleSection(),
|
||||
const SizedBox(height: 24),
|
||||
_buildTypeAideSection(),
|
||||
const SizedBox(height: 24),
|
||||
_buildStatutSection(),
|
||||
const SizedBox(height: 24),
|
||||
_buildPrioriteSection(),
|
||||
const SizedBox(height: 24),
|
||||
_buildUrgenteSection(),
|
||||
const SizedBox(height: 24),
|
||||
_buildMontantSection(),
|
||||
const SizedBox(height: 24),
|
||||
_buildDateSection(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildActions(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
return Row(
|
||||
children: [
|
||||
Text(
|
||||
'Filtrer les demandes',
|
||||
style: AppTextStyles.titleLarge.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
icon: const Icon(Icons.close),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMotCleSection() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Recherche par mot-clé',
|
||||
style: AppTextStyles.titleMedium.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: _motCleController,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Titre, description, demandeur...',
|
||||
prefixIcon: Icon(Icons.search),
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_filtres = _filtres.copyWith(motCle: value.isEmpty ? null : value);
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTypeAideSection() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Type d\'aide',
|
||||
style: AppTextStyles.titleMedium.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
_buildFilterChip(
|
||||
label: 'Tous',
|
||||
isSelected: _filtres.typeAide == null,
|
||||
onSelected: () {
|
||||
setState(() {
|
||||
_filtres = _filtres.copyWith(typeAide: null);
|
||||
});
|
||||
},
|
||||
),
|
||||
...TypeAide.values.map((type) => _buildFilterChip(
|
||||
label: type.libelle,
|
||||
isSelected: _filtres.typeAide == type,
|
||||
onSelected: () {
|
||||
setState(() {
|
||||
_filtres = _filtres.copyWith(typeAide: type);
|
||||
});
|
||||
},
|
||||
)),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatutSection() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Statut',
|
||||
style: AppTextStyles.titleMedium.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
_buildFilterChip(
|
||||
label: 'Tous',
|
||||
isSelected: _filtres.statut == null,
|
||||
onSelected: () {
|
||||
setState(() {
|
||||
_filtres = _filtres.copyWith(statut: null);
|
||||
});
|
||||
},
|
||||
),
|
||||
...StatutAide.values.map((statut) => _buildFilterChip(
|
||||
label: statut.libelle,
|
||||
isSelected: _filtres.statut == statut,
|
||||
onSelected: () {
|
||||
setState(() {
|
||||
_filtres = _filtres.copyWith(statut: statut);
|
||||
});
|
||||
},
|
||||
)),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPrioriteSection() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Priorité',
|
||||
style: AppTextStyles.titleMedium.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
_buildFilterChip(
|
||||
label: 'Toutes',
|
||||
isSelected: _filtres.priorite == null,
|
||||
onSelected: () {
|
||||
setState(() {
|
||||
_filtres = _filtres.copyWith(priorite: null);
|
||||
});
|
||||
},
|
||||
),
|
||||
...PrioriteAide.values.map((priorite) => _buildFilterChip(
|
||||
label: priorite.libelle,
|
||||
isSelected: _filtres.priorite == priorite,
|
||||
onSelected: () {
|
||||
setState(() {
|
||||
_filtres = _filtres.copyWith(priorite: priorite);
|
||||
});
|
||||
},
|
||||
)),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUrgenteSection() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Urgence',
|
||||
style: AppTextStyles.titleMedium.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: CheckboxListTile(
|
||||
title: const Text('Demandes urgentes uniquement'),
|
||||
value: _filtres.urgente == true,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_filtres = _filtres.copyWith(urgente: value == true ? true : null);
|
||||
});
|
||||
},
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMontantSection() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Montant demandé (FCFA)',
|
||||
style: AppTextStyles.titleMedium.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _montantMinController,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Minimum',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
onChanged: (value) {
|
||||
final montant = double.tryParse(value);
|
||||
setState(() {
|
||||
_filtres = _filtres.copyWith(montantMin: montant);
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _montantMaxController,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Maximum',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
onChanged: (value) {
|
||||
final montant = double.tryParse(value);
|
||||
setState(() {
|
||||
_filtres = _filtres.copyWith(montantMax: montant);
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDateSection() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Période de création',
|
||||
style: AppTextStyles.titleMedium.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () => _selectDate(context, true),
|
||||
icon: const Icon(Icons.calendar_today),
|
||||
label: Text(
|
||||
_filtres.dateDebutCreation != null
|
||||
? '${_filtres.dateDebutCreation!.day}/${_filtres.dateDebutCreation!.month}/${_filtres.dateDebutCreation!.year}'
|
||||
: 'Date début',
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () => _selectDate(context, false),
|
||||
icon: const Icon(Icons.calendar_today),
|
||||
label: Text(
|
||||
_filtres.dateFinCreation != null
|
||||
? '${_filtres.dateFinCreation!.day}/${_filtres.dateFinCreation!.month}/${_filtres.dateFinCreation!.year}'
|
||||
: 'Date fin',
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFilterChip({
|
||||
required String label,
|
||||
required bool isSelected,
|
||||
required VoidCallback onSelected,
|
||||
}) {
|
||||
return FilterChip(
|
||||
label: Text(label),
|
||||
selected: isSelected,
|
||||
onSelected: (_) => onSelected(),
|
||||
selectedColor: AppColors.primary.withOpacity(0.2),
|
||||
checkmarkColor: AppColors.primary,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActions() {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: _reinitialiserFiltres,
|
||||
child: const Text('Réinitialiser'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: _appliquerFiltres,
|
||||
child: Text('Appliquer (${_filtres.nombreFiltresActifs})'),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _selectDate(BuildContext context, bool isStartDate) async {
|
||||
final DateTime? picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: isStartDate
|
||||
? _filtres.dateDebutCreation ?? DateTime.now()
|
||||
: _filtres.dateFinCreation ?? DateTime.now(),
|
||||
firstDate: DateTime(2020),
|
||||
lastDate: DateTime.now(),
|
||||
);
|
||||
|
||||
if (picked != null) {
|
||||
setState(() {
|
||||
if (isStartDate) {
|
||||
_filtres = _filtres.copyWith(dateDebutCreation: picked);
|
||||
} else {
|
||||
_filtres = _filtres.copyWith(dateFinCreation: picked);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _reinitialiserFiltres() {
|
||||
setState(() {
|
||||
_filtres = const FiltresDemandesAide();
|
||||
_motCleController.clear();
|
||||
_montantMinController.clear();
|
||||
_montantMaxController.clear();
|
||||
});
|
||||
}
|
||||
|
||||
void _appliquerFiltres() {
|
||||
widget.onFiltresChanged(_filtres);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../core/theme/app_colors.dart';
|
||||
import '../../../../core/theme/app_text_styles.dart';
|
||||
import '../bloc/demandes_aide/demandes_aide_event.dart';
|
||||
|
||||
/// Bottom sheet pour trier les demandes d'aide
|
||||
///
|
||||
/// Permet à l'utilisateur de sélectionner un critère de tri
|
||||
/// et l'ordre (croissant/décroissant) pour la liste des demandes.
|
||||
class DemandesAideSortBottomSheet extends StatefulWidget {
|
||||
final TriDemandes? critereActuel;
|
||||
final bool croissantActuel;
|
||||
final Function(TriDemandes critere, bool croissant) onTriChanged;
|
||||
|
||||
const DemandesAideSortBottomSheet({
|
||||
super.key,
|
||||
this.critereActuel,
|
||||
required this.croissantActuel,
|
||||
required this.onTriChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
State<DemandesAideSortBottomSheet> createState() => _DemandesAideSortBottomSheetState();
|
||||
}
|
||||
|
||||
class _DemandesAideSortBottomSheetState extends State<DemandesAideSortBottomSheet> {
|
||||
late TriDemandes? _critereSelectionne;
|
||||
late bool _croissant;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_critereSelectionne = widget.critereActuel;
|
||||
_croissant = widget.croissantActuel;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildHeader(),
|
||||
const SizedBox(height: 16),
|
||||
_buildCriteresList(),
|
||||
const SizedBox(height: 16),
|
||||
_buildOrdreSection(),
|
||||
const SizedBox(height: 24),
|
||||
_buildActions(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
return Row(
|
||||
children: [
|
||||
Text(
|
||||
'Trier les demandes',
|
||||
style: AppTextStyles.titleLarge.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
icon: const Icon(Icons.close),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCriteresList() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Critère de tri',
|
||||
style: AppTextStyles.titleMedium.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
...TriDemandes.values.map((critere) => _buildCritereItem(critere)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCritereItem(TriDemandes critere) {
|
||||
final isSelected = _critereSelectionne == critere;
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||
elevation: isSelected ? 2 : 0,
|
||||
color: isSelected ? AppColors.primary.withOpacity(0.1) : null,
|
||||
child: ListTile(
|
||||
leading: Icon(
|
||||
_getCritereIcon(critere),
|
||||
color: isSelected ? AppColors.primary : AppColors.textSecondary,
|
||||
),
|
||||
title: Text(
|
||||
critere.libelle,
|
||||
style: AppTextStyles.bodyLarge.copyWith(
|
||||
color: isSelected ? AppColors.primary : AppColors.textPrimary,
|
||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
_getCritereDescription(critere),
|
||||
style: AppTextStyles.bodySmall.copyWith(
|
||||
color: isSelected ? AppColors.primary : AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
trailing: isSelected
|
||||
? Icon(
|
||||
Icons.check_circle,
|
||||
color: AppColors.primary,
|
||||
)
|
||||
: null,
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_critereSelectionne = critere;
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOrdreSection() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Ordre de tri',
|
||||
style: AppTextStyles.titleMedium.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Card(
|
||||
elevation: _croissant ? 2 : 0,
|
||||
color: _croissant ? AppColors.primary.withOpacity(0.1) : null,
|
||||
child: ListTile(
|
||||
leading: Icon(
|
||||
Icons.arrow_upward,
|
||||
color: _croissant ? AppColors.primary : AppColors.textSecondary,
|
||||
),
|
||||
title: Text(
|
||||
'Croissant',
|
||||
style: AppTextStyles.bodyMedium.copyWith(
|
||||
color: _croissant ? AppColors.primary : AppColors.textPrimary,
|
||||
fontWeight: _croissant ? FontWeight.w600 : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
_getOrdreDescription(true),
|
||||
style: AppTextStyles.bodySmall.copyWith(
|
||||
color: _croissant ? AppColors.primary : AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
trailing: _croissant
|
||||
? Icon(
|
||||
Icons.check_circle,
|
||||
color: AppColors.primary,
|
||||
)
|
||||
: null,
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_croissant = true;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Card(
|
||||
elevation: !_croissant ? 2 : 0,
|
||||
color: !_croissant ? AppColors.primary.withOpacity(0.1) : null,
|
||||
child: ListTile(
|
||||
leading: Icon(
|
||||
Icons.arrow_downward,
|
||||
color: !_croissant ? AppColors.primary : AppColors.textSecondary,
|
||||
),
|
||||
title: Text(
|
||||
'Décroissant',
|
||||
style: AppTextStyles.bodyMedium.copyWith(
|
||||
color: !_croissant ? AppColors.primary : AppColors.textPrimary,
|
||||
fontWeight: !_croissant ? FontWeight.w600 : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
_getOrdreDescription(false),
|
||||
style: AppTextStyles.bodySmall.copyWith(
|
||||
color: !_croissant ? AppColors.primary : AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
trailing: !_croissant
|
||||
? Icon(
|
||||
Icons.check_circle,
|
||||
color: AppColors.primary,
|
||||
)
|
||||
: null,
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_croissant = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActions() {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: _reinitialiserTri,
|
||||
child: const Text('Réinitialiser'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: _critereSelectionne != null ? _appliquerTri : null,
|
||||
child: const Text('Appliquer'),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
IconData _getCritereIcon(TriDemandes critere) {
|
||||
switch (critere) {
|
||||
case TriDemandes.dateCreation:
|
||||
return Icons.calendar_today;
|
||||
case TriDemandes.dateModification:
|
||||
return Icons.update;
|
||||
case TriDemandes.titre:
|
||||
return Icons.title;
|
||||
case TriDemandes.statut:
|
||||
return Icons.flag;
|
||||
case TriDemandes.priorite:
|
||||
return Icons.priority_high;
|
||||
case TriDemandes.montant:
|
||||
return Icons.attach_money;
|
||||
case TriDemandes.demandeur:
|
||||
return Icons.person;
|
||||
}
|
||||
}
|
||||
|
||||
String _getCritereDescription(TriDemandes critere) {
|
||||
switch (critere) {
|
||||
case TriDemandes.dateCreation:
|
||||
return 'Trier par date de création de la demande';
|
||||
case TriDemandes.dateModification:
|
||||
return 'Trier par date de dernière modification';
|
||||
case TriDemandes.titre:
|
||||
return 'Trier par titre de la demande (alphabétique)';
|
||||
case TriDemandes.statut:
|
||||
return 'Trier par statut de la demande';
|
||||
case TriDemandes.priorite:
|
||||
return 'Trier par niveau de priorité';
|
||||
case TriDemandes.montant:
|
||||
return 'Trier par montant demandé';
|
||||
case TriDemandes.demandeur:
|
||||
return 'Trier par nom du demandeur (alphabétique)';
|
||||
}
|
||||
}
|
||||
|
||||
String _getOrdreDescription(bool croissant) {
|
||||
if (_critereSelectionne == null) return '';
|
||||
|
||||
switch (_critereSelectionne!) {
|
||||
case TriDemandes.dateCreation:
|
||||
case TriDemandes.dateModification:
|
||||
return croissant ? 'Plus ancien en premier' : 'Plus récent en premier';
|
||||
case TriDemandes.titre:
|
||||
case TriDemandes.demandeur:
|
||||
return croissant ? 'A à Z' : 'Z à A';
|
||||
case TriDemandes.statut:
|
||||
return croissant ? 'Brouillon à Terminée' : 'Terminée à Brouillon';
|
||||
case TriDemandes.priorite:
|
||||
return croissant ? 'Basse à Critique' : 'Critique à Basse';
|
||||
case TriDemandes.montant:
|
||||
return croissant ? 'Montant le plus faible' : 'Montant le plus élevé';
|
||||
}
|
||||
}
|
||||
|
||||
void _reinitialiserTri() {
|
||||
setState(() {
|
||||
_critereSelectionne = null;
|
||||
_croissant = true;
|
||||
});
|
||||
}
|
||||
|
||||
void _appliquerTri() {
|
||||
if (_critereSelectionne != null) {
|
||||
widget.onTriChanged(_critereSelectionne!, _croissant);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user