/// Dialog de création d'un compte épargne pour un membre (admin / admin organisation). /// Structure : 1) Choisir l'organisation 2) Choisir le membre de cette organisation 3) Type de compte + notes. library creer_compte_epargne_dialog; import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import '../../../members/data/models/membre_complete_model.dart'; import '../../../members/domain/repositories/membre_repository.dart'; import '../../../organizations/data/models/organization_model.dart'; import '../../../organizations/domain/repositories/organization_repository.dart'; import '../../../../shared/models/membre_search_criteria.dart'; import '../../data/repositories/transaction_epargne_repository.dart'; /// Types de compte alignés avec le backend TypeCompteEpargne. const List> _typesCompte = [ {'code': 'COURANT', 'label': 'Compte courant'}, {'code': 'EPARGNE_LIBRE', 'label': 'Épargne libre'}, {'code': 'EPARGNE_BLOQUEE', 'label': 'Épargne bloquée (garantie crédit)'}, {'code': 'DEPOT_A_TERME', 'label': 'Dépôt à terme'}, {'code': 'EPARGNE_PROJET', 'label': 'Épargne projet'}, ]; class CreerCompteEpargneDialog extends StatefulWidget { final VoidCallback? onCreated; const CreerCompteEpargneDialog({super.key, this.onCreated}); @override State createState() => _CreerCompteEpargneDialogState(); } class _CreerCompteEpargneDialogState extends State { String? _organisationId; MembreCompletModel? _selectedMembre; String _typeCompte = 'EPARGNE_LIBRE'; final _notesController = TextEditingController(); bool _loading = false; bool _loadingMembres = false; bool _submitting = false; String? _error; List _organisations = []; List _membres = []; @override void initState() { super.initState(); _loadOrganisations(); } @override void dispose() { _notesController.dispose(); super.dispose(); } Future _loadOrganisations() async { setState(() { _loading = true; _error = null; _organisationId = null; _selectedMembre = null; _membres = []; }); try { final orgRepo = GetIt.instance(); final orgs = await orgRepo.getOrganizations(page: 0, size: 100); if (mounted) { setState(() { _organisations = orgs; _loading = false; }); } } catch (e) { if (mounted) { setState(() { _loading = false; _error = 'Erreur chargement organisations: $e'; }); } } } Future _loadMembresDeLOrganisation(String organisationId) async { if (organisationId.isEmpty) { setState(() { _membres = []; _selectedMembre = null; }); return; } setState(() { _loadingMembres = true; _selectedMembre = null; _membres = []; }); try { final membreRepo = GetIt.instance(); final result = await membreRepo.searchMembres( criteria: MembreSearchCriteria( organisationIds: [organisationId], includeInactifs: false, ), page: 0, size: 200, ); if (mounted) { setState(() { _membres = result.membres; _loadingMembres = false; }); } } catch (e) { if (mounted) { setState(() { _loadingMembres = false; _membres = []; }); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Impossible de charger les membres: $e')), ); } } } Future _submit() async { if (_organisationId == null || _organisationId!.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Sélectionnez une organisation')), ); return; } if (_selectedMembre == null || _selectedMembre!.id == null || _selectedMembre!.id!.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Sélectionnez un membre')), ); return; } setState(() => _submitting = true); try { final compteRepo = GetIt.I(); await compteRepo.creerCompte( membreId: _selectedMembre!.id!, organisationId: _organisationId!, typeCompte: _typeCompte, notesOuverture: _notesController.text.trim().isEmpty ? null : _notesController.text.trim(), ); if (!mounted) return; Navigator.of(context).pop(true); widget.onCreated?.call(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Compte épargne créé')), ); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Erreur: $e')), ); } finally { if (mounted) setState(() => _submitting = false); } } @override Widget build(BuildContext context) { return AlertDialog( title: const Text('Créer un compte épargne'), content: SingleChildScrollView( child: _loading ? const Padding( padding: EdgeInsets.all(24), child: Center(child: CircularProgressIndicator()), ) : _error != null ? Padding( padding: const EdgeInsets.all(8), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text(_error!, style: TextStyle(color: Theme.of(context).colorScheme.error)), const SizedBox(height: 12), TextButton(onPressed: _loadOrganisations, child: const Text('Réessayer')), ], ), ) : Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // 1. Organisation DropdownButtonFormField( value: _organisationId, isExpanded: true, decoration: const InputDecoration( labelText: 'Organisation *', border: OutlineInputBorder(), prefixIcon: Icon(Icons.business), ), items: _organisations .map((o) => DropdownMenuItem( value: o.id, child: Text(o.nom ?? o.id ?? '', overflow: TextOverflow.ellipsis, maxLines: 1), )) .toList(), onChanged: _submitting ? null : (v) { setState(() { _organisationId = v; _selectedMembre = null; }); if (v != null && v.isNotEmpty) _loadMembresDeLOrganisation(v); }, ), const SizedBox(height: 16), // 2. Membre de l'organisation — l'administrateur sélectionne le membre pour lequel créer le compte if (_organisationId != null && _organisationId!.isNotEmpty) ...[ if (_loadingMembres) const Padding( padding: EdgeInsets.symmetric(vertical: 12), child: Center(child: CircularProgressIndicator()), ) else if (_membres.isEmpty) Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Text( 'Aucun membre dans cette organisation. Le compte épargne ne peut être créé que pour un membre existant.', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.error, ), ), ) else DropdownButtonFormField( value: _selectedMembre, isExpanded: true, decoration: const InputDecoration( labelText: 'Membre *', hintText: 'Choisir le membre pour lequel créer le compte', border: OutlineInputBorder(), prefixIcon: Icon(Icons.person), ), items: _membres .map((m) => DropdownMenuItem( value: m, child: Text( '${m.prenom} ${m.nom}${m.numeroMembre != null ? ' (${m.numeroMembre})' : ''}', overflow: TextOverflow.ellipsis, maxLines: 1, ), )) .toList(), onChanged: _submitting ? null : (v) => setState(() => _selectedMembre = v), ), const SizedBox(height: 16), ], // 3. Type de compte DropdownButtonFormField( value: _typeCompte, isExpanded: true, decoration: const InputDecoration( labelText: 'Type de compte', border: OutlineInputBorder(), ), items: _typesCompte .map((t) => DropdownMenuItem( value: t['code'], child: Text(t['label']!, overflow: TextOverflow.ellipsis, maxLines: 1), )) .toList(), onChanged: _submitting ? null : (v) => setState(() => _typeCompte = v ?? 'EPARGNE_LIBRE'), ), const SizedBox(height: 16), // 4. Notes TextFormField( controller: _notesController, decoration: const InputDecoration( labelText: 'Notes (optionnel)', border: OutlineInputBorder(), ), maxLines: 2, enabled: !_submitting, ), ], ), ), actions: [ TextButton( onPressed: _submitting ? null : () => Navigator.of(context).pop(), child: const Text('Annuler'), ), FilledButton( onPressed: (_loading || _submitting || _organisationId == null || _selectedMembre == null || _selectedMembre!.id == null) ? null : _submit, child: _submitting ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2)) : const Text('Créer'), ), ], ); } }