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:
dahoud
2026-03-15 02:12:17 +00:00
parent bbc409de9d
commit e8ad874015
635 changed files with 58160 additions and 20674 deletions

View File

@@ -7,6 +7,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:intl/intl.dart';
import '../../bloc/evenements_bloc.dart';
import '../../bloc/evenements_event.dart';
import '../../bloc/evenements_state.dart';
import '../../data/models/evenement_model.dart';
/// Dialogue de création d'événement
@@ -19,7 +20,8 @@ class CreateEventDialog extends StatefulWidget {
class _CreateEventDialogState extends State<CreateEventDialog> {
final _formKey = GlobalKey<FormState>();
bool _createSent = false;
// Contrôleurs de texte
final _titreController = TextEditingController();
final _descriptionController = TextEditingController();
@@ -46,7 +48,29 @@ class _CreateEventDialogState extends State<CreateEventDialog> {
@override
Widget build(BuildContext context) {
return Dialog(
return BlocListener<EvenementsBloc, EvenementsState>(
listenWhen: (_, state) => _createSent && (state is EvenementCreated || state is EvenementsError),
listener: (context, state) {
if (!_createSent || !mounted) return;
if (state is EvenementsError) {
setState(() => _createSent = false);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.message), backgroundColor: Colors.red),
);
return;
}
if (state is EvenementCreated) {
setState(() => _createSent = false);
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Événement créé avec succès'),
backgroundColor: Colors.green,
),
);
}
},
child: Dialog(
child: Container(
width: MediaQuery.of(context).size.width * 0.9,
constraints: const BoxConstraints(maxHeight: 600),
@@ -297,6 +321,7 @@ class _CreateEventDialogState extends State<CreateEventDialog> {
],
),
),
),
);
}
@@ -409,19 +434,8 @@ class _CreateEventDialogState extends State<CreateEventDialog> {
statut: StatutEvenement.planifie,
);
// Envoyer l'événement au BLoC
setState(() => _createSent = true);
context.read<EvenementsBloc>().add(CreateEvenement(evenement));
// Fermer le dialogue
Navigator.pop(context);
// Afficher un message de succès
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Événement créé avec succès'),
backgroundColor: Colors.green,
),
);
}
}
}

View File

@@ -7,6 +7,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:intl/intl.dart';
import '../../bloc/evenements_bloc.dart';
import '../../bloc/evenements_event.dart';
import '../../bloc/evenements_state.dart';
import '../../data/models/evenement_model.dart';
/// Dialogue de modification d'événement
@@ -24,6 +25,7 @@ class EditEventDialog extends StatefulWidget {
class _EditEventDialogState extends State<EditEventDialog> {
final _formKey = GlobalKey<FormState>();
bool _updateSent = false;
// Contrôleurs de texte
late final TextEditingController _titreController;
@@ -74,7 +76,29 @@ class _EditEventDialogState extends State<EditEventDialog> {
@override
Widget build(BuildContext context) {
return Dialog(
return BlocListener<EvenementsBloc, EvenementsState>(
listenWhen: (_, state) => _updateSent && (state is EvenementUpdated || state is EvenementsError),
listener: (context, state) {
if (!_updateSent || !mounted) return;
if (state is EvenementsError) {
setState(() => _updateSent = false);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.message), backgroundColor: Colors.red),
);
return;
}
if (state is EvenementUpdated) {
setState(() => _updateSent = false);
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Événement modifié avec succès'),
backgroundColor: Colors.green,
),
);
}
},
child: Dialog(
child: Container(
width: MediaQuery.of(context).size.width * 0.9,
constraints: const BoxConstraints(maxHeight: 600),
@@ -356,6 +380,7 @@ class _EditEventDialogState extends State<EditEventDialog> {
],
),
),
),
);
}
@@ -492,19 +517,8 @@ class _EditEventDialogState extends State<EditEventDialog> {
estPublic: _estPublic,
);
// Envoyer l'événement au BLoC
setState(() => _updateSent = true);
context.read<EvenementsBloc>().add(UpdateEvenement(widget.evenement.id!.toString(), evenementUpdated));
// Fermer le dialogue
Navigator.pop(context);
// Afficher un message de succès
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Événement modifié avec succès'),
backgroundColor: Colors.green,
),
);
}
}
}

View File

@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../bloc/evenements_bloc.dart';
import '../../bloc/evenements_event.dart';
import '../../bloc/evenements_state.dart';
import '../../data/models/evenement_model.dart';
class InscriptionEventDialog extends StatefulWidget {
@@ -24,6 +25,7 @@ class InscriptionEventDialog extends StatefulWidget {
class _InscriptionEventDialogState extends State<InscriptionEventDialog> {
final _formKey = GlobalKey<FormState>();
final _commentaireController = TextEditingController();
bool _actionSent = false;
@override
void dispose() {
@@ -33,7 +35,38 @@ class _InscriptionEventDialogState extends State<InscriptionEventDialog> {
@override
Widget build(BuildContext context) {
return Dialog(
return BlocListener<EvenementsBloc, EvenementsState>(
listenWhen: (_, state) =>
_actionSent &&
(state is EvenementInscrit ||
state is EvenementDesinscrit ||
state is EvenementsError),
listener: (context, state) {
if (!_actionSent || !mounted) return;
if (state is EvenementsError) {
setState(() => _actionSent = false);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.message), backgroundColor: Colors.red),
);
return;
}
if (state is EvenementInscrit || state is EvenementDesinscrit) {
setState(() => _actionSent = false);
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
state is EvenementInscrit
? 'Inscription réussie'
: 'Désinscription réussie',
),
backgroundColor:
state is EvenementInscrit ? Colors.green : Colors.orange,
),
);
}
},
child: Dialog(
child: Container(
width: MediaQuery.of(context).size.width * 0.9,
constraints: const BoxConstraints(maxHeight: 500),
@@ -67,6 +100,7 @@ class _InscriptionEventDialogState extends State<InscriptionEventDialog> {
],
),
),
),
);
}
@@ -290,30 +324,13 @@ class _InscriptionEventDialogState extends State<InscriptionEventDialog> {
}
void _submitForm() {
setState(() => _actionSent = true);
if (widget.isInscrit) {
// Désinscription
context.read<EvenementsBloc>().add(DesinscrireEvenement(widget.evenement.id!.toString()));
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Désinscription réussie'),
backgroundColor: Colors.orange,
),
);
} else {
// Inscription
context.read<EvenementsBloc>().add(
InscrireEvenement(widget.evenement.id!.toString()),
);
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Inscription réussie'),
backgroundColor: Colors.green,
),
);
}
}
}