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

@@ -1,17 +1,45 @@
/// BLoC pour la gestion des organisations
/// BLoC pour la gestion des organisations (Clean Architecture)
library organizations_bloc;
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:injectable/injectable.dart';
import '../data/models/organization_model.dart';
import '../data/services/organization_service.dart';
import '../domain/usecases/get_organizations.dart';
import '../domain/usecases/get_organization_by_id.dart';
import '../domain/usecases/create_organization.dart' as uc;
import '../domain/usecases/update_organization.dart' as uc;
import '../domain/usecases/delete_organization.dart' as uc;
import '../domain/usecases/get_organization_members.dart';
import '../domain/usecases/update_organization_config.dart';
import '../domain/repositories/organization_repository.dart';
import 'organizations_event.dart';
import 'organizations_state.dart';
/// BLoC principal pour la gestion des organisations
@injectable
class OrganizationsBloc extends Bloc<OrganizationsEvent, OrganizationsState> {
final OrganizationService _organizationService;
final GetOrganizations _getOrganizations;
final GetOrganizationById _getOrganizationById;
final uc.CreateOrganization _createOrganization;
final uc.UpdateOrganization _updateOrganization;
final uc.DeleteOrganization _deleteOrganization;
final GetOrganizationMembers _getOrganizationMembers;
final UpdateOrganizationConfig _updateOrganizationConfig;
final IOrganizationRepository _repository; // Pour méthodes non-couvertes (activate, suspend, search, stats)
final OrganizationService _organizationService; // Pour helpers (sort, filter local)
OrganizationsBloc(this._organizationService) : super(const OrganizationsInitial()) {
OrganizationsBloc(
this._getOrganizations,
this._getOrganizationById,
this._createOrganization,
this._updateOrganization,
this._deleteOrganization,
this._getOrganizationMembers,
this._updateOrganizationConfig,
this._repository,
this._organizationService,
) : super(const OrganizationsInitial()) {
// Enregistrement des handlers d'événements
on<LoadOrganizations>(_onLoadOrganizations);
on<LoadMoreOrganizations>(_onLoadMoreOrganizations);
@@ -42,18 +70,39 @@ class OrganizationsBloc extends Bloc<OrganizationsEvent, OrganizationsState> {
emit(const OrganizationsLoading());
}
final organizations = await _organizationService.getOrganizations(
page: event.page,
size: event.size,
recherche: event.recherche,
);
List<OrganizationModel> listToUse;
List<String>? filterIds;
if (event.useMesOnly) {
// Admin d'organisation : endpoint backend /mes retourne uniquement ses organisations
listToUse = await _repository.getMesOrganisations();
filterIds = listToUse.map((o) => o.id).whereType<String>().toList();
} else {
final organizations = await _getOrganizations(
page: event.page,
size: event.size,
recherche: event.recherche,
);
if (event.filterOrganizationIds != null &&
event.filterOrganizationIds!.isNotEmpty) {
final allowedIds = event.filterOrganizationIds!.toSet();
listToUse = organizations
.where((org) => org.id != null && allowedIds.contains(org.id))
.toList();
filterIds = event.filterOrganizationIds;
} else {
listToUse = organizations;
filterIds = null;
}
}
emit(OrganizationsLoaded(
organizations: organizations,
filteredOrganizations: organizations,
hasReachedMax: organizations.length < event.size,
organizations: listToUse,
filteredOrganizations: listToUse,
hasReachedMax: event.useMesOnly || listToUse.length < event.size,
currentPage: event.page,
currentSearch: event.recherche,
filterOrganizationIds: filterIds,
useMesOnly: event.useMesOnly,
));
} catch (e) {
emit(OrganizationsError(
@@ -77,13 +126,21 @@ class OrganizationsBloc extends Bloc<OrganizationsEvent, OrganizationsState> {
try {
final nextPage = currentState.currentPage + 1;
final newOrganizations = await _organizationService.getOrganizations(
final newOrganizations = await _getOrganizations(
page: nextPage,
size: 20,
recherche: currentState.currentSearch,
);
final allOrganizations = [...currentState.organizations, ...newOrganizations];
var allOrganizations = [...currentState.organizations, ...newOrganizations];
// Réappliquer le filtre orgAdmin si présent
if (currentState.filterOrganizationIds != null &&
currentState.filterOrganizationIds!.isNotEmpty) {
final allowedIds = currentState.filterOrganizationIds!.toSet();
allOrganizations = allOrganizations
.where((org) => org.id != null && allowedIds.contains(org.id))
.toList();
}
final filteredOrganizations = _applyCurrentFilters(allOrganizations, currentState);
emit(currentState.copyWith(
@@ -137,7 +194,7 @@ class OrganizationsBloc extends Bloc<OrganizationsEvent, OrganizationsState> {
));
// Puis recherche serveur pour plus de résultats
final serverResults = await _organizationService.getOrganizations(
final serverResults = await _getOrganizations(
page: 0,
size: 50,
recherche: event.query,
@@ -169,7 +226,7 @@ class OrganizationsBloc extends Bloc<OrganizationsEvent, OrganizationsState> {
emit(const OrganizationsLoading());
try {
final organizations = await _organizationService.searchOrganizations(
final organizations = await _repository.searchOrganizations(
nom: event.nom,
type: event.type,
statut: event.statut,
@@ -204,7 +261,7 @@ class OrganizationsBloc extends Bloc<OrganizationsEvent, OrganizationsState> {
emit(OrganizationLoading(event.id));
try {
final organization = await _organizationService.getOrganizationById(event.id);
final organization = await _getOrganizationById(event.id);
if (organization != null) {
emit(OrganizationLoaded(organization));
} else {
@@ -226,7 +283,7 @@ class OrganizationsBloc extends Bloc<OrganizationsEvent, OrganizationsState> {
emit(const OrganizationCreating());
try {
final createdOrganization = await _organizationService.createOrganization(event.organization);
final createdOrganization = await _createOrganization(event.organization);
emit(OrganizationCreated(createdOrganization));
// Recharger la liste si elle était déjà chargée
@@ -249,7 +306,7 @@ class OrganizationsBloc extends Bloc<OrganizationsEvent, OrganizationsState> {
emit(OrganizationUpdating(event.id));
try {
final updatedOrganization = await _organizationService.updateOrganization(
final updatedOrganization = await _updateOrganization(
event.id,
event.organization,
);
@@ -284,7 +341,7 @@ class OrganizationsBloc extends Bloc<OrganizationsEvent, OrganizationsState> {
emit(OrganizationDeleting(event.id));
try {
await _organizationService.deleteOrganization(event.id);
await _deleteOrganization(event.id);
emit(OrganizationDeleted(event.id));
// Retirer de la liste si elle était déjà chargée
@@ -313,7 +370,7 @@ class OrganizationsBloc extends Bloc<OrganizationsEvent, OrganizationsState> {
emit(OrganizationActivating(event.id));
try {
final activatedOrganization = await _organizationService.activateOrganization(event.id);
final activatedOrganization = await _repository.activateOrganization(event.id);
emit(OrganizationActivated(activatedOrganization));
// Mettre à jour la liste si elle était déjà chargée
@@ -345,7 +402,7 @@ class OrganizationsBloc extends Bloc<OrganizationsEvent, OrganizationsState> {
emit(OrganizationSuspending(event.id));
try {
final suspendedOrganization = await _organizationService.suspendOrganization(event.id);
final suspendedOrganization = await _repository.suspendOrganization(event.id);
emit(OrganizationSuspended(suspendedOrganization));
// Mettre à jour la liste si elle était déjà chargée
@@ -454,7 +511,7 @@ class OrganizationsBloc extends Bloc<OrganizationsEvent, OrganizationsState> {
emit(const OrganizationsStatsLoading());
try {
final stats = await _organizationService.getOrganizationsStats();
final stats = await _repository.getOrganizationsStats();
emit(OrganizationsStatsLoaded(stats));
} catch (e) {
emit(const OrganizationsStatsError('Erreur lors du chargement des statistiques'));
@@ -483,7 +540,15 @@ class OrganizationsBloc extends Bloc<OrganizationsEvent, OrganizationsState> {
RefreshOrganizations event,
Emitter<OrganizationsState> emit,
) {
add(const LoadOrganizations(refresh: true));
final currentState = state;
if (currentState is OrganizationsLoaded && currentState.useMesOnly) {
add(const LoadOrganizations(refresh: true, useMesOnly: true));
} else {
final filterIds = currentState is OrganizationsLoaded
? currentState.filterOrganizationIds
: null;
add(LoadOrganizations(refresh: true, filterOrganizationIds: filterIds));
}
}
/// Remet à zéro l'état

View File

@@ -18,16 +18,22 @@ class LoadOrganizations extends OrganizationsEvent {
final int size;
final String? recherche;
final bool refresh;
/// Filtre par IDs d'organisations (ex. pour orgAdmin : uniquement son organisation)
final List<String>? filterOrganizationIds;
/// Si true, appelle GET /api/organisations/mes (uniquement les orgs du membre connecté)
final bool useMesOnly;
const LoadOrganizations({
this.page = 0,
this.size = 20,
this.recherche,
this.refresh = false,
this.filterOrganizationIds,
this.useMesOnly = false,
});
@override
List<Object?> get props => [page, size, recherche, refresh];
List<Object?> get props => [page, size, recherche, refresh, filterOrganizationIds, useMesOnly];
}
/// Événement pour charger plus d'organisations (pagination)

View File

@@ -44,6 +44,10 @@ class OrganizationsLoaded extends OrganizationsState {
final OrganizationSortType? sortType;
final bool sortAscending;
final Map<String, dynamic>? stats;
/// Filtre appliqué (ex. orgAdmin : uniquement ses organisations)
final List<String>? filterOrganizationIds;
/// True si la liste provient de GET /mes (admin d'organisation)
final bool useMesOnly;
const OrganizationsLoaded({
required this.organizations,
@@ -56,6 +60,8 @@ class OrganizationsLoaded extends OrganizationsState {
this.sortType,
this.sortAscending = true,
this.stats,
this.filterOrganizationIds,
this.useMesOnly = false,
});
/// Copie avec modifications
@@ -70,6 +76,8 @@ class OrganizationsLoaded extends OrganizationsState {
OrganizationSortType? sortType,
bool? sortAscending,
Map<String, dynamic>? stats,
List<String>? filterOrganizationIds,
bool? useMesOnly,
bool clearSearch = false,
bool clearStatusFilter = false,
bool clearTypeFilter = false,
@@ -86,6 +94,8 @@ class OrganizationsLoaded extends OrganizationsState {
sortType: clearSort ? null : (sortType ?? this.sortType),
sortAscending: sortAscending ?? this.sortAscending,
stats: stats ?? this.stats,
filterOrganizationIds: filterOrganizationIds ?? this.filterOrganizationIds,
useMesOnly: useMesOnly ?? this.useMesOnly,
);
}
@@ -130,6 +140,8 @@ class OrganizationsLoaded extends OrganizationsState {
sortType,
sortAscending,
stats,
filterOrganizationIds,
useMesOnly,
];
}

View File

@@ -213,6 +213,10 @@ class OrganizationModel extends Equatable {
@JsonKey(name: 'nombreAdministrateurs')
final int nombreAdministrateurs;
/// Nombre d'événements (fourni par l'API si disponible)
@JsonKey(name: 'nombreEvenements')
final int? nombreEvenements;
/// Budget annuel
@JsonKey(name: 'budgetAnnuel')
final double? budgetAnnuel;
@@ -280,6 +284,7 @@ class OrganizationModel extends Equatable {
this.logo,
this.nombreMembres = 0,
this.nombreAdministrateurs = 0,
this.nombreEvenements,
this.budgetAnnuel,
this.devise = 'XOF',
this.cotisationObligatoire = false,
@@ -323,6 +328,7 @@ class OrganizationModel extends Equatable {
String? logo,
int? nombreMembres,
int? nombreAdministrateurs,
int? nombreEvenements,
double? budgetAnnuel,
String? devise,
bool? cotisationObligatoire,
@@ -357,6 +363,7 @@ class OrganizationModel extends Equatable {
logo: logo ?? this.logo,
nombreMembres: nombreMembres ?? this.nombreMembres,
nombreAdministrateurs: nombreAdministrateurs ?? this.nombreAdministrateurs,
nombreEvenements: nombreEvenements ?? this.nombreEvenements,
budgetAnnuel: budgetAnnuel ?? this.budgetAnnuel,
devise: devise ?? this.devise,
cotisationObligatoire: cotisationObligatoire ?? this.cotisationObligatoire,
@@ -414,6 +421,7 @@ class OrganizationModel extends Equatable {
logo,
nombreMembres,
nombreAdministrateurs,
nombreEvenements,
budgetAnnuel,
devise,
cotisationObligatoire,

View File

@@ -34,6 +34,7 @@ OrganizationModel _$OrganizationModelFromJson(Map<String, dynamic> json) =>
nombreMembres: (json['nombreMembres'] as num?)?.toInt() ?? 0,
nombreAdministrateurs:
(json['nombreAdministrateurs'] as num?)?.toInt() ?? 0,
nombreEvenements: (json['nombreEvenements'] as num?)?.toInt(),
budgetAnnuel: (json['budgetAnnuel'] as num?)?.toDouble(),
devise: json['devise'] as String? ?? 'XOF',
cotisationObligatoire: json['cotisationObligatoire'] as bool? ?? false,
@@ -75,6 +76,7 @@ Map<String, dynamic> _$OrganizationModelToJson(OrganizationModel instance) =>
'logo': instance.logo,
'nombreMembres': instance.nombreMembres,
'nombreAdministrateurs': instance.nombreAdministrateurs,
'nombreEvenements': instance.nombreEvenements,
'budgetAnnuel': instance.budgetAnnuel,
'devise': instance.devise,
'cotisationObligatoire': instance.cotisationObligatoire,

View File

@@ -1,59 +1,20 @@
/// Repository pour la gestion des organisations
/// Interface avec l'API backend OrganizationResource
library organization_repository;
library organization_repository_impl;
import 'package:dio/dio.dart';
import 'package:injectable/injectable.dart';
import 'package:unionflow_mobile_apps/core/network/api_client.dart';
import '../../domain/repositories/organization_repository.dart';
import '../models/organization_model.dart';
/// Interface du repository des organisations
abstract class OrganizationRepository {
/// Récupère la liste des organisations avec pagination
Future<List<OrganizationModel>> getOrganizations({
int page = 0,
int size = 20,
String? recherche,
});
/// Récupère une organisation par son ID
Future<OrganizationModel?> getOrganizationById(String id);
/// Crée une nouvelle organisation
Future<OrganizationModel> createOrganization(OrganizationModel organization);
/// Met à jour une organisation
Future<OrganizationModel> updateOrganization(String id, OrganizationModel organization);
/// Supprime une organisation
Future<void> deleteOrganization(String id);
/// Active une organisation
Future<OrganizationModel> activateOrganization(String id);
/// Suspend une organisation
Future<OrganizationModel> suspendOrganization(String id);
/// Recherche avancée d'organisations
Future<List<OrganizationModel>> searchOrganizations({
String? nom,
TypeOrganization? type,
StatutOrganization? statut,
String? ville,
String? region,
String? pays,
int page = 0,
int size = 20,
});
/// Récupère les statistiques des organisations
Future<Map<String, dynamic>> getOrganizationsStats();
}
/// Implémentation du repository des organisations
class OrganizationRepositoryImpl implements OrganizationRepository {
final Dio _dio;
@LazySingleton(as: IOrganizationRepository)
class OrganizationRepositoryImpl implements IOrganizationRepository {
final ApiClient _apiClient;
static const String _baseUrl = '/api/organisations';
OrganizationRepositoryImpl(this._dio);
OrganizationRepositoryImpl(this._apiClient);
@override
Future<List<OrganizationModel>> getOrganizations({
@@ -71,12 +32,13 @@ class OrganizationRepositoryImpl implements OrganizationRepository {
queryParams['recherche'] = recherche;
}
final response = await _dio.get(
final response = await _apiClient.get(
_baseUrl,
queryParameters: queryParams,
);
if (response.statusCode == 200) {
// Le backend retourne directement une liste [...]
final List<dynamic> data = response.data as List<dynamic>;
return data
.map((json) => OrganizationModel.fromJson(json as Map<String, dynamic>))
@@ -91,10 +53,29 @@ class OrganizationRepositoryImpl implements OrganizationRepository {
}
}
@override
Future<List<OrganizationModel>> getMesOrganisations() async {
try {
const String path = '$_baseUrl/mes';
final response = await _apiClient.get(path);
if (response.statusCode == 200) {
final List<dynamic> data = response.data as List<dynamic>;
return data
.map((json) => OrganizationModel.fromJson(json as Map<String, dynamic>))
.toList();
}
throw Exception('Erreur lors de la récupération de mes organisations: ${response.statusCode}');
} on DioException catch (e) {
throw Exception('Erreur réseau lors de la récupération de mes organisations: ${e.message}');
} catch (e) {
throw Exception('Erreur inattendue lors de la récupération de mes organisations: $e');
}
}
@override
Future<OrganizationModel?> getOrganizationById(String id) async {
try {
final response = await _dio.get('$_baseUrl/$id');
final response = await _apiClient.get('$_baseUrl/$id');
if (response.statusCode == 200) {
return OrganizationModel.fromJson(response.data as Map<String, dynamic>);
@@ -116,7 +97,7 @@ class OrganizationRepositoryImpl implements OrganizationRepository {
@override
Future<OrganizationModel> createOrganization(OrganizationModel organization) async {
try {
final response = await _dio.post(
final response = await _apiClient.post(
_baseUrl,
data: organization.toJson(),
);
@@ -144,7 +125,7 @@ class OrganizationRepositoryImpl implements OrganizationRepository {
@override
Future<OrganizationModel> updateOrganization(String id, OrganizationModel organization) async {
try {
final response = await _dio.put(
final response = await _apiClient.put(
'$_baseUrl/$id',
data: organization.toJson(),
);
@@ -172,7 +153,7 @@ class OrganizationRepositoryImpl implements OrganizationRepository {
@override
Future<void> deleteOrganization(String id) async {
try {
final response = await _dio.delete('$_baseUrl/$id');
final response = await _apiClient.delete('$_baseUrl/$id');
if (response.statusCode != 200 && response.statusCode != 204) {
throw Exception('Erreur lors de la suppression de l\'organisation: ${response.statusCode}');
@@ -195,7 +176,7 @@ class OrganizationRepositoryImpl implements OrganizationRepository {
@override
Future<OrganizationModel> activateOrganization(String id) async {
try {
final response = await _dio.post('$_baseUrl/$id/activer');
final response = await _apiClient.post('$_baseUrl/$id/activer');
if (response.statusCode == 200) {
return OrganizationModel.fromJson(response.data as Map<String, dynamic>);
@@ -215,7 +196,7 @@ class OrganizationRepositoryImpl implements OrganizationRepository {
@override
Future<OrganizationModel> suspendOrganization(String id) async {
try {
final response = await _dio.post('$_baseUrl/$id/suspendre');
final response = await _apiClient.post('$_baseUrl/$id/suspendre');
if (response.statusCode == 200) {
return OrganizationModel.fromJson(response.data as Map<String, dynamic>);
@@ -256,12 +237,13 @@ class OrganizationRepositoryImpl implements OrganizationRepository {
if (region?.isNotEmpty == true) queryParams['region'] = region;
if (pays?.isNotEmpty == true) queryParams['pays'] = pays;
final response = await _dio.get(
final response = await _apiClient.get(
'$_baseUrl/recherche',
queryParameters: queryParams,
);
if (response.statusCode == 200) {
// Le backend retourne directement une liste [...]
final List<dynamic> data = response.data as List<dynamic>;
return data
.map((json) => OrganizationModel.fromJson(json as Map<String, dynamic>))
@@ -276,10 +258,62 @@ class OrganizationRepositoryImpl implements OrganizationRepository {
}
}
@override
Future<List<Map<String, dynamic>>> getOrganizationMembers(String organizationId) async {
try {
final response = await _apiClient.get('$_baseUrl/$organizationId/membres');
if (response.statusCode == 200) {
final List<dynamic> data = response.data as List<dynamic>;
return data.map((e) => Map<String, dynamic>.from(e as Map)).toList();
} else {
throw Exception('Erreur lors de la récupération des membres: ${response.statusCode}');
}
} on DioException catch (e) {
if (e.response?.statusCode == 404) {
throw Exception('Organisation non trouvée');
}
throw Exception('Erreur réseau lors de la récupération des membres: ${e.message}');
} catch (e) {
throw Exception('Erreur inattendue lors de la récupération des membres: $e');
}
}
@override
Future<OrganizationModel> updateOrganizationConfig(
String id,
Map<String, dynamic> config,
) async {
try {
final response = await _apiClient.put(
'$_baseUrl/$id/configuration',
data: config,
);
if (response.statusCode == 200) {
return OrganizationModel.fromJson(response.data as Map<String, dynamic>);
} else {
throw Exception('Erreur lors de la mise à jour de la configuration: ${response.statusCode}');
}
} on DioException catch (e) {
if (e.response?.statusCode == 404) {
throw Exception('Organisation non trouvée');
} else if (e.response?.statusCode == 400) {
final errorData = e.response?.data;
if (errorData is Map<String, dynamic> && errorData.containsKey('error')) {
throw Exception('Configuration invalide: ${errorData['error']}');
}
}
throw Exception('Erreur réseau lors de la mise à jour de la configuration: ${e.message}');
} catch (e) {
throw Exception('Erreur inattendue lors de la mise à jour de la configuration: $e');
}
}
@override
Future<Map<String, dynamic>> getOrganizationsStats() async {
try {
final response = await _dio.get('$_baseUrl/statistiques');
final response = await _apiClient.get('$_baseUrl/statistiques');
if (response.statusCode == 200) {
return response.data as Map<String, dynamic>;
@@ -292,5 +326,4 @@ class OrganizationRepositoryImpl implements OrganizationRepository {
throw Exception('Erreur inattendue lors de la récupération des statistiques: $e');
}
}
}

View File

@@ -1,13 +1,15 @@
/// Service pour la gestion des organisations
/// Couche de logique métier entre le repository et l'interface utilisateur
/// Helpers pour tri, filtrage local et recherche
library organization_service;
import 'package:injectable/injectable.dart';
import '../models/organization_model.dart';
import '../repositories/organization_repository.dart';
import '../../domain/repositories/organization_repository.dart';
/// Service de gestion des organisations
/// Service de gestion des organisations (helpers uniquement)
@injectable
class OrganizationService {
final OrganizationRepository _repository;
final IOrganizationRepository _repository;
OrganizationService(this._repository);
@@ -28,6 +30,15 @@ class OrganizationService {
}
}
/// Récupère les organisations du membre connecté (pour admin d'organisation)
Future<List<OrganizationModel>> getMesOrganisations() async {
try {
return await _repository.getMesOrganisations();
} catch (e) {
throw Exception('Erreur lors de la récupération de mes organisations: $e');
}
}
/// Récupère une organisation par son ID
Future<OrganizationModel?> getOrganizationById(String id) async {
if (id.isEmpty) {

View File

@@ -1,59 +0,0 @@
/// Configuration de l'injection de dépendances pour le module Organizations
library organizations_di;
import 'package:dio/dio.dart';
import 'package:get_it/get_it.dart';
import '../data/repositories/organization_repository.dart';
import '../data/services/organization_service.dart';
import '../bloc/organizations_bloc.dart';
/// Configuration des dépendances du module Organizations
class OrganizationsDI {
static final GetIt _getIt = GetIt.instance;
/// Enregistre toutes les dépendances du module
static void registerDependencies() {
// Repository
_getIt.registerLazySingleton<OrganizationRepository>(
() => OrganizationRepositoryImpl(_getIt<Dio>()),
);
// Service
_getIt.registerLazySingleton<OrganizationService>(
() => OrganizationService(_getIt<OrganizationRepository>()),
);
// BLoC - Factory pour permettre plusieurs instances
_getIt.registerFactory<OrganizationsBloc>(
() => OrganizationsBloc(_getIt<OrganizationService>()),
);
}
/// Nettoie les dépendances du module
static void unregisterDependencies() {
if (_getIt.isRegistered<OrganizationsBloc>()) {
_getIt.unregister<OrganizationsBloc>();
}
if (_getIt.isRegistered<OrganizationService>()) {
_getIt.unregister<OrganizationService>();
}
if (_getIt.isRegistered<OrganizationRepository>()) {
_getIt.unregister<OrganizationRepository>();
}
}
/// Obtient une instance du BLoC
static OrganizationsBloc getOrganizationsBloc() {
return _getIt<OrganizationsBloc>();
}
/// Obtient une instance du service
static OrganizationService getOrganizationService() {
return _getIt<OrganizationService>();
}
/// Obtient une instance du repository
static OrganizationRepository getOrganizationRepository() {
return _getIt<OrganizationRepository>();
}
}

View File

@@ -0,0 +1,61 @@
/// Interface du repository des organisations (Clean Architecture - Domain Layer)
library organization_repository;
import '../../data/models/organization_model.dart';
/// Interface du repository pour la gestion des organisations
/// Contrat défini dans la couche Domain, implémenté dans la couche Data
abstract class IOrganizationRepository {
/// Récupère la liste des organisations avec pagination
Future<List<OrganizationModel>> getOrganizations({
int page = 0,
int size = 20,
String? recherche,
});
/// Récupère les organisations du membre connecté (pour OrgAdmin)
Future<List<OrganizationModel>> getMesOrganisations();
/// Récupère une organisation par son ID
Future<OrganizationModel?> getOrganizationById(String id);
/// Crée une nouvelle organisation (SuperAdmin)
Future<OrganizationModel> createOrganization(OrganizationModel organization);
/// Met à jour une organisation (OrgAdmin)
Future<OrganizationModel> updateOrganization(String id, OrganizationModel organization);
/// Supprime une organisation (SuperAdmin)
Future<void> deleteOrganization(String id);
/// Active une organisation
Future<OrganizationModel> activateOrganization(String id);
/// Suspend une organisation
Future<OrganizationModel> suspendOrganization(String id);
/// Recherche avancée d'organisations
Future<List<OrganizationModel>> searchOrganizations({
String? nom,
TypeOrganization? type,
StatutOrganization? statut,
String? ville,
String? region,
String? pays,
int page = 0,
int size = 20,
});
/// Récupère les membres d'une organisation (GET /api/organisations/{id}/membres)
Future<List<Map<String, dynamic>>> getOrganizationMembers(String organizationId);
/// Met à jour la configuration d'une organisation (PUT /api/organisations/{id}/configuration)
/// Configuration: logo, couleurs, préférences, modules activés, etc.
Future<OrganizationModel> updateOrganizationConfig(
String id,
Map<String, dynamic> config,
);
/// Récupère les statistiques des organisations
Future<Map<String, dynamic>> getOrganizationsStats();
}

View File

@@ -0,0 +1,22 @@
/// Use Case: Créer une nouvelle organisation
library create_organization;
import 'package:injectable/injectable.dart';
import '../../data/models/organization_model.dart';
import '../repositories/organization_repository.dart';
/// Crée une nouvelle organisation (SuperAdmin uniquement)
@injectable
class CreateOrganization {
final IOrganizationRepository _repository;
CreateOrganization(this._repository);
/// Exécute le use case
/// [organization] : Modèle de l'organisation à créer
/// Retourne l'organisation créée avec son ID
/// Lève une exception en cas d'erreur (données invalides, conflit)
Future<OrganizationModel> call(OrganizationModel organization) async {
return _repository.createOrganization(organization);
}
}

View File

@@ -0,0 +1,21 @@
/// Use Case: Supprimer une organisation
library delete_organization;
import 'package:injectable/injectable.dart';
import '../repositories/organization_repository.dart';
/// Supprime une organisation (SuperAdmin uniquement)
@injectable
class DeleteOrganization {
final IOrganizationRepository _repository;
DeleteOrganization(this._repository);
/// Exécute le use case
/// [id] : Identifiant de l'organisation à supprimer
/// Lève une exception si organisation non trouvée ou suppression impossible
/// Note: Peut être un soft delete selon l'implémentation backend
Future<void> call(String id) async {
return _repository.deleteOrganization(id);
}
}

View File

@@ -0,0 +1,21 @@
/// Use Case: Récupérer une organisation par ID
library get_organization_by_id;
import 'package:injectable/injectable.dart';
import '../../data/models/organization_model.dart';
import '../repositories/organization_repository.dart';
/// Récupère le détail d'une organisation par son identifiant
@injectable
class GetOrganizationById {
final IOrganizationRepository _repository;
GetOrganizationById(this._repository);
/// Exécute le use case
/// [id] : Identifiant de l'organisation
/// Retourne l'organisation ou null si non trouvée
Future<OrganizationModel?> call(String id) async {
return _repository.getOrganizationById(id);
}
}

View File

@@ -0,0 +1,21 @@
/// Use Case: Récupérer les membres d'une organisation
library get_organization_members;
import 'package:injectable/injectable.dart';
import '../repositories/organization_repository.dart';
/// Récupère la liste des membres d'une organisation
@injectable
class GetOrganizationMembers {
final IOrganizationRepository _repository;
GetOrganizationMembers(this._repository);
/// Exécute le use case
/// [organizationId] : Identifiant de l'organisation
/// Retourne une liste de membres (Map avec id, nom, prenom, role, etc.)
/// Endpoint: GET /api/organisations/{id}/membres
Future<List<Map<String, dynamic>>> call(String organizationId) async {
return _repository.getOrganizationMembers(organizationId);
}
}

View File

@@ -0,0 +1,31 @@
/// Use Case: Récupérer la liste des organisations
library get_organizations;
import 'package:injectable/injectable.dart';
import '../../data/models/organization_model.dart';
import '../repositories/organization_repository.dart';
/// Récupère la liste paginée des organisations
@injectable
class GetOrganizations {
final IOrganizationRepository _repository;
GetOrganizations(this._repository);
/// Exécute le use case
/// [page] : Numéro de page (défaut: 0)
/// [size] : Taille de la page (défaut: 20)
/// [recherche] : Terme de recherche optionnel
/// Retourne une liste d'organisations
Future<List<OrganizationModel>> call({
int page = 0,
int size = 20,
String? recherche,
}) async {
return _repository.getOrganizations(
page: page,
size: size,
recherche: recherche,
);
}
}

View File

@@ -0,0 +1,23 @@
/// Use Case: Mettre à jour une organisation
library update_organization;
import 'package:injectable/injectable.dart';
import '../../data/models/organization_model.dart';
import '../repositories/organization_repository.dart';
/// Met à jour une organisation existante (OrgAdmin ou SuperAdmin)
@injectable
class UpdateOrganization {
final IOrganizationRepository _repository;
UpdateOrganization(this._repository);
/// Exécute le use case
/// [id] : Identifiant de l'organisation
/// [organization] : Modèle avec les données mises à jour
/// Retourne l'organisation mise à jour
/// Lève une exception si organisation non trouvée ou données invalides
Future<OrganizationModel> call(String id, OrganizationModel organization) async {
return _repository.updateOrganization(id, organization);
}
}

View File

@@ -0,0 +1,25 @@
/// Use Case: Mettre à jour la configuration d'une organisation
library update_organization_config;
import 'package:injectable/injectable.dart';
import '../../data/models/organization_model.dart';
import '../repositories/organization_repository.dart';
/// Met à jour la configuration spécifique d'une organisation
/// (logo, couleurs, modules activés, préférences, etc.)
@injectable
class UpdateOrganizationConfig {
final IOrganizationRepository _repository;
UpdateOrganizationConfig(this._repository);
/// Exécute le use case
/// [id] : Identifiant de l'organisation
/// [config] : Configuration à mettre à jour
/// Exemple: { "logo": "url", "couleurPrimaire": "#FF5733", "modulesActifs": ["finance", "events"] }
/// Retourne l'organisation avec la configuration mise à jour
/// Endpoint: PUT /api/organisations/{id}/configuration
Future<OrganizationModel> call(String id, Map<String, dynamic> config) async {
return _repository.updateOrganizationConfig(id, config);
}
}

View File

@@ -9,6 +9,7 @@ import '../../data/models/organization_model.dart';
import '../../bloc/organizations_bloc.dart';
import '../../bloc/organizations_event.dart';
import '../../bloc/organizations_state.dart';
import 'edit_organization_page.dart';
/// Page de détail d'une organisation avec design system cohérent
class OrganizationDetailPage extends StatefulWidget {
@@ -387,7 +388,7 @@ class _OrganizationDetailPageState extends State<OrganizationDetailPage> {
child: _buildStatItem(
icon: Icons.event,
label: 'Événements',
value: '0', // Nécessite endpoint stats par organisation
value: (organization.nombreEvenements ?? 0).toString(),
color: const Color(0xFF10B981),
),
),
@@ -590,7 +591,7 @@ class _OrganizationDetailPageState extends State<OrganizationDetailPage> {
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: () => _showEditDialog(),
onPressed: () => _showEditDialog(organization),
icon: const Icon(Icons.edit),
label: const Text('Modifier'),
style: ElevatedButton.styleFrom(
@@ -725,11 +726,36 @@ class _OrganizationDetailPageState extends State<OrganizationDetailPage> {
}
}
/// Affiche le dialog d'édition
void _showEditDialog() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Édition - À implémenter')),
);
/// Ouvre la page d'édition ou le dialog selon le contexte
void _showEditDialog([OrganizationModel? organization]) {
if (organization == null) {
final state = context.read<OrganizationsBloc>().state;
if (state is OrganizationLoaded) {
organization = state.organization;
}
}
if (organization == null || !context.mounted) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Chargement de l\'organisation en cours...')),
);
}
return;
}
final org = organization;
final bloc = context.read<OrganizationsBloc>();
Navigator.of(context).push<void>(
MaterialPageRoute<void>(
builder: (_) => BlocProvider.value(
value: bloc,
child: EditOrganizationPage(organization: org),
),
),
).then((_) {
if (context.mounted) {
bloc.add(LoadOrganizationById(widget.organizationId));
}
});
}
/// Affiche la confirmation de suppression

View File

@@ -3,10 +3,15 @@ library organisations_page_wrapper;
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../di/organizations_di.dart';
import 'package:get_it/get_it.dart';
import '../../../authentication/data/models/user_role.dart';
import '../../../authentication/presentation/bloc/auth_bloc.dart';
import '../../bloc/organizations_bloc.dart';
import '../../bloc/organizations_event.dart';
import 'organizations_page.dart';
final _getIt = GetIt.instance;
/// Wrapper qui fournit le BLoC pour la page des organisations
class OrganizationsPageWrapper extends StatelessWidget {
const OrganizationsPageWrapper({super.key});
@@ -14,7 +19,15 @@ class OrganizationsPageWrapper extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider<OrganizationsBloc>(
create: (context) => OrganizationsDI.getOrganizationsBloc(),
create: (context) {
final bloc = _getIt<OrganizationsBloc>();
// Admin d'organisation : ne charger que son/ses organisation(s)
final authState = context.read<AuthBloc>().state;
final useMesOnly = authState is AuthAuthenticated &&
authState.effectiveRole == UserRole.orgAdmin;
bloc.add(LoadOrganizations(useMesOnly: useMesOnly));
return bloc;
},
child: const OrganizationsPage(),
);
}

View File

@@ -209,7 +209,7 @@ class _CreateOrganizationDialogState extends State<CreateOrganizationDialog> {
labelText: 'Site web',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.language),
hintText: 'https://www.exemple.com',
hintText: 'https://',
),
keyboardType: TextInputType.url,
),

View File

@@ -308,7 +308,7 @@ class _EditOrganizationDialogState extends State<EditOrganizationDialog> {
labelText: 'Site web',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.language),
hintText: 'https://www.exemple.com',
hintText: 'https://',
),
keyboardType: TextInputType.url,
);