import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import '../../../../core/utils/logger.dart'; import '../../data/models/compte_epargne_model.dart'; import '../../data/models/transaction_epargne_model.dart'; import '../../data/repositories/transaction_epargne_repository.dart'; // CompteEpargneRepository + TransactionEpargneRepository import '../../../../shared/design_system/tokens/app_colors.dart'; import '../../../../shared/design_system/unionflow_design_system.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'; /// Page détail d'un compte épargne : solde, infos, actions (Dépôt, Retrait, Transfert), dernieres transactions. class EpargneDetailPage extends StatefulWidget { final CompteEpargneModel compte; final List tousLesComptes; final VoidCallback? onDataChanged; const EpargneDetailPage({ super.key, required this.compte, required this.tousLesComptes, this.onDataChanged, }); @override State createState() => _EpargneDetailPageState(); } class _EpargneDetailPageState extends State { List _transactions = []; bool _loadingTx = true; bool _loadingMore = false; bool _hasMore = true; int _page = 0; static const int _pageSize = 20; String? _errorTx; CompteEpargneModel? _compte; // rafraîchi après actions @override void initState() { super.initState(); _compte = widget.compte; _loadTransactions(reset: true); } Future _refreshCompte() async { try { final repo = GetIt.I(); if (_compte?.id != null) { final c = await repo.getById(_compte!.id!); if (c != null && mounted) setState(() => _compte = c); } } catch (e, st) { AppLogger.error('EpargneDetailPage: _refreshCompte échoué', error: e, stackTrace: st); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Impossible de rafraîchir le compte')), ); } } } Future _loadTransactions({bool reset = false}) async { if (_compte?.id == null) { setState(() { _loadingTx = false; _transactions = []; }); return; } if (reset) { setState(() { _loadingTx = true; _errorTx = null; _page = 0; _hasMore = true; }); } else { if (_loadingMore || !_hasMore) return; setState(() => _loadingMore = true); } try { final repo = GetIt.I(); final list = await repo.getByCompte(_compte!.id!, page: _page, size: _pageSize); if (!mounted) return; final parsed = list.map((e) => TransactionEpargneModel.fromJson(e)).toList(); setState(() { if (reset) { _transactions = parsed; } else { _transactions = [..._transactions, ...parsed]; } _hasMore = parsed.length == _pageSize; _page = _page + 1; _loadingTx = false; _loadingMore = false; _errorTx = null; }); } catch (e, st) { AppLogger.error('EpargneDetailPage: _loadTransactions échoué', error: e, stackTrace: st); if (!mounted) return; setState(() { if (reset) _transactions = []; _loadingTx = false; _loadingMore = false; _errorTx = e.toString().replaceFirst('Exception: ', ''); }); } } void _openDepot() { if (_compte?.id == null) return; showDialog( context: context, builder: (ctx) => DepotEpargneDialog( compteId: _compte!.id!, onSuccess: () { _refreshCompte(); _loadTransactions(reset: true); widget.onDataChanged?.call(); }, ), ).then((_) => _refreshCompte()); } void _openRetrait() { 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: () { _refreshCompte(); _loadTransactions(reset: true); widget.onDataChanged?.call(); }, ), ).then((_) => _refreshCompte()); } void _openTransfert() { if (_compte?.id == null) return; showDialog( context: context, builder: (ctx) => TransfertEpargneDialog( compteSource: _compte!, tousLesComptes: widget.tousLesComptes, onSuccess: () { _refreshCompte(); _loadTransactions(reset: true); widget.onDataChanged?.call(); }, ), ).then((_) => _refreshCompte()); } void _openHistorique() { 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; } @override Widget build(BuildContext context) { final c = _compte ?? widget.compte; final soldeDispo = (c.soldeActuel - c.soldeBloque).clamp(0.0, double.infinity); final actif = c.statut == 'ACTIF'; return Scaffold( appBar: AppBar( title: const Text('Détail du compte'), backgroundColor: Colors.transparent, elevation: 0, foregroundColor: ColorTokens.onSurface, actions: [ IconButton( icon: const Icon(Icons.history), onPressed: _transactions.isEmpty ? null : _openHistorique, tooltip: 'Historique', ), ], ), body: Container( width: double.infinity, decoration: const BoxDecoration( color: AppColors.lightBackground, ), child: SafeArea( child: RefreshIndicator( onRefresh: () async { await _refreshCompte(); await _loadTransactions(reset: true); }, child: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.all(SpacingTokens.sm), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Card( elevation: 2, shadowColor: ColorTokens.shadow, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(RadiusTokens.lg)), child: Padding( padding: const EdgeInsets.all(SpacingTokens.lg), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( c.numeroCompte ?? c.id ?? '—', style: TypographyTokens.titleMedium.copyWith( color: ColorTokens.onSurfaceVariant, letterSpacing: 0.5, ), ), if (_typeCompteLibelle(c.typeCompte) != null) Text( _typeCompteLibelle(c.typeCompte)!, style: TypographyTokens.bodySmall.copyWith(color: ColorTokens.onSurfaceVariant), ), const SizedBox(height: SpacingTokens.md), Text( '${c.soldeActuel.toStringAsFixed(0)} XOF', style: TypographyTokens.headlineMedium.copyWith( fontWeight: FontWeight.bold, color: ColorTokens.primary, ), ), if (c.soldeBloque > 0) Text( 'dont ${c.soldeBloque.toStringAsFixed(0)} XOF bloqué(s)', style: TypographyTokens.bodySmall.copyWith(color: ColorTokens.onSurfaceVariant), ), Text( 'Disponible: ${soldeDispo.toStringAsFixed(0)} XOF', style: TypographyTokens.labelMedium.copyWith(color: ColorTokens.primary), ), if (c.dateOuverture != null) Padding( padding: const EdgeInsets.only(top: SpacingTokens.sm), child: Text( 'Ouvert le ${c.dateOuverture!.day.toString().padLeft(2, '0')}/${c.dateOuverture!.month.toString().padLeft(2, '0')}/${c.dateOuverture!.year}', style: TypographyTokens.bodySmall.copyWith(color: ColorTokens.onSurfaceVariant), ), ), if (c.description != null && c.description!.isNotEmpty) Padding( padding: const EdgeInsets.only(top: SpacingTokens.xs), child: Text( c.description!, style: TypographyTokens.bodySmall.copyWith(color: ColorTokens.onSurfaceVariant), maxLines: 2, overflow: TextOverflow.ellipsis, ), ), ], ), ), ), const SizedBox(height: SpacingTokens.lg), if (actif) ...[ Row( children: [ Expanded( child: FilledButton.icon( onPressed: _openDepot, icon: const Icon(Icons.add_circle_outline, size: 20), label: const Text('Dépôt'), ), ), const SizedBox(width: SpacingTokens.sm), Expanded( child: FilledButton.tonalIcon( onPressed: soldeDispo > 0 ? _openRetrait : null, icon: const Icon(Icons.remove_circle_outline, size: 20), label: const Text('Retrait'), ), ), ], ), const SizedBox(height: SpacingTokens.sm), SizedBox( width: double.infinity, child: OutlinedButton.icon( onPressed: widget.tousLesComptes.length > 1 ? _openTransfert : null, icon: const Icon(Icons.swap_horiz, size: 20), label: const Text('Transfert vers un autre compte'), ), ), const SizedBox(height: SpacingTokens.lg), ], Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Dernières opérations', style: TypographyTokens.titleSmall, ), TextButton( onPressed: _openHistorique, child: const Text('Voir tout'), ), ], ), if (_loadingTx) const Padding( padding: EdgeInsets.all(SpacingTokens.xl), child: Center(child: CircularProgressIndicator()), ) else if (_errorTx != null) Padding( padding: const EdgeInsets.all(SpacingTokens.md), child: Column( children: [ Text(_errorTx!, style: TextStyle(color: ColorTokens.error)), const SizedBox(height: 8), FilledButton.tonal( onPressed: _loadTransactions, child: const Text('Réessayer'), ), ], ), ) else if (_transactions.isEmpty) Padding( padding: const EdgeInsets.all(SpacingTokens.lg), child: Text( 'Aucune transaction', style: TypographyTokens.bodySmall.copyWith(color: ColorTokens.onSurfaceVariant), textAlign: TextAlign.center, ), ) else ...[ Card( child: Column( children: _transactions.map((t) { final dateStr = t.dateTransaction != null ? '${t.dateTransaction!.day.toString().padLeft(2, '0')}/${t.dateTransaction!.month.toString().padLeft(2, '0')}/${t.dateTransaction!.year} ${t.dateTransaction!.hour.toString().padLeft(2, '0')}:${t.dateTransaction!.minute.toString().padLeft(2, '0')}' : null; return ListTile( leading: CircleAvatar( backgroundColor: t.isCredit ? ColorTokens.success.withOpacity(0.2) : ColorTokens.error.withOpacity(0.2), child: Icon( t.isCredit ? Icons.arrow_downward : Icons.arrow_upward, color: t.isCredit ? ColorTokens.success : ColorTokens.error, size: 20, ), ), title: Text(_libelleType(t.type), style: TypographyTokens.bodyMedium), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (dateStr != null) Text(dateStr, style: TypographyTokens.bodySmall.copyWith(color: ColorTokens.onSurfaceVariant)), Text( 'Avant: ${t.soldeAvant.toStringAsFixed(0)} → Après: ${t.soldeApres.toStringAsFixed(0)} XOF', style: TypographyTokens.bodySmall.copyWith(color: ColorTokens.onSurfaceVariant), ), if (t.motif != null && t.motif!.isNotEmpty) Text( t.motif!, style: TypographyTokens.bodySmall.copyWith( color: ColorTokens.onSurfaceVariant, fontStyle: FontStyle.italic, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), trailing: Text( '${t.isCredit ? '+' : '-'}${t.montant.toStringAsFixed(0)} XOF', style: TypographyTokens.titleSmall.copyWith( color: t.isCredit ? ColorTokens.success : ColorTokens.error, fontWeight: FontWeight.w600, ), ), isThreeLine: t.motif != null && t.motif!.isNotEmpty, ); }).toList(), ), ), if (_hasMore) Padding( padding: const EdgeInsets.symmetric(vertical: SpacingTokens.sm), child: _loadingMore ? const Center(child: CircularProgressIndicator()) : TextButton( onPressed: () => _loadTransactions(), child: const Text('Charger plus'), ), ), ], ], ), ), ), ), ), ); } String _libelleType(String? type) { if (type == null) return '—'; const map = { 'DEPOT': 'Dépôt', 'RETRAIT': 'Retrait', 'TRANSFERT_ENTRANT': 'Virement reçu', 'TRANSFERT_SORTANT': 'Virement envoyé', }; return map[type] ?? type; } }