310 lines
12 KiB
Dart
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'),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|