feat(unionflow): ajout Spec-Kit, constitution, mission mutuelles
- Config Spec-Kit pour Spec-Driven Development - CONSTITUTION.md + .specify/memory/constitution.md - Commandes Cursor /speckit.*, règles projet - Mission: associations + mutuelles d'épargne et de financement - .gitignore: versionner config spec-kit unionflow Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
/// Dialog de création d'une demande d'adhésion
|
||||
library create_adhesion_dialog;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import '../../bloc/adhesions_bloc.dart';
|
||||
import '../../data/models/adhesion_model.dart';
|
||||
import '../../../organizations/data/models/organization_model.dart';
|
||||
import '../../../organizations/data/repositories/organization_repository.dart';
|
||||
import '../../../members/data/services/membre_search_service.dart';
|
||||
import '../../../members/data/models/membre_complete_model.dart';
|
||||
|
||||
class CreateAdhesionDialog extends StatefulWidget {
|
||||
final VoidCallback onCreated;
|
||||
|
||||
const CreateAdhesionDialog({super.key, required this.onCreated});
|
||||
|
||||
@override
|
||||
State<CreateAdhesionDialog> createState() => _CreateAdhesionDialogState();
|
||||
}
|
||||
|
||||
class _CreateAdhesionDialogState extends State<CreateAdhesionDialog> {
|
||||
final _fraisController = TextEditingController();
|
||||
String? _membreId;
|
||||
String? _organisationId;
|
||||
bool _loading = false;
|
||||
List<OrganizationModel> _organisations = [];
|
||||
List<MembreCompletModel> _membres = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadOrgs();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_fraisController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _loadOrgs() async {
|
||||
try {
|
||||
final repo = GetIt.instance<OrganizationRepository>();
|
||||
final list = await repo.getOrganizations(page: 0, size: 100);
|
||||
if (mounted) setState(() => _organisations = list);
|
||||
} catch (_) {
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _searchMembres(String query) async {
|
||||
if (query.length < 2) {
|
||||
setState(() => _membres = []);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final service = GetIt.instance<MembreSearchService>();
|
||||
final result = await service.quickSearch(query: query, size: 20);
|
||||
if (mounted) setState(() => _membres = result.membres);
|
||||
} catch (_) {
|
||||
if (mounted) setState(() => _membres = []);
|
||||
}
|
||||
}
|
||||
|
||||
void _submit() {
|
||||
if (_membreId == null || _organisationId == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Veuillez sélectionner un membre et une organisation')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
final frais = double.tryParse(_fraisController.text.replaceAll(',', '.'));
|
||||
if (frais == null || frais <= 0) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Frais d\'adhésion invalides')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
setState(() => _loading = true);
|
||||
final adhesion = AdhesionModel(
|
||||
membreId: _membreId,
|
||||
organisationId: _organisationId,
|
||||
fraisAdhesion: frais,
|
||||
codeDevise: 'XOF',
|
||||
dateDemande: DateTime.now(),
|
||||
);
|
||||
context.read<AdhesionsBloc>().add(CreateAdhesion(adhesion));
|
||||
widget.onCreated();
|
||||
if (mounted) {
|
||||
setState(() => _loading = false);
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Nouvelle demande d\'adhésion'),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Rechercher un membre (nom, prénom)',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
onChanged: _searchMembres,
|
||||
enabled: !_loading,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
DropdownButtonFormField<String>(
|
||||
value: _membreId,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Membre',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
items: _membres
|
||||
.map((m) => DropdownMenuItem<String>(
|
||||
value: m.id,
|
||||
child: Text('${m.prenom} ${m.nom}'),
|
||||
))
|
||||
.toList(),
|
||||
onChanged: _loading ? null : (v) => setState(() => _membreId = v),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
DropdownButtonFormField<String>(
|
||||
value: _organisationId,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Organisation',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
items: _organisations
|
||||
.map((o) => DropdownMenuItem<String>(
|
||||
value: o.id,
|
||||
child: Text(o.nom),
|
||||
))
|
||||
.toList(),
|
||||
onChanged: _loading ? null : (v) => setState(() => _organisationId = v),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: _fraisController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Frais d\'adhésion (FCFA)',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
enabled: !_loading,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
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('Créer'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
/// Dialog pour enregistrer un paiement sur une adhésion
|
||||
library paiement_adhesion_dialog;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../bloc/adhesions_bloc.dart';
|
||||
|
||||
class PaiementAdhesionDialog extends StatefulWidget {
|
||||
final String adhesionId;
|
||||
final double montantRestant;
|
||||
final VoidCallback onPaid;
|
||||
|
||||
const PaiementAdhesionDialog({
|
||||
super.key,
|
||||
required this.adhesionId,
|
||||
required this.montantRestant,
|
||||
required this.onPaid,
|
||||
});
|
||||
|
||||
@override
|
||||
State<PaiementAdhesionDialog> createState() => _PaiementAdhesionDialogState();
|
||||
}
|
||||
|
||||
class _PaiementAdhesionDialogState extends State<PaiementAdhesionDialog> {
|
||||
final _montantController = TextEditingController();
|
||||
final _refController = TextEditingController();
|
||||
String? _methode;
|
||||
bool _loading = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_montantController.text = widget.montantRestant.toStringAsFixed(0);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_montantController.dispose();
|
||||
_refController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _submit() {
|
||||
final montant = double.tryParse(_montantController.text.replaceAll(',', '.'));
|
||||
if (montant == null || montant <= 0) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Montant invalide')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (montant > widget.montantRestant) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Le montant ne peut pas dépasser le restant dû')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
setState(() => _loading = true);
|
||||
context.read<AdhesionsBloc>().add(
|
||||
EnregistrerPaiementAdhesion(
|
||||
widget.adhesionId,
|
||||
montantPaye: montant,
|
||||
methodePaiement: _methode,
|
||||
referencePaiement: _refController.text.trim().isEmpty
|
||||
? null
|
||||
: _refController.text.trim(),
|
||||
),
|
||||
);
|
||||
widget.onPaid();
|
||||
if (mounted) {
|
||||
setState(() => _loading = false);
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Enregistrer un paiement'),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('Restant dû : ${widget.montantRestant.toStringAsFixed(0)} FCFA'),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: _montantController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Montant payé (FCFA)',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
enabled: !_loading,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
DropdownButtonFormField<String>(
|
||||
value: _methode,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Méthode de paiement',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
items: const [
|
||||
DropdownMenuItem(value: 'ESPECES', child: Text('Espèces')),
|
||||
DropdownMenuItem(value: 'VIREMENT', child: Text('Virement')),
|
||||
DropdownMenuItem(value: 'WAVE_MONEY', child: Text('Wave Money')),
|
||||
DropdownMenuItem(value: 'ORANGE_MONEY', child: Text('Orange Money')),
|
||||
DropdownMenuItem(value: 'CHEQUE', child: Text('Chèque')),
|
||||
],
|
||||
onChanged: _loading ? null : (v) => setState(() => _methode = v),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: _refController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Référence (optionnel)',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
enabled: !_loading,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
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('Enregistrer'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/// Dialog pour rejeter une adhésion (saisie du motif)
|
||||
library rejet_adhesion_dialog;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../bloc/adhesions_bloc.dart';
|
||||
|
||||
class RejetAdhesionDialog extends StatefulWidget {
|
||||
final String adhesionId;
|
||||
final VoidCallback onRejected;
|
||||
|
||||
const RejetAdhesionDialog({
|
||||
super.key,
|
||||
required this.adhesionId,
|
||||
required this.onRejected,
|
||||
});
|
||||
|
||||
@override
|
||||
State<RejetAdhesionDialog> createState() => _RejetAdhesionDialogState();
|
||||
}
|
||||
|
||||
class _RejetAdhesionDialogState extends State<RejetAdhesionDialog> {
|
||||
final _controller = TextEditingController();
|
||||
bool _loading = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _submit() {
|
||||
final motif = _controller.text.trim();
|
||||
if (motif.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Veuillez saisir un motif de rejet')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
setState(() => _loading = true);
|
||||
context.read<AdhesionsBloc>().add(RejeterAdhesion(widget.adhesionId, motif));
|
||||
widget.onRejected();
|
||||
if (mounted) {
|
||||
setState(() => _loading = false);
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Rejeter la demande'),
|
||||
content: TextField(
|
||||
controller: _controller,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Motif du rejet',
|
||||
hintText: 'Saisir le motif...',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
maxLines: 3,
|
||||
enabled: !_loading,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _loading ? null : () => Navigator.of(context).pop(),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: _loading ? null : _submit,
|
||||
style: FilledButton.styleFrom(backgroundColor: Colors.red),
|
||||
child: _loading
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Text('Rejeter'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user