Files
unionflow-client-quarkus-pr…/unionflow-mobile-apps/lib/features/members/presentation/widgets/membre_cotisations_section.dart
2025-09-13 19:05:06 +00:00

432 lines
12 KiB
Dart

import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../../../../core/models/membre_model.dart';
import '../../../../core/models/cotisation_model.dart';
import '../../../../shared/theme/app_theme.dart';
/// Section des cotisations d'un membre
class MembreCotisationsSection extends StatelessWidget {
const MembreCotisationsSection({
super.key,
required this.membre,
required this.cotisations,
required this.isLoading,
this.onRefresh,
});
final MembreModel membre;
final List<CotisationModel> cotisations;
final bool isLoading;
final VoidCallback? onRefresh;
@override
Widget build(BuildContext context) {
if (isLoading) {
return const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Chargement des cotisations...'),
],
),
);
}
return RefreshIndicator(
onRefresh: () async {
onRefresh?.call();
},
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSummaryCard(),
const SizedBox(height: 16),
_buildCotisationsList(),
],
),
),
);
}
Widget _buildSummaryCard() {
final totalDu = cotisations.fold<double>(
0,
(sum, cotisation) => sum + cotisation.montantDu,
);
final totalPaye = cotisations.fold<double>(
0,
(sum, cotisation) => sum + cotisation.montantPaye,
);
final totalRestant = totalDu - totalPaye;
final cotisationsPayees = cotisations.where((c) => c.statut == 'PAYEE').length;
final cotisationsEnRetard = cotisations.where((c) => c.isEnRetard).length;
return Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.account_balance_wallet,
color: AppTheme.primaryColor,
size: 24,
),
const SizedBox(width: 8),
const Text(
'Résumé des cotisations',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
],
),
const SizedBox(height: 20),
Row(
children: [
Expanded(
child: _buildSummaryItem(
'Total dû',
_formatAmount(totalDu),
AppTheme.infoColor,
Icons.receipt_long,
),
),
Expanded(
child: _buildSummaryItem(
'Payé',
_formatAmount(totalPaye),
AppTheme.successColor,
Icons.check_circle,
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildSummaryItem(
'Restant',
_formatAmount(totalRestant),
totalRestant > 0 ? AppTheme.warningColor : AppTheme.successColor,
Icons.pending,
),
),
Expanded(
child: _buildSummaryItem(
'En retard',
'$cotisationsEnRetard',
cotisationsEnRetard > 0 ? AppTheme.errorColor : AppTheme.successColor,
Icons.warning,
),
),
],
),
const SizedBox(height: 16),
LinearProgressIndicator(
value: totalDu > 0 ? totalPaye / totalDu : 0,
backgroundColor: AppTheme.backgroundLight,
valueColor: AlwaysStoppedAnimation<Color>(
totalPaye == totalDu ? AppTheme.successColor : AppTheme.primaryColor,
),
),
const SizedBox(height: 8),
Text(
'$cotisationsPayees/${cotisations.length} cotisations payées',
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
),
],
),
),
);
}
Widget _buildSummaryItem(String label, String value, Color color, IconData icon) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: color.withOpacity(0.3)),
),
child: Column(
children: [
Icon(icon, color: color, size: 20),
const SizedBox(height: 4),
Text(
value,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: color,
),
),
Text(
label,
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
),
],
),
);
}
Widget _buildCotisationsList() {
if (cotisations.isEmpty) {
return Card(
elevation: 1,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
children: [
Icon(
Icons.receipt_long_outlined,
size: 48,
color: AppTheme.textHint,
),
const SizedBox(height: 16),
const Text(
'Aucune cotisation',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 8),
const Text(
'Ce membre n\'a pas encore de cotisations enregistrées.',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
color: AppTheme.textSecondary,
),
),
],
),
),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.list_alt,
color: AppTheme.primaryColor,
size: 20,
),
const SizedBox(width: 8),
const Text(
'Historique des cotisations',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
],
),
const SizedBox(height: 12),
...cotisations.map((cotisation) => _buildCotisationCard(cotisation)),
],
);
}
Widget _buildCotisationCard(CotisationModel cotisation) {
return Card(
elevation: 1,
margin: const EdgeInsets.only(bottom: 8),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
cotisation.periode ?? 'Période non définie',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 4),
Text(
cotisation.typeCotisation,
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
),
],
),
),
_buildStatusBadge(cotisation),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildCotisationDetail(
'Montant dû',
_formatAmount(cotisation.montantDu),
Icons.receipt,
),
),
Expanded(
child: _buildCotisationDetail(
'Montant payé',
_formatAmount(cotisation.montantPaye),
Icons.payment,
),
),
],
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: _buildCotisationDetail(
'Échéance',
DateFormat('dd/MM/yyyy').format(cotisation.dateEcheance),
Icons.schedule,
),
),
if (cotisation.datePaiement != null)
Expanded(
child: _buildCotisationDetail(
'Payé le',
DateFormat('dd/MM/yyyy').format(cotisation.datePaiement!),
Icons.check_circle,
),
),
],
),
],
),
),
);
}
Widget _buildStatusBadge(CotisationModel cotisation) {
Color color;
String label;
switch (cotisation.statut) {
case 'PAYEE':
color = AppTheme.successColor;
label = 'Payée';
break;
case 'EN_ATTENTE':
color = AppTheme.warningColor;
label = 'En attente';
break;
case 'EN_RETARD':
color = AppTheme.errorColor;
label = 'En retard';
break;
case 'PARTIELLEMENT_PAYEE':
color = AppTheme.infoColor;
label = 'Partielle';
break;
default:
color = AppTheme.textSecondary;
label = cotisation.statut;
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: color.withOpacity(0.3)),
),
child: Text(
label,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: color,
),
),
);
}
Widget _buildCotisationDetail(String label, String value, IconData icon) {
return Row(
children: [
Icon(icon, size: 14, color: AppTheme.textSecondary),
const SizedBox(width: 4),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(
fontSize: 10,
color: AppTheme.textSecondary,
),
),
Text(
value,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppTheme.textPrimary,
),
),
],
),
),
],
);
}
String _formatAmount(double amount) {
return NumberFormat.currency(
locale: 'fr_FR',
symbol: 'FCFA',
decimalDigits: 0,
).format(amount);
}
}