import 'dart:io'; import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:file_picker/file_picker.dart'; import '../../../../core/config/environment.dart'; import '../../../../core/constants/lcb_ft_constants.dart'; import '../../../../core/data/repositories/parametres_lcb_ft_repository.dart'; import '../../../../core/utils/error_formatter.dart'; import '../../../../shared/design_system/tokens/app_colors.dart'; import '../../data/models/transaction_epargne_request.dart'; import '../../data/repositories/transaction_epargne_repository.dart'; import '../../data/services/document_upload_service.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; bool _uploadingDocument = false; _DepotMode _mode = _DepotMode.manual; late TransactionEpargneRepository _repository; late ParametresLcbFtRepository _parametresRepository; late DocumentUploadService _uploadService; /// Seuil LCB-FT récupéré depuis l'API (fallback à 500k XOF). double _seuilLcbFt = kSeuilOrigineFondsObligatoireXOF; /// Pièce justificative pour opérations au-dessus du seuil File? _pieceJustificative; String? _pieceJustificativeId; @override void initState() { super.initState(); _repository = GetIt.I(); _parametresRepository = GetIt.I(); _uploadService = 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; }); } } 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(); } /// Choisir et uploader une pièce justificative (photo ou PDF) Future _choisirPieceJustificative() async { try { final result = await FilePicker.platform.pickFiles( type: FileType.custom, allowedExtensions: ['jpg', 'jpeg', 'png', 'pdf'], allowMultiple: false, ); if (result == null || result.files.isEmpty) return; final file = File(result.files.single.path!); setState(() { _uploadingDocument = true; _pieceJustificative = file; }); // Upload du fichier final documentId = await _uploadService.uploadDocument( file: file, description: 'Pièce justificative - Dépôt épargne', typeDocument: 'PIECE_JUSTIFICATIVE', ); if (!mounted) return; setState(() { _pieceJustificativeId = documentId; _uploadingDocument = false; }); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('✓ Pièce justificative uploadée avec succès'), backgroundColor: AppColors.success, ), ); } catch (e) { if (!mounted) return; setState(() { _uploadingDocument = false; _pieceJustificative = null; }); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Erreur upload : ${e.toString()}'), backgroundColor: AppColors.error, ), ); } } 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'); // Mode dev/mock : simuler le paiement au lieu d'ouvrir le navigateur final isMock = url.contains('mock') || url.contains('localhost') || !AppConfig.isProd; if (isMock) { // Simulation : attendre 1s puis confirmer le succès await Future.delayed(const Duration(milliseconds: 800)); if (!mounted) return; Navigator.of(context).pop(true); widget.onSuccess?.call(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: const Row(children: [ Icon(Icons.science_rounded, color: Colors.white, size: 16), SizedBox(width: 8), Expanded(child: Text('Dépôt simulé avec succès (mode dev)')), ]), backgroundColor: AppColors.success, ), ); return; } // Mode prod : ouvrir Wave 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; } if (_origineFondsRequis && _pieceJustificativeId == null) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( 'Une pièce justificative est requise pour les opérations ≥ ${_seuilLcbFt.toStringAsFixed(0)} XOF (LCB-FT).', ), duration: const Duration(seconds: 4), ), ); 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(), pieceJustificativeId: _pieceJustificativeId, ); 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, ), ), ), if (_origineFondsRequis) ...[ const SizedBox(height: 16), OutlinedButton.icon( onPressed: _uploadingDocument ? null : _choisirPieceJustificative, icon: _uploadingDocument ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ) : Icon( _pieceJustificativeId != null ? Icons.check_circle : Icons.attach_file, color: _pieceJustificativeId != null ? AppColors.success : null, ), label: Text( _pieceJustificativeId != null ? 'Pièce justificative uploadée' : 'Joindre une pièce justificative *', ), style: OutlinedButton.styleFrom( minimumSize: const Size(double.infinity, 48), side: _pieceJustificativeId != null ? const BorderSide(color: AppColors.success) : null, ), ), if (_pieceJustificative != null) Padding( padding: const EdgeInsets.only(top: 4.0), child: Text( _pieceJustificative!.path.split('/').last, style: Theme.of(context).textTheme.bodySmall, overflow: TextOverflow.ellipsis, ), ), Padding( padding: const EdgeInsets.only(top: 4.0), child: Text( 'Photo ou PDF (max 5 MB)', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: AppColors.textTertiary, ), ), ), ], ], ], ), ), ), 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'), ), ], ); } }