import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import 'package:url_launcher/url_launcher.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/transaction_epargne_request.dart'; import '../../data/repositories/transaction_epargne_repository.dart'; /// Dialogue de dépôt sur un compte épargne. /// Deux modes : enregistrement manuel (LCB-FT) ou paiement via Wave (mobile money, même flux que cotisations). class DepotEpargneDialog extends StatefulWidget { final String compteId; final VoidCallback? onSuccess; const DepotEpargneDialog({ super.key, required this.compteId, this.onSuccess, }); @override State createState() => _DepotEpargneDialogState(); } enum _DepotMode { manual, wave } class _DepotEpargneDialogState extends State { final _formKey = GlobalKey(); final _montantController = TextEditingController(); final _motifController = TextEditingController(); final _origineFondsController = TextEditingController(); final _wavePhoneController = TextEditingController(); bool _loading = false; bool _waveLoading = false; _DepotMode _mode = _DepotMode.manual; late TransactionEpargneRepository _repository; late ParametresLcbFtRepository _parametresRepository; /// Seuil LCB-FT récupéré depuis l'API (fallback à 500k XOF). double _seuilLcbFt = kSeuilOrigineFondsObligatoireXOF; bool _seuilLoaded = false; @override void initState() { super.initState(); _repository = GetIt.I(); _parametresRepository = GetIt.I(); _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; }); } } bool get _origineFondsRequis { final m = double.tryParse(_montantController.text.replaceAll(',', '.')); return m != null && m >= _seuilLcbFt; } @override void dispose() { _montantController.dispose(); _motifController.dispose(); _origineFondsController.dispose(); _wavePhoneController.dispose(); super.dispose(); } Future _submitWave() async { final montant = double.tryParse(_montantController.text.replaceAll(',', '.')); if (montant == null || montant <= 0) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Indiquez un montant valide')), ); return; } final phone = _wavePhoneController.text.replaceAll(RegExp(r'\D'), ''); if (phone.length < 9) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Indiquez votre numéro Wave (9 chiffres)')), ); return; } setState(() => _waveLoading = true); try { final result = await _repository.initierDepotEpargneEnLigne( compteId: widget.compteId, montant: montant, numeroTelephone: phone, ); final url = result.waveLaunchUrl.isNotEmpty ? result.waveLaunchUrl : result.redirectUrl; if (url.isEmpty) throw Exception('URL Wave non reçue'); final uri = Uri.parse(url); if (await canLaunchUrl(uri)) { await launchUrl(uri, mode: LaunchMode.externalApplication); } else { await launchUrl(uri); } if (!mounted) return; Navigator.of(context).pop(true); widget.onSuccess?.call(); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(result.message)), ); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(ErrorFormatter.format(e)), duration: ErrorFormatter.isLcbFtError(e) ? const Duration(seconds: 6) : const Duration(seconds: 3), ), ); } finally { if (mounted) setState(() => _waveLoading = false); } } Future _submit() async { if (!_formKey.currentState!.validate()) return; final montant = double.tryParse(_montantController.text.replaceAll(',', '.')); if (montant == null || montant <= 0) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Montant invalide')), ); 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.compteId, typeTransaction: 'DEPOT', montant: montant, motif: _motifController.text.trim().isEmpty ? null : _motifController.text.trim(), origineFonds: _origineFondsController.text.trim().isEmpty ? null : _origineFondsController.text.trim(), ); await _repository.executer(request); if (!mounted) return; Navigator.of(context).pop(true); widget.onSuccess?.call(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Dépôt enregistré')), ); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(ErrorFormatter.format(e)), duration: ErrorFormatter.isLcbFtError(e) ? const Duration(seconds: 6) : const Duration(seconds: 3), ), ); } finally { if (mounted) setState(() => _loading = false); } } @override Widget build(BuildContext context) { return AlertDialog( title: const Text('Dépôt sur compte épargne'), content: Form( key: _formKey, child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ SegmentedButton<_DepotMode>( segments: const [ ButtonSegment(value: _DepotMode.manual, label: Text('Manuel'), icon: Icon(Icons.edit_note)), ButtonSegment(value: _DepotMode.wave, label: Text('Wave'), icon: Icon(Icons.phone_android)), ], selected: {_mode}, onSelectionChanged: (s) => setState(() => _mode = s.first), ), const SizedBox(height: 16), TextFormField( controller: _montantController, decoration: const InputDecoration( labelText: 'Montant (XOF)', border: OutlineInputBorder(), ), keyboardType: const TextInputType.numberWithOptions(decimal: true), validator: _mode == _DepotMode.manual ? (v) { if (v == null || v.isEmpty) return 'Obligatoire'; final n = double.tryParse(v.replaceAll(',', '.')); if (n == null || n <= 0) return 'Montant invalide'; return null; } : null, onChanged: (_) => setState(() {}), ), if (_mode == _DepotMode.wave) ...[ const SizedBox(height: 16), TextFormField( controller: _wavePhoneController, decoration: const InputDecoration( labelText: 'Numéro Wave (9 chiffres) *', hintText: 'Ex: 771234567', border: OutlineInputBorder(), ), keyboardType: TextInputType.phone, maxLength: 12, ), ] else ...[ 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.0), child: Text( 'Requis pour les opérations ≥ ${_seuilLcbFt.toStringAsFixed(0)} XOF', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.primary, ), ), ), ], ], ), ), ), actions: [ TextButton( onPressed: (_loading || _waveLoading) ? null : () => Navigator.of(context).pop(), child: const Text('Annuler'), ), if (_mode == _DepotMode.wave) FilledButton( onPressed: _waveLoading ? null : _submitWave, child: _waveLoading ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2)) : const Text('Ouvrir Wave'), ) else FilledButton( onPressed: _loading ? null : _submit, child: _loading ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2)) : const Text('Enregistrer'), ), ], ); } }