feat: WebSocket temps réel + Finance Workflow + corrections
- Task #6: WebSocket /ws/dashboard + Kafka events (5 topics) * Backend: KafkaEventProducer, KafkaEventConsumer * Mobile: WebSocketService (reconnection, heartbeat, typed events) * DashboardBloc: Auto-refresh depuis WebSocket events - Finance Workflow: approbations + budgets (backend + mobile) * Backend: entities, services, resources, migrations Flyway V6 * Mobile: features finance_workflow complète avec BLoC - Corrections DI: interfaces IRepository partout * IProfileRepository, IOrganizationRepository, IMembreRepository * GetIt configuré avec @injectable - Spec-Kit: constitution + templates mis à jour * .specify/memory/constitution.md enrichie * Templates agent, plan, spec, tasks, checklist - Nettoyage: fichiers temporaires supprimés Signed-off-by: lions dev Team
This commit is contained in:
@@ -4,12 +4,13 @@ library create_adhesion_dialog;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import '../../../../core/utils/logger.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 '../../../organizations/domain/repositories/organization_repository.dart';
|
||||
import '../../../members/data/models/membre_complete_model.dart';
|
||||
import '../../../profile/domain/repositories/profile_repository.dart';
|
||||
|
||||
class CreateAdhesionDialog extends StatefulWidget {
|
||||
final VoidCallback onCreated;
|
||||
@@ -22,16 +23,42 @@ class CreateAdhesionDialog extends StatefulWidget {
|
||||
|
||||
class _CreateAdhesionDialogState extends State<CreateAdhesionDialog> {
|
||||
final _fraisController = TextEditingController();
|
||||
String? _membreId;
|
||||
String? _organisationId;
|
||||
bool _loading = false;
|
||||
bool _isInitLoading = true;
|
||||
List<OrganizationModel> _organisations = [];
|
||||
List<MembreCompletModel> _membres = [];
|
||||
MembreCompletModel? _me;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadOrgs();
|
||||
_loadInitialData();
|
||||
}
|
||||
|
||||
Future<void> _loadInitialData() async {
|
||||
try {
|
||||
final user = await GetIt.instance<IProfileRepository>().getMe();
|
||||
final orgRepo = GetIt.instance<IOrganizationRepository>();
|
||||
final list = await orgRepo.getOrganizations(page: 0, size: 100);
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_me = user;
|
||||
_organisations = list;
|
||||
_isInitLoading = false;
|
||||
});
|
||||
}
|
||||
} catch (e, st) {
|
||||
AppLogger.error('CreateAdhesionDialog: chargement profil/organisations échoué', error: e, stackTrace: st);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isInitLoading = false;
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Impossible de charger le profil ou les organisations. Réessayez.')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -40,32 +67,14 @@ class _CreateAdhesionDialogState extends State<CreateAdhesionDialog> {
|
||||
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 = []);
|
||||
void _submit() {
|
||||
if (_me == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Profil non chargé, veuillez réessayer')),
|
||||
);
|
||||
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) {
|
||||
if (_organisationId == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Veuillez sélectionner un membre et une organisation')),
|
||||
);
|
||||
@@ -80,7 +89,7 @@ class _CreateAdhesionDialogState extends State<CreateAdhesionDialog> {
|
||||
}
|
||||
setState(() => _loading = true);
|
||||
final adhesion = AdhesionModel(
|
||||
membreId: _membreId,
|
||||
membreId: _me!.id,
|
||||
organisationId: _organisationId,
|
||||
fraisAdhesion: frais,
|
||||
codeDevise: 'XOF',
|
||||
@@ -102,32 +111,24 @@ class _CreateAdhesionDialogState extends State<CreateAdhesionDialog> {
|
||||
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),
|
||||
),
|
||||
if (_isInitLoading)
|
||||
const CircularProgressIndicator()
|
||||
else if (_me != null)
|
||||
TextFormField(
|
||||
initialValue: '${_me!.prenom} ${_me!.nom}',
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Membre',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.person),
|
||||
),
|
||||
enabled: false,
|
||||
)
|
||||
else
|
||||
const Text('Impossible de récupérer votre profil', style: TextStyle(color: Colors.red)),
|
||||
const SizedBox(height: 16),
|
||||
DropdownButtonFormField<String>(
|
||||
value: _organisationId,
|
||||
isExpanded: true,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Organisation',
|
||||
border: OutlineInputBorder(),
|
||||
@@ -135,7 +136,7 @@ class _CreateAdhesionDialogState extends State<CreateAdhesionDialog> {
|
||||
items: _organisations
|
||||
.map((o) => DropdownMenuItem<String>(
|
||||
value: o.id,
|
||||
child: Text(o.nom),
|
||||
child: Text(o.nom, overflow: TextOverflow.ellipsis, maxLines: 1),
|
||||
))
|
||||
.toList(),
|
||||
onChanged: _loading ? null : (v) => setState(() => _organisationId = v),
|
||||
|
||||
@@ -3,6 +3,7 @@ library paiement_adhesion_dialog;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../../shared/constants/payment_method_assets.dart';
|
||||
import '../../bloc/adhesions_bloc.dart';
|
||||
|
||||
class PaiementAdhesionDialog extends StatefulWidget {
|
||||
@@ -40,6 +41,25 @@ class _PaiementAdhesionDialogState extends State<PaiementAdhesionDialog> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
List<DropdownMenuItem<String>> _buildPaymentMethodItems() {
|
||||
const codes = ['ESPECES', 'VIREMENT', 'WAVE_MONEY', 'ORANGE_MONEY', 'CHEQUE'];
|
||||
const labels = {'ESPECES': 'Espèces', 'VIREMENT': 'Virement', 'WAVE_MONEY': 'Wave Money', 'ORANGE_MONEY': 'Orange Money', 'CHEQUE': 'Chèque'};
|
||||
return codes.map((code) {
|
||||
final label = labels[code] ?? code;
|
||||
return DropdownMenuItem<String>(
|
||||
value: code,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
PaymentMethodIcon(paymentMethodCode: code, width: 24, height: 24),
|
||||
const SizedBox(width: 12),
|
||||
Text(label),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
void _submit() {
|
||||
final montant = double.tryParse(_montantController.text.replaceAll(',', '.'));
|
||||
if (montant == null || montant <= 0) {
|
||||
@@ -98,13 +118,7 @@ class _PaiementAdhesionDialogState extends State<PaiementAdhesionDialog> {
|
||||
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')),
|
||||
],
|
||||
items: _buildPaymentMethodItems(),
|
||||
onChanged: _loading ? null : (v) => setState(() => _methode = v),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
@@ -22,6 +22,7 @@ class RejetAdhesionDialog extends StatefulWidget {
|
||||
class _RejetAdhesionDialogState extends State<RejetAdhesionDialog> {
|
||||
final _controller = TextEditingController();
|
||||
bool _loading = false;
|
||||
bool _rejectSent = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
@@ -37,18 +38,36 @@ class _RejetAdhesionDialogState extends State<RejetAdhesionDialog> {
|
||||
);
|
||||
return;
|
||||
}
|
||||
setState(() => _loading = true);
|
||||
setState(() {
|
||||
_loading = true;
|
||||
_rejectSent = 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(
|
||||
return BlocListener<AdhesionsBloc, AdhesionsState>(
|
||||
listenWhen: (_, state) => _rejectSent && (state.status == AdhesionsStatus.loaded || state.status == AdhesionsStatus.error),
|
||||
listener: (context, state) {
|
||||
if (!_rejectSent || !mounted) return;
|
||||
if (state.status == AdhesionsStatus.error) {
|
||||
setState(() {
|
||||
_loading = false;
|
||||
_rejectSent = false;
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(state.message ?? 'Erreur lors du rejet')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (state.status == AdhesionsStatus.loaded) {
|
||||
setState(() => _rejectSent = false);
|
||||
widget.onRejected();
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
child: AlertDialog(
|
||||
title: const Text('Rejeter la demande'),
|
||||
content: TextField(
|
||||
controller: _controller,
|
||||
@@ -77,6 +96,7 @@ class _RejetAdhesionDialogState extends State<RejetAdhesionDialog> {
|
||||
: const Text('Rejeter'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user