Refactoring

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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