Initial commit: unionflow-mobile-apps
Application Flutter complète (sans build artifacts). Signed-off-by: lions dev Team
This commit is contained in:
@@ -0,0 +1,309 @@
|
||||
/// 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'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user