Initial commit: unionflow-mobile-apps

Application Flutter complète (sans build artifacts).

Signed-off-by: lions dev Team
This commit is contained in:
dahoud
2026-03-15 16:30:08 +00:00
commit d094d6db9c
1790 changed files with 507435 additions and 0 deletions

View File

@@ -0,0 +1,209 @@
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/transaction_epargne_request.dart';
import '../../data/repositories/transaction_epargne_repository.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<RetraitEpargneDialog> createState() => _RetraitEpargneDialogState();
}
class _RetraitEpargneDialogState extends State<RetraitEpargneDialog> {
final _formKey = GlobalKey<FormState>();
final _montantController = TextEditingController();
final _motifController = TextEditingController();
final _origineFondsController = TextEditingController();
bool _loading = false;
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<TransactionEpargneRepository>();
_parametresRepository = GetIt.I<ParametresLcbFtRepository>();
_chargerSeuil();
}
/// Charge le seuil LCB-FT depuis l'API au chargement du dialog.
Future<void> _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();
super.dispose();
}
Future<void> _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;
}
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(),
);
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),
),
),
],
),
),
),
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'),
),
],
);
}
}