import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:intl/intl.dart'; import '../../../../shared/design_system/unionflow_design_system.dart'; import '../../../../shared/design_system/tokens/app_typography.dart'; import '../../../../shared/widgets/info_badge.dart'; import '../../../../shared/widgets/loading_widget.dart'; import '../../../../shared/widgets/error_widget.dart'; import 'package:unionflow_mobile_apps/features/contributions/bloc/contributions_bloc.dart'; import 'package:unionflow_mobile_apps/features/contributions/bloc/contributions_event.dart'; import 'package:unionflow_mobile_apps/features/contributions/bloc/contributions_state.dart'; import 'package:unionflow_mobile_apps/features/contributions/data/models/contribution_model.dart'; import 'package:unionflow_mobile_apps/features/contributions/presentation/widgets/payment_dialog.dart'; import 'package:unionflow_mobile_apps/features/contributions/presentation/widgets/create_contribution_dialog.dart'; import 'package:unionflow_mobile_apps/features/contributions/presentation/pages/mes_statistiques_cotisations_page.dart'; /// Page de gestion des contributions - Version Design System class ContributionsPage extends StatefulWidget { const ContributionsPage({super.key}); @override State createState() => _ContributionsPageState(); } class _ContributionsPageState extends State with SingleTickerProviderStateMixin { late TabController _tabController; final _currencyFormat = NumberFormat.currency(locale: 'fr_FR', symbol: 'FCFA', decimalDigits: 0); @override void initState() { super.initState(); _tabController = TabController(length: 4, vsync: this); } @override void dispose() { _tabController.dispose(); super.dispose(); } void _loadContributions() { final currentTab = _tabController.index; switch (currentTab) { case 0: context.read().add(const LoadContributions()); break; case 1: context.read().add(const LoadContributionsPayees()); break; case 2: context.read().add(const LoadContributionsNonPayees()); break; case 3: context.read().add(const LoadContributionsEnRetard()); break; } } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: ColorTokens.background, appBar: UFAppBar( title: 'Cotisations', actions: [ IconButton( icon: const Icon(Icons.bar_chart, size: 20), onPressed: () => _showStats(), ), IconButton( icon: const Icon(Icons.add_circle_outline, size: 20), onPressed: () => _showCreateDialog(), ), ], bottom: TabBar( controller: _tabController, onTap: (_) => _loadContributions(), labelColor: ColorTokens.onPrimary, unselectedLabelColor: ColorTokens.onPrimary.withOpacity(0.7), indicatorColor: ColorTokens.onPrimary, labelStyle: AppTypography.badgeText.copyWith(fontWeight: FontWeight.bold), tabs: const [ Tab(text: 'Toutes'), Tab(text: 'Payées'), Tab(text: 'Dues'), Tab(text: 'Retard'), ], ), ), body: TabBarView( controller: _tabController, children: List.generate(4, (_) => _buildContributionsList()), ), ); } Widget _buildContributionsList() { return BlocBuilder( builder: (context, state) { if (state is ContributionsLoading) { return const Center(child: AppLoadingWidget()); } if (state is ContributionsError) { return Center( child: AppErrorWidget( message: state.message, onRetry: _loadContributions, ), ); } if (state is ContributionsLoaded) { return _buildListOrEmpty(state.contributions); } // Au retour de "Mes Statistiques", la liste peut être conservée dans ContributionsStatsLoaded if (state is ContributionsStatsLoaded) { if (state.contributions != null) { return _buildListOrEmpty(state.contributions!); } // Stats ouverts sans liste préalable : charger les contributions une fois WidgetsBinding.instance.addPostFrameCallback((_) { if (context.mounted) { context.read().add(const LoadContributions()); } }); return const Center(child: Text('Initialisation...')); } return const Center(child: Text('Initialisation...')); }, ); } Widget _buildListOrEmpty(List contributions) { if (contributions.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.payment_outlined, size: 48, color: ColorTokens.onSurfaceVariant.withOpacity(0.5)), const SizedBox(height: SpacingTokens.md), Text('Aucune contribution', style: AppTypography.bodyTextSmall), ], ), ); } return Column( children: [ _buildMiniStats(contributions), Expanded( child: RefreshIndicator( onRefresh: () async => _loadContributions(), child: ListView.builder( padding: const EdgeInsets.all(SpacingTokens.md), itemCount: contributions.length, itemBuilder: (context, index) => _buildContributionCard(contributions[index]), ), ), ), ], ); } Widget _buildMiniStats(List contributions) { final totalDue = contributions.fold(0.0, (sum, c) => sum + c.montant); final totalPaid = contributions.fold(0.0, (sum, c) => sum + (c.montantPaye ?? 0.0)); return Container( padding: const EdgeInsets.symmetric(horizontal: SpacingTokens.md, vertical: SpacingTokens.sm), color: ColorTokens.surfaceVariant.withOpacity(0.3), child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _buildMetric('DU', _currencyFormat.format(totalDue), ColorTokens.secondary), _buildMetric('PAYÉ', _currencyFormat.format(totalPaid), ColorTokens.success), _buildMetric('RESTANT', _currencyFormat.format(totalDue - totalPaid), ColorTokens.error), ], ), ); } Widget _buildMetric(String label, String value, Color color) { return Column( children: [ Text(label, style: AppTypography.badgeText.copyWith(color: ColorTokens.onSurfaceVariant)), Text(value, style: AppTypography.headerSmall.copyWith(color: color, fontWeight: FontWeight.bold)), ], ); } Widget _buildContributionCard(ContributionModel contribution) { return UFCard( margin: const EdgeInsets.only(bottom: SpacingTokens.sm), onTap: () => _showContributionDetails(contribution), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(contribution.membreNomComplet, style: AppTypography.headerSmall), Text(contribution.libellePeriode, style: AppTypography.subtitleSmall), ], ), ), _buildStatutBadge(contribution.statut, contribution.estEnRetard), ], ), const SizedBox(height: SpacingTokens.md), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ _buildAmountValue('Montant', contribution.montant), if (contribution.montantPaye != null && contribution.montantPaye! > 0) _buildAmountValue('Payé', contribution.montantPaye!, color: ColorTokens.success), _buildAmountValue('Échéance', contribution.dateEcheance, isDate: true), ], ), if (contribution.statut == ContributionStatus.partielle) ...[ const SizedBox(height: SpacingTokens.sm), ClipRRect( borderRadius: BorderRadius.circular(RadiusTokens.sm), child: LinearProgressIndicator( value: contribution.pourcentagePaye / 100, backgroundColor: ColorTokens.surfaceVariant, valueColor: const AlwaysStoppedAnimation(ColorTokens.primary), minHeight: 4, ), ), ], ], ), ); } Widget _buildAmountValue(String label, dynamic value, {Color? color, bool isDate = false}) { String displayValue = isDate ? DateFormat('dd/MM/yy').format(value as DateTime) : _currencyFormat.format(value as double); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: AppTypography.badgeText.copyWith(color: ColorTokens.onSurfaceVariant)), Text(displayValue, style: AppTypography.bodyTextSmall.copyWith( color: color ?? ColorTokens.onSurface, fontWeight: FontWeight.w600, )), ], ); } Widget _buildStatutBadge(ContributionStatus statut, bool enRetard) { if (enRetard && statut != ContributionStatus.payee) { return const InfoBadge(text: 'RETARD', backgroundColor: Color(0xFFFFEBEB), textColor: ColorTokens.error); } switch (statut) { case ContributionStatus.payee: return const InfoBadge(text: 'PAYÉE', backgroundColor: Color(0xFFE3F9E5), textColor: ColorTokens.success); case ContributionStatus.nonPayee: case ContributionStatus.enAttente: return const InfoBadge(text: 'DUE', backgroundColor: Color(0xFFFFF4E5), textColor: ColorTokens.warning); case ContributionStatus.partielle: return const InfoBadge(text: 'PARTIELLE', backgroundColor: Color(0xFFE5F1FF), textColor: ColorTokens.info); case ContributionStatus.annulee: return InfoBadge.neutral('ANNULÉE'); default: return InfoBadge.neutral(statut.name.toUpperCase()); } } void _showContributionDetails(ContributionModel contribution) { showModalBottomSheet( context: context, backgroundColor: ColorTokens.surface, shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(RadiusTokens.lg))), builder: (context) => Padding( padding: const EdgeInsets.all(SpacingTokens.xl), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(contribution.membreNomComplet, style: AppTypography.headerSmall), Text(contribution.libellePeriode, style: AppTypography.subtitleSmall), const Divider(height: SpacingTokens.xl), _buildDetailRow('Montant Total', _currencyFormat.format(contribution.montant)), _buildDetailRow('Montant Payé', _currencyFormat.format(contribution.montantPaye ?? 0.0)), _buildDetailRow('Reste à payer', _currencyFormat.format(contribution.montantRestant), isCritical: contribution.montantRestant > 0), _buildDetailRow('Date d\'échéance', DateFormat('dd MMMM yyyy').format(contribution.dateEcheance)), if (contribution.description != null) ...[ const SizedBox(height: SpacingTokens.md), Text(contribution.description!, style: AppTypography.bodyTextSmall), ], const SizedBox(height: SpacingTokens.xl), Row( children: [ if (contribution.statut != ContributionStatus.payee) Expanded( child: Padding( padding: const EdgeInsets.only(right: 8), child: UFPrimaryButton( label: 'Enregistrer Paiement', onPressed: () { Navigator.pop(context); _showPaymentDialog(contribution); }, ), ), ), Expanded( child: OutlinedButton( onPressed: () => Navigator.pop(context), style: OutlinedButton.styleFrom( side: const BorderSide(color: ColorTokens.outline), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(RadiusTokens.md)), ), child: Text('Fermer', style: AppTypography.actionText.copyWith(color: ColorTokens.onSurface)), ), ), ], ), ], ), ), ); } Widget _buildDetailRow(String label, String value, {bool isCritical = false}) { return Padding( padding: const EdgeInsets.symmetric(vertical: SpacingTokens.xs), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(label, style: AppTypography.bodyTextSmall.copyWith(color: ColorTokens.onSurfaceVariant)), Text(value, style: AppTypography.bodyTextSmall.copyWith( fontWeight: FontWeight.bold, color: isCritical ? ColorTokens.error : ColorTokens.onSurface, )), ], ), ); } void _showPaymentDialog(ContributionModel contribution) { final contributionsBloc = context.read(); showDialog( context: context, builder: (context) => BlocProvider.value( value: contributionsBloc, child: PaymentDialog(cotisation: contribution), ), ); } void _showCreateDialog() { final contributionsBloc = context.read(); showDialog( context: context, builder: (context) => BlocProvider.value( value: contributionsBloc, child: const CreateContributionDialog(), ), ); } void _showStats() { final contributionsBloc = context.read(); Navigator.of(context).push( MaterialPageRoute( builder: (_) => BlocProvider.value( value: contributionsBloc, child: const MesStatistiquesCotisationsPage(), ), ), ); } }