432 lines
12 KiB
Dart
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);
|
|
}
|
|
}
|