import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:get_it/get_it.dart'; import '../../../authentication/data/models/user_role.dart'; import '../../../authentication/presentation/bloc/auth_bloc.dart'; import '../../data/models/compte_epargne_model.dart'; import '../../data/repositories/transaction_epargne_repository.dart'; import '../../../../shared/design_system/unionflow_design_system.dart'; import '../../../../shared/widgets/core_card.dart'; import '../../../../shared/widgets/info_badge.dart'; import '../widgets/creer_compte_epargne_dialog.dart'; import '../widgets/depot_epargne_dialog.dart'; import '../widgets/retrait_epargne_dialog.dart'; import '../widgets/transfert_epargne_dialog.dart'; import '../widgets/historique_epargne_sheet.dart'; import 'epargne_detail_page.dart'; /// Page listant les comptes épargne — rendu bank-grade : récap, cartes avec actions (Dépôt, Retrait, Transfert, Détail, Historique). class EpargnePage extends StatefulWidget { const EpargnePage({super.key}); @override State createState() => _EpargnePageState(); } class _EpargnePageState extends State { List _comptes = []; bool _loading = true; String? _error; @override void initState() { super.initState(); _loadComptes(); } Future _loadComptes() async { final authState = context.read().state; if (authState is! AuthAuthenticated) { if (!mounted) return; setState(() { _loading = false; _error = 'Non connecté'; }); return; } if (!mounted) return; setState(() { _loading = true; _error = null; }); try { final compteRepo = GetIt.I(); final list = await compteRepo.getMesComptes(); if (!mounted) return; setState(() { _comptes = list; _loading = false; _error = null; }); } catch (e) { if (!mounted) return; setState(() { _comptes = []; _loading = false; _error = 'Erreur: ${e.toString().replaceFirst('Exception: ', '')}'; }); } } void _openDepot(CompteEpargneModel compte) { if (compte.id == null) return; showDialog( context: context, builder: (ctx) => DepotEpargneDialog( compteId: compte.id!, onSuccess: _loadComptes, ), ).then((_) => _loadComptes()); } void _openRetrait(CompteEpargneModel compte) { if (compte.id == null) return; final soldeDispo = (compte.soldeActuel - compte.soldeBloque).clamp(0.0, double.infinity); showDialog( context: context, builder: (ctx) => RetraitEpargneDialog( compteId: compte.id!, numeroCompte: compte.numeroCompte ?? compte.id!, soldeDisponible: soldeDispo, onSuccess: _loadComptes, ), ).then((_) => _loadComptes()); } void _openTransfert(CompteEpargneModel compte) { if (compte.id == null) return; showDialog( context: context, builder: (ctx) => TransfertEpargneDialog( compteSource: compte, tousLesComptes: _comptes, onSuccess: _loadComptes, ), ).then((_) => _loadComptes()); } void _openDetail(CompteEpargneModel compte) { Navigator.of(context).push( MaterialPageRoute( builder: (ctx) => EpargneDetailPage( compte: compte, tousLesComptes: _comptes, onDataChanged: _loadComptes, ), ), ).then((_) => _loadComptes()); } void _openHistorique(CompteEpargneModel compte) { if (compte.id == null) return; showModalBottomSheet( context: context, isScrollControlled: true, useSafeArea: true, builder: (ctx) => HistoriqueEpargneSheet(compte: compte), ); } String? _typeCompteLibelle(String? code) { if (code == null) return null; const map = { 'COURANT': 'Compte courant', 'EPARGNE_LIBRE': 'Épargne libre', 'EPARGNE_BLOQUEE': 'Épargne bloquée', 'DEPOT_A_TERME': 'Dépôt à terme', 'EPARGNE_PROJET': 'Épargne projet', }; return map[code] ?? code; } bool _canCreateCompte(BuildContext context) { final state = context.read().state; if (state is! AuthAuthenticated) return false; final role = state.effectiveRole; return role == UserRole.superAdmin || role == UserRole.orgAdmin; } void _openCreerCompte() { showDialog( context: context, builder: (ctx) => CreerCompteEpargneDialog(onCreated: _loadComptes), ).then((_) => _loadComptes()); } Widget _buildRecapCard() { double total = 0; for (final c in _comptes) { total += (c.soldeActuel - c.soldeBloque).clamp(0.0, double.infinity); } return CoreCard( padding: const EdgeInsets.all(SpacingTokens.lg), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'VUE D\'ENSEMBLE', style: AppTypography.subtitleSmall.copyWith(fontWeight: FontWeight.bold), ), const Icon(Icons.account_balance_wallet, color: AppColors.primary, size: 24), ], ), const SizedBox(height: SpacingTokens.md), Text( '${total.toStringAsFixed(0)} XOF', style: AppTypography.headerSmall.copyWith(fontSize: 24, color: AppColors.primary), ), const SizedBox(height: SpacingTokens.xs), Text( 'Solde disponible total • ${_comptes.length} compte${_comptes.length > 1 ? 's' : ''}', style: AppTypography.bodyTextSmall.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant), ), if (_comptes.any((c) => c.soldeBloque > 0)) ...[ const SizedBox(height: SpacingTokens.xs), Row( children: [ Icon(Icons.shield_outlined, size: 12, color: AppColors.warning), const SizedBox(width: 4), Text( 'Certains fonds sont sous surveillance LCB-FT', style: AppTypography.bodyTextSmall.copyWith(fontSize: 10, color: AppColors.warning), ), ], ), ], ], ), ); } Widget _buildCompteCard(CompteEpargneModel c) { final typeLibelle = _typeCompteLibelle(c.typeCompte); final dateStr = c.dateOuverture != null ? 'Ouvert le ${c.dateOuverture!.day.toString().padLeft(2, '0')}/${c.dateOuverture!.month.toString().padLeft(2, '0')}/${c.dateOuverture!.year}' : null; final soldeDispo = (c.soldeActuel - c.soldeBloque).clamp(0.0, double.infinity); final actif = c.statut == 'ACTIF'; final canTransfert = _comptes.length > 1; return CoreCard( margin: const EdgeInsets.only(bottom: SpacingTokens.md), padding: const EdgeInsets.all(SpacingTokens.md), onTap: () => _openDetail(c), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( c.numeroCompte ?? 'Compte ${c.id ?? ""}', style: AppTypography.actionText, ), if (typeLibelle != null) Text( typeLibelle, style: AppTypography.subtitleSmall.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant), ), if (dateStr != null) Text( dateStr, style: AppTypography.bodyTextSmall.copyWith( fontSize: 10, color: Theme.of(context).brightness == Brightness.dark ? AppColors.textSecondaryDark : AppColors.textSecondary, ), ), ], ), ), if (c.soldeBloque > 0) Padding( padding: const EdgeInsets.only(right: SpacingTokens.xs), child: Tooltip( message: 'Fonds bloqués — surveillance LCB-FT', child: Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3), decoration: BoxDecoration( color: AppColors.warningContainer, borderRadius: BorderRadius.circular(6), border: Border.all(color: AppColors.warning, width: 1), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.shield_outlined, size: 12, color: AppColors.warning), const SizedBox(width: 3), Text('LCB-FT', style: TextStyle(fontSize: 9, fontWeight: FontWeight.bold, color: AppColors.warning)), ], ), ), ), ), if (c.statut != null) InfoBadge( text: c.statut!, backgroundColor: c.statut == 'ACTIF' ? AppColors.success : AppColors.textSecondary, ), ], ), const SizedBox(height: SpacingTokens.md), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('SOLDE ACTUEL', style: AppTypography.subtitleSmall.copyWith(fontSize: 8, fontWeight: FontWeight.bold)), Text( '${c.soldeActuel.toStringAsFixed(0)} XOF', style: AppTypography.headerSmall.copyWith(fontSize: 14, color: AppColors.primary), ), ], ), if (c.soldeBloque > 0) Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text('BLOQUÉ', style: AppTypography.subtitleSmall.copyWith(fontSize: 8, fontWeight: FontWeight.bold)), Text( '${c.soldeBloque.toStringAsFixed(0)} XOF', style: AppTypography.bodyTextSmall.copyWith(fontSize: 12, color: AppColors.error), ), ], ), ], ), if (c.description != null && c.description!.isNotEmpty) ...[ const SizedBox(height: SpacingTokens.sm), Text( c.description!, style: AppTypography.bodyTextSmall.copyWith(fontSize: 11, color: AppColors.textSecondary), maxLines: 2, overflow: TextOverflow.ellipsis, ), ], if (actif) ...[ const SizedBox(height: SpacingTokens.md), const Divider(height: 1), const SizedBox(height: SpacingTokens.sm), Row( children: [ Expanded( child: FilledButton.tonal( onPressed: () => _openDepot(c), style: FilledButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: SpacingTokens.sm), backgroundColor: AppColors.primary.withOpacity(0.1), foregroundColor: AppColors.primary, ), child: const Text('Dépôt', style: TextStyle(fontSize: 12)), ), ), const SizedBox(width: SpacingTokens.xs), Expanded( child: FilledButton.tonal( onPressed: soldeDispo > 0 ? () => _openRetrait(c) : null, style: FilledButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: SpacingTokens.sm), backgroundColor: AppColors.primary.withOpacity(0.1), foregroundColor: AppColors.primary, ), child: const Text('Retrait', style: TextStyle(fontSize: 12)), ), ), if (canTransfert) ...[ const SizedBox(width: SpacingTokens.xs), Expanded( child: FilledButton.tonal( onPressed: soldeDispo > 0 ? () => _openTransfert(c) : null, style: FilledButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: SpacingTokens.sm), backgroundColor: AppColors.primary.withOpacity(0.1), foregroundColor: AppColors.primary, ), child: const Text('Transférer', style: TextStyle(fontSize: 12)), ), ), ], ], ), const SizedBox(height: SpacingTokens.xs), Row( children: [ Expanded( child: OutlinedButton.icon( onPressed: () => _openDetail(c), icon: const Icon(Icons.info_outline, size: 16), label: const Text('Détail', style: TextStyle(fontSize: 12)), style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: SpacingTokens.sm), foregroundColor: Theme.of(context).brightness == Brightness.dark ? AppColors.textPrimaryDark : AppColors.textPrimary, ), ), ), const SizedBox(width: SpacingTokens.xs), Expanded( child: OutlinedButton.icon( onPressed: () => _openHistorique(c), icon: const Icon(Icons.history, size: 16), label: const Text('Historique', style: TextStyle(fontSize: 12)), style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: SpacingTokens.sm), foregroundColor: Theme.of(context).brightness == Brightness.dark ? AppColors.textPrimaryDark : AppColors.textPrimary, ), ), ), ], ), ], ], ), ); } Widget _buildBodyContent() { if (_loading) { return const Center(child: CircularProgressIndicator()); } if (_error != null) { return Center( child: Padding( padding: const EdgeInsets.all(SpacingTokens.lg), child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.error_outline, size: 48, color: AppColors.error), const SizedBox(height: SpacingTokens.md), Text( _error!, style: AppTypography.bodyTextSmall.copyWith(color: AppColors.error), textAlign: TextAlign.center, ), const SizedBox(height: SpacingTokens.lg), FilledButton( onPressed: _loadComptes, child: const Text('Réessayer'), ), ], ), ), ); } if (_comptes.isEmpty) { return Center( child: Padding( padding: const EdgeInsets.symmetric(horizontal: SpacingTokens.xl), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.savings_outlined, size: 64, color: Theme.of(context).colorScheme.onSurfaceVariant), const SizedBox(height: SpacingTokens.lg), Text( 'Aucun compte épargne', style: AppTypography.actionText.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant), textAlign: TextAlign.center, ), const SizedBox(height: SpacingTokens.sm), Text( 'Votre organisation peut ouvrir un compte épargne pour vous. Contactez-la pour en bénéficier.', style: AppTypography.bodyTextSmall.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant), textAlign: TextAlign.center, ), ], ), ), ); } return RefreshIndicator( onRefresh: _loadComptes, child: ListView( padding: const EdgeInsets.all(SpacingTokens.sm), children: [ _buildRecapCard(), const SizedBox(height: SpacingTokens.sm), ..._comptes.map(_buildCompteCard), ], ), ); } @override Widget build(BuildContext context) { final showFab = _canCreateCompte(context); return Scaffold( backgroundColor: Theme.of(context).scaffoldBackgroundColor, appBar: UFAppBar( title: 'Comptes Épargne', moduleGradient: ModuleColors.epargneGradient, ), body: _buildBodyContent(), floatingActionButton: showFab ? FloatingActionButton( onPressed: _openCreerCompte, tooltip: 'Créer un compte épargne pour un membre', backgroundColor: AppColors.primary, foregroundColor: AppColors.onPrimary, child: const Icon(Icons.add), ) : null, ); } }