import 'dart:io'; import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import 'package:file_picker/file_picker.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 '../../data/services/document_upload_service.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; bool _uploadingDocument = false; String? _compteDestinationId; late TransactionEpargneRepository _repository; late ParametresLcbFtRepository _parametresRepository; late DocumentUploadService _uploadService; /// Seuil LCB-FT récupéré depuis l'API (fallback à 500k XOF). double _seuilLcbFt = kSeuilOrigineFondsObligatoireXOF; bool _seuilLoaded = false; /// Pièce justificative pour opérations au-dessus du seuil File? _pieceJustificative; String? _pieceJustificativeId; 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(); _uploadService = 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(); } /// 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 - Transfert é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: ColorTokens.success, ), ); } catch (e) { if (!mounted) return; setState(() { _uploadingDocument = false; _pieceJustificative = null; }); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Erreur upload : ${e.toString()}'), backgroundColor: ColorTokens.error, ), ); } } 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; } 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.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(), pieceJustificativeId: _pieceJustificativeId, ); 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), ), ), 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 ? Colors.green : 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: Colors.green) : 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: Colors.grey, ), ), ), ], ], ), ), ), 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'), ), ], ); } }