Files
unionflow-mobile-apps/lib/features/epargne/presentation/widgets/creer_compte_epargne_dialog.dart
dahoud d094d6db9c Initial commit: unionflow-mobile-apps
Application Flutter complète (sans build artifacts).

Signed-off-by: lions dev Team
2026-03-15 16:30:08 +00:00

310 lines
12 KiB
Dart

/// 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<Map<String, String>> _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<CreerCompteEpargneDialog> createState() => _CreerCompteEpargneDialogState();
}
class _CreerCompteEpargneDialogState extends State<CreerCompteEpargneDialog> {
String? _organisationId;
MembreCompletModel? _selectedMembre;
String _typeCompte = 'EPARGNE_LIBRE';
final _notesController = TextEditingController();
bool _loading = false;
bool _loadingMembres = false;
bool _submitting = false;
String? _error;
List<OrganizationModel> _organisations = [];
List<MembreCompletModel> _membres = [];
@override
void initState() {
super.initState();
_loadOrganisations();
}
@override
void dispose() {
_notesController.dispose();
super.dispose();
}
Future<void> _loadOrganisations() async {
setState(() {
_loading = true;
_error = null;
_organisationId = null;
_selectedMembre = null;
_membres = [];
});
try {
final orgRepo = GetIt.instance<IOrganizationRepository>();
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<void> _loadMembresDeLOrganisation(String organisationId) async {
if (organisationId.isEmpty) {
setState(() {
_membres = [];
_selectedMembre = null;
});
return;
}
setState(() {
_loadingMembres = true;
_selectedMembre = null;
_membres = [];
});
try {
final membreRepo = GetIt.instance<IMembreRepository>();
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<void> _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<CompteEpargneRepository>();
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<String>(
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<MembreCompletModel>(
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<String>(
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'),
),
],
);
}
}