677 lines
21 KiB
Dart
677 lines
21 KiB
Dart
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')),
|
|
);
|
|
}
|
|
}
|