import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import '../../../../core/constants/lcb_ft_constants.dart'; import '../../../../core/data/repositories/parametres_lcb_ft_repository.dart'; import '../../../../core/utils/error_formatter.dart'; import '../../data/models/compte_epargne_model.dart'; import '../../data/models/transaction_epargne_request.dart'; import '../../data/repositories/transaction_epargne_repository.dart'; import '../../../../shared/design_system/unionflow_design_system.dart'; /// Dialogue de transfert entre deux comptes épargne du membre. class TransfertEpargneDialog extends StatefulWidget { final CompteEpargneModel compteSource; final List tousLesComptes; final VoidCallback? onSuccess; const TransfertEpargneDialog({ super.key, required this.compteSource, required this.tousLesComptes, this.onSuccess, }); @override State createState() => _TransfertEpargneDialogState(); } class _TransfertEpargneDialogState extends State { final _formKey = GlobalKey(); final _montantController = TextEditingController(); final _motifController = TextEditingController(); final _origineFondsController = TextEditingController(); bool _loading = false; String? _compteDestinationId; late TransactionEpargneRepository _repository; late ParametresLcbFtRepository _parametresRepository; /// Seuil LCB-FT récupéré depuis l'API (fallback à 500k XOF). double _seuilLcbFt = kSeuilOrigineFondsObligatoireXOF; bool _seuilLoaded = false; List get _comptesDestination { if (widget.compteSource.id == null) return []; return widget.tousLesComptes .where((c) => c.id != null && c.id != widget.compteSource.id && c.statut == 'ACTIF') .toList(); } bool get _origineFondsRequis { final m = double.tryParse(_montantController.text.replaceAll(',', '.')); return m != null && m >= _seuilLcbFt; } @override void initState() { super.initState(); _repository = GetIt.I(); _parametresRepository = GetIt.I(); if (_comptesDestination.isNotEmpty) _compteDestinationId = _comptesDestination.first.id; _chargerSeuil(); } /// Charge le seuil LCB-FT depuis l'API au chargement du dialog. Future _chargerSeuil() async { final seuil = await _parametresRepository.getSeuilJustification(); if (mounted) { setState(() { _seuilLcbFt = seuil.montantSeuil; _seuilLoaded = true; }); } } @override void dispose() { _montantController.dispose(); _motifController.dispose(); _origineFondsController.dispose(); super.dispose(); } Future _submit() async { if (!_formKey.currentState!.validate()) return; if (_compteDestinationId == null || _compteDestinationId!.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Sélectionnez un compte de destination')), ); return; } final montant = double.tryParse(_montantController.text.replaceAll(',', '.')); if (montant == null || montant <= 0) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Montant invalide'))); return; } final soldeDispo = widget.compteSource.soldeActuel - widget.compteSource.soldeBloque; if (montant > soldeDispo) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Solde disponible insuffisant (${soldeDispo.toStringAsFixed(0)} XOF)')), ); return; } if (_origineFondsRequis && _origineFondsController.text.trim().isEmpty) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( 'L\'origine des fonds est obligatoire pour les opérations à partir de ${_seuilLcbFt.toStringAsFixed(0)} XOF (LCB-FT).', ), ), ); return; } setState(() => _loading = true); try { final request = TransactionEpargneRequest( compteId: widget.compteSource.id!, typeTransaction: 'TRANSFERT_SORTANT', montant: montant, compteDestinationId: _compteDestinationId, motif: _motifController.text.trim().isEmpty ? null : _motifController.text.trim(), origineFonds: _origineFondsController.text.trim().isEmpty ? null : _origineFondsController.text.trim(), ); await _repository.transferer(request); if (!mounted) return; Navigator.of(context).pop(true); widget.onSuccess?.call(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Transfert effectué'), backgroundColor: ColorTokens.success), ); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(ErrorFormatter.format(e)), backgroundColor: ColorTokens.error, duration: ErrorFormatter.isLcbFtError(e) ? const Duration(seconds: 6) : const Duration(seconds: 3), ), ); } finally { if (mounted) setState(() => _loading = false); } } @override Widget build(BuildContext context) { final destinations = _comptesDestination; if (destinations.isEmpty) { return AlertDialog( title: const Text('Transfert'), content: const Text( 'Vous n\'avez pas d\'autre compte épargne actif pour effectuer un transfert.', ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Fermer'), ), ], ); } return AlertDialog( title: const Text('Transfert entre comptes'), content: Form( key: _formKey, child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'De: ${widget.compteSource.numeroCompte ?? widget.compteSource.id}', style: TypographyTokens.bodySmall?.copyWith(color: ColorTokens.onSurfaceVariant), ), Text( 'Solde disponible: ${(widget.compteSource.soldeActuel - widget.compteSource.soldeBloque).toStringAsFixed(0)} XOF', style: TypographyTokens.titleSmall, ), const SizedBox(height: 16), DropdownButtonFormField( value: _compteDestinationId, isExpanded: true, decoration: const InputDecoration( labelText: 'Compte de destination', border: OutlineInputBorder(), ), items: destinations .map((c) => DropdownMenuItem( value: c.id, child: Text( '${c.numeroCompte ?? c.id} — ${c.soldeActuel.toStringAsFixed(0)} XOF', overflow: TextOverflow.ellipsis, maxLines: 1, ), )) .toList(), onChanged: (v) => setState(() => _compteDestinationId = v), ), const SizedBox(height: 16), TextFormField( controller: _montantController, decoration: const InputDecoration( labelText: 'Montant (XOF)', border: OutlineInputBorder(), ), keyboardType: const TextInputType.numberWithOptions(decimal: true), validator: (v) { if (v == null || v.isEmpty) return 'Obligatoire'; final n = double.tryParse(v.replaceAll(',', '.')); if (n == null || n <= 0) return 'Montant invalide'; final solde = widget.compteSource.soldeActuel - widget.compteSource.soldeBloque; if (n > solde) return 'Solde insuffisant'; return null; }, onChanged: (_) => setState(() {}), ), const SizedBox(height: 16), TextFormField( controller: _motifController, decoration: const InputDecoration( labelText: 'Motif (optionnel)', border: OutlineInputBorder(), ), maxLines: 2, ), const SizedBox(height: 16), TextFormField( controller: _origineFondsController, decoration: InputDecoration( labelText: 'Origine des fonds (LCB-FT)', hintText: _origineFondsRequis ? 'Obligatoire au-dessus du seuil' : 'Optionnel', border: const OutlineInputBorder(), ), onChanged: (_) => setState(() {}), ), if (_origineFondsRequis) Padding( padding: const EdgeInsets.only(top: 8), child: Text( 'Requis pour les opérations ≥ ${_seuilLcbFt.toStringAsFixed(0)} XOF', style: TypographyTokens.bodySmall?.copyWith(color: ColorTokens.primary), ), ), ], ), ), ), actions: [ TextButton( onPressed: _loading ? null : () => Navigator.of(context).pop(), child: const Text('Annuler'), ), FilledButton( onPressed: _loading ? null : _submit, child: _loading ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2)) : const Text('Transférer'), ), ], ); } }