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/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 retrait sur un compte épargne. /// LCB-FT : origine des fonds obligatoire au-dessus du seuil. class RetraitEpargneDialog extends StatefulWidget { final String compteId; final String numeroCompte; final double soldeDisponible; final VoidCallback? onSuccess; const RetraitEpargneDialog({ super.key, required this.compteId, required this.numeroCompte, required this.soldeDisponible, this.onSuccess, }); @override State createState() => _RetraitEpargneDialogState(); } class _RetraitEpargneDialogState extends State { final _formKey = GlobalKey(); final _montantController = TextEditingController(); final _motifController = TextEditingController(); final _origineFondsController = TextEditingController(); bool _loading = false; bool _uploadingDocument = false; 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(); 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 - Retrait épargne', typeDocument: 'PIECE_JUSTIFICATIVE', ); if (!mounted) return; setState(() { _pieceJustificativeId = documentId; _uploadingDocument = false; }); _showSnack('✓ Pièce justificative uploadée avec succès', isError: false); } catch (e) { if (!mounted) return; setState(() { _uploadingDocument = false; _pieceJustificative = null; }); _showSnack('Erreur upload : ${e.toString()}'); } } Future _submit() async { if (!_formKey.currentState!.validate()) return; final montant = double.tryParse(_montantController.text.replaceAll(',', '.')); if (montant == null || montant <= 0) { _showSnack('Montant invalide'); return; } if (montant > widget.soldeDisponible) { _showSnack('Solde disponible insuffisant (${widget.soldeDisponible.toStringAsFixed(0)} XOF)'); return; } if (_origineFondsRequis && _origineFondsController.text.trim().isEmpty) { _showSnack( 'L\'origine des fonds est obligatoire pour les opérations à partir de ${_seuilLcbFt.toStringAsFixed(0)} XOF (LCB-FT).', ); return; } if (_origineFondsRequis && _pieceJustificativeId == null) { _showSnack( 'Une pièce justificative est requise pour les opérations ≥ ${_seuilLcbFt.toStringAsFixed(0)} XOF (LCB-FT).', duration: 4, ); return; } setState(() => _loading = true); try { final request = TransactionEpargneRequest( compteId: widget.compteId, typeTransaction: 'RETRAIT', 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(); _showSnack('Retrait enregistré', isError: false); } catch (e) { if (!mounted) return; _showSnack( ErrorFormatter.format(e), duration: ErrorFormatter.isLcbFtError(e) ? 6 : 3, ); } finally { if (mounted) setState(() => _loading = false); } } void _showSnack(String msg, {bool isError = true, int duration = 3}) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(msg), backgroundColor: isError ? ColorTokens.error : ColorTokens.success, duration: Duration(seconds: duration), ), ); } @override Widget build(BuildContext context) { return AlertDialog( title: const Text('Retrait'), content: Form( key: _formKey, child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( widget.numeroCompte, style: TypographyTokens.bodySmall.copyWith(color: ColorTokens.onSurfaceVariant), ), Text( 'Solde disponible: ${widget.soldeDisponible.toStringAsFixed(0)} XOF', style: TypographyTokens.titleSmall, ), 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'; if (n > widget.soldeDisponible) 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: Theme.of(context).textTheme.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('Valider le retrait'), ), ], ); } }