refactoring
This commit is contained in:
95
lib/features/organizations/bloc/org_types_bloc.dart
Normal file
95
lib/features/organizations/bloc/org_types_bloc.dart
Normal file
@@ -0,0 +1,95 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import '../domain/entities/type_reference_entity.dart';
|
||||
import '../domain/usecases/get_org_types.dart';
|
||||
import '../domain/usecases/create_org_type.dart';
|
||||
import '../domain/usecases/update_org_type.dart';
|
||||
import '../domain/usecases/delete_org_type.dart';
|
||||
|
||||
part 'org_types_event.dart';
|
||||
part 'org_types_state.dart';
|
||||
|
||||
@injectable
|
||||
class OrgTypesBloc extends Bloc<OrgTypesEvent, OrgTypesState> {
|
||||
final GetOrgTypes _getOrgTypes;
|
||||
final CreateOrgType _createOrgType;
|
||||
final UpdateOrgType _updateOrgType;
|
||||
final DeleteOrgType _deleteOrgType;
|
||||
|
||||
OrgTypesBloc(
|
||||
this._getOrgTypes,
|
||||
this._createOrgType,
|
||||
this._updateOrgType,
|
||||
this._deleteOrgType,
|
||||
) : super(const OrgTypesInitial()) {
|
||||
on<LoadOrgTypes>(_onLoad);
|
||||
on<CreateOrgTypeEvent>(_onCreate);
|
||||
on<UpdateOrgTypeEvent>(_onUpdate);
|
||||
on<DeleteOrgTypeEvent>(_onDelete);
|
||||
}
|
||||
|
||||
Future<void> _onLoad(LoadOrgTypes event, Emitter<OrgTypesState> emit) async {
|
||||
emit(const OrgTypesLoading());
|
||||
try {
|
||||
final types = await _getOrgTypes();
|
||||
emit(OrgTypesLoaded(types));
|
||||
} catch (e) {
|
||||
if (e is DioException && e.type == DioExceptionType.cancel) return;
|
||||
emit(OrgTypesError(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onCreate(CreateOrgTypeEvent event, Emitter<OrgTypesState> emit) async {
|
||||
final current = state is OrgTypesLoaded ? (state as OrgTypesLoaded).types : <TypeReferenceEntity>[];
|
||||
emit(OrgTypeOperating(List.from(current)));
|
||||
try {
|
||||
await _createOrgType(
|
||||
code: event.code,
|
||||
libelle: event.libelle,
|
||||
description: event.description,
|
||||
couleur: event.couleur,
|
||||
ordreAffichage: event.ordreAffichage,
|
||||
);
|
||||
final types = await _getOrgTypes();
|
||||
emit(OrgTypeSuccess(types: types, message: 'Type créé avec succès'));
|
||||
} catch (e) {
|
||||
if (e is DioException && e.type == DioExceptionType.cancel) return;
|
||||
emit(OrgTypeOperationError(types: List.from(current), message: e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onUpdate(UpdateOrgTypeEvent event, Emitter<OrgTypesState> emit) async {
|
||||
final current = state is OrgTypesLoaded ? (state as OrgTypesLoaded).types : <TypeReferenceEntity>[];
|
||||
emit(OrgTypeOperating(List.from(current)));
|
||||
try {
|
||||
await _updateOrgType(
|
||||
id: event.id,
|
||||
code: event.code,
|
||||
libelle: event.libelle,
|
||||
description: event.description,
|
||||
couleur: event.couleur,
|
||||
ordreAffichage: event.ordreAffichage,
|
||||
);
|
||||
final types = await _getOrgTypes();
|
||||
emit(OrgTypeSuccess(types: types, message: 'Type modifié avec succès'));
|
||||
} catch (e) {
|
||||
if (e is DioException && e.type == DioExceptionType.cancel) return;
|
||||
emit(OrgTypeOperationError(types: List.from(current), message: e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onDelete(DeleteOrgTypeEvent event, Emitter<OrgTypesState> emit) async {
|
||||
final current = state is OrgTypesLoaded ? (state as OrgTypesLoaded).types : <TypeReferenceEntity>[];
|
||||
emit(OrgTypeOperating(List.from(current)));
|
||||
try {
|
||||
await _deleteOrgType(event.id);
|
||||
final types = await _getOrgTypes();
|
||||
emit(OrgTypeSuccess(types: types, message: 'Type supprimé'));
|
||||
} catch (e) {
|
||||
if (e is DioException && e.type == DioExceptionType.cancel) return;
|
||||
emit(OrgTypeOperationError(types: List.from(current), message: e.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
58
lib/features/organizations/bloc/org_types_event.dart
Normal file
58
lib/features/organizations/bloc/org_types_event.dart
Normal file
@@ -0,0 +1,58 @@
|
||||
part of 'org_types_bloc.dart';
|
||||
|
||||
abstract class OrgTypesEvent extends Equatable {
|
||||
const OrgTypesEvent();
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
class LoadOrgTypes extends OrgTypesEvent {
|
||||
const LoadOrgTypes();
|
||||
}
|
||||
|
||||
class CreateOrgTypeEvent extends OrgTypesEvent {
|
||||
final String code;
|
||||
final String libelle;
|
||||
final String? description;
|
||||
final String? couleur;
|
||||
final int ordreAffichage;
|
||||
|
||||
const CreateOrgTypeEvent({
|
||||
required this.code,
|
||||
required this.libelle,
|
||||
this.description,
|
||||
this.couleur,
|
||||
this.ordreAffichage = 0,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [code, libelle, description, couleur, ordreAffichage];
|
||||
}
|
||||
|
||||
class UpdateOrgTypeEvent extends OrgTypesEvent {
|
||||
final String id;
|
||||
final String code;
|
||||
final String libelle;
|
||||
final String? description;
|
||||
final String? couleur;
|
||||
final int ordreAffichage;
|
||||
|
||||
const UpdateOrgTypeEvent({
|
||||
required this.id,
|
||||
required this.code,
|
||||
required this.libelle,
|
||||
this.description,
|
||||
this.couleur,
|
||||
this.ordreAffichage = 0,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [id, code, libelle, description, couleur, ordreAffichage];
|
||||
}
|
||||
|
||||
class DeleteOrgTypeEvent extends OrgTypesEvent {
|
||||
final String id;
|
||||
const DeleteOrgTypeEvent(this.id);
|
||||
@override
|
||||
List<Object?> get props => [id];
|
||||
}
|
||||
52
lib/features/organizations/bloc/org_types_state.dart
Normal file
52
lib/features/organizations/bloc/org_types_state.dart
Normal file
@@ -0,0 +1,52 @@
|
||||
part of 'org_types_bloc.dart';
|
||||
|
||||
abstract class OrgTypesState extends Equatable {
|
||||
const OrgTypesState();
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
class OrgTypesInitial extends OrgTypesState {
|
||||
const OrgTypesInitial();
|
||||
}
|
||||
|
||||
class OrgTypesLoading extends OrgTypesState {
|
||||
const OrgTypesLoading();
|
||||
}
|
||||
|
||||
class OrgTypesLoaded extends OrgTypesState {
|
||||
final List<TypeReferenceEntity> types;
|
||||
const OrgTypesLoaded(this.types);
|
||||
@override
|
||||
List<Object?> get props => [types];
|
||||
}
|
||||
|
||||
class OrgTypesError extends OrgTypesState {
|
||||
final String message;
|
||||
const OrgTypesError(this.message);
|
||||
@override
|
||||
List<Object?> get props => [message];
|
||||
}
|
||||
|
||||
class OrgTypeOperating extends OrgTypesState {
|
||||
final List<TypeReferenceEntity> types;
|
||||
const OrgTypeOperating(this.types);
|
||||
@override
|
||||
List<Object?> get props => [types];
|
||||
}
|
||||
|
||||
class OrgTypeSuccess extends OrgTypesState {
|
||||
final List<TypeReferenceEntity> types;
|
||||
final String message;
|
||||
const OrgTypeSuccess({required this.types, required this.message});
|
||||
@override
|
||||
List<Object?> get props => [types, message];
|
||||
}
|
||||
|
||||
class OrgTypeOperationError extends OrgTypesState {
|
||||
final List<TypeReferenceEntity> types;
|
||||
final String message;
|
||||
const OrgTypeOperationError({required this.types, required this.message});
|
||||
@override
|
||||
List<Object?> get props => [types, message];
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
/// BLoC pour la gestion des organisations (Clean Architecture)
|
||||
library organizations_bloc;
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import '../data/models/organization_model.dart';
|
||||
@@ -99,6 +100,7 @@ class OrganizationsBloc extends Bloc<OrganizationsEvent, OrganizationsState> {
|
||||
useMesOnly: event.useMesOnly,
|
||||
));
|
||||
} catch (e) {
|
||||
if (e is DioException && e.type == DioExceptionType.cancel) return;
|
||||
emit(OrganizationsError(
|
||||
'Erreur lors du chargement des organisations',
|
||||
details: e.toString(),
|
||||
@@ -144,6 +146,7 @@ class OrganizationsBloc extends Bloc<OrganizationsEvent, OrganizationsState> {
|
||||
currentPage: nextPage,
|
||||
));
|
||||
} catch (e) {
|
||||
if (e is DioException && e.type == DioExceptionType.cancel) return;
|
||||
emit(OrganizationsError(
|
||||
'Erreur lors du chargement de plus d\'organisations',
|
||||
details: e.toString(),
|
||||
@@ -204,6 +207,7 @@ class OrganizationsBloc extends Bloc<OrganizationsEvent, OrganizationsState> {
|
||||
));
|
||||
}
|
||||
} catch (e) {
|
||||
if (e is DioException && e.type == DioExceptionType.cancel) return;
|
||||
emit(OrganizationsError(
|
||||
'Erreur lors de la recherche',
|
||||
details: e.toString(),
|
||||
@@ -240,6 +244,7 @@ class OrganizationsBloc extends Bloc<OrganizationsEvent, OrganizationsState> {
|
||||
statusFilter: event.statut,
|
||||
));
|
||||
} catch (e) {
|
||||
if (e is DioException && e.type == DioExceptionType.cancel) return;
|
||||
emit(OrganizationsError(
|
||||
'Erreur lors de la recherche avancée',
|
||||
details: e.toString(),
|
||||
@@ -262,6 +267,7 @@ class OrganizationsBloc extends Bloc<OrganizationsEvent, OrganizationsState> {
|
||||
emit(OrganizationError('Organisation non trouvée', organizationId: event.id));
|
||||
}
|
||||
} catch (e) {
|
||||
if (e is DioException && e.type == DioExceptionType.cancel) return;
|
||||
emit(OrganizationError(
|
||||
'Erreur lors du chargement de l\'organisation',
|
||||
organizationId: event.id,
|
||||
@@ -279,12 +285,10 @@ class OrganizationsBloc extends Bloc<OrganizationsEvent, OrganizationsState> {
|
||||
try {
|
||||
final createdOrganization = await _createOrganization(event.organization);
|
||||
emit(OrganizationCreated(createdOrganization));
|
||||
|
||||
// Recharger la liste si elle était déjà chargée
|
||||
if (state is OrganizationsLoaded) {
|
||||
add(const RefreshOrganizations());
|
||||
}
|
||||
// Toujours recharger la liste après création
|
||||
add(const RefreshOrganizations());
|
||||
} catch (e) {
|
||||
if (e is DioException && e.type == DioExceptionType.cancel) return;
|
||||
emit(OrganizationsError(
|
||||
'Erreur lors de la création de l\'organisation',
|
||||
details: e.toString(),
|
||||
@@ -299,6 +303,8 @@ class OrganizationsBloc extends Bloc<OrganizationsEvent, OrganizationsState> {
|
||||
) async {
|
||||
emit(OrganizationUpdating(event.id));
|
||||
|
||||
// Capturer l'état avant tout emit
|
||||
final previousState = state;
|
||||
try {
|
||||
final updatedOrganization = await _updateOrganization(
|
||||
event.id,
|
||||
@@ -306,20 +312,20 @@ class OrganizationsBloc extends Bloc<OrganizationsEvent, OrganizationsState> {
|
||||
);
|
||||
emit(OrganizationUpdated(updatedOrganization));
|
||||
|
||||
// Mettre à jour la liste si elle était déjà chargée
|
||||
final currentState = state;
|
||||
if (currentState is OrganizationsLoaded) {
|
||||
final updatedList = currentState.organizations.map((org) {
|
||||
// Mettre à jour la liste en place
|
||||
if (previousState is OrganizationsLoaded) {
|
||||
final updatedList = previousState.organizations.map((org) {
|
||||
return org.id == event.id ? updatedOrganization : org;
|
||||
}).toList();
|
||||
|
||||
final filteredList = _applyCurrentFilters(updatedList, currentState);
|
||||
emit(currentState.copyWith(
|
||||
final filteredList = _applyCurrentFilters(updatedList, previousState);
|
||||
emit(previousState.copyWith(
|
||||
organizations: updatedList,
|
||||
filteredOrganizations: filteredList,
|
||||
));
|
||||
}
|
||||
} catch (e) {
|
||||
if (e is DioException && e.type == DioExceptionType.cancel) return;
|
||||
emit(OrganizationsError(
|
||||
'Erreur lors de la mise à jour de l\'organisation',
|
||||
details: e.toString(),
|
||||
@@ -334,21 +340,23 @@ class OrganizationsBloc extends Bloc<OrganizationsEvent, OrganizationsState> {
|
||||
) async {
|
||||
emit(OrganizationDeleting(event.id));
|
||||
|
||||
// Capturer l'état avant tout emit
|
||||
final previousState = state;
|
||||
try {
|
||||
await _deleteOrganization(event.id);
|
||||
emit(OrganizationDeleted(event.id));
|
||||
|
||||
// Retirer de la liste si elle était déjà chargée
|
||||
final currentState = state;
|
||||
if (currentState is OrganizationsLoaded) {
|
||||
final updatedList = currentState.organizations.where((org) => org.id != event.id).toList();
|
||||
final filteredList = _applyCurrentFilters(updatedList, currentState);
|
||||
emit(currentState.copyWith(
|
||||
// Retirer de la liste en place
|
||||
if (previousState is OrganizationsLoaded) {
|
||||
final updatedList = previousState.organizations.where((org) => org.id != event.id).toList();
|
||||
final filteredList = _applyCurrentFilters(updatedList, previousState);
|
||||
emit(previousState.copyWith(
|
||||
organizations: updatedList,
|
||||
filteredOrganizations: filteredList,
|
||||
));
|
||||
}
|
||||
} catch (e) {
|
||||
if (e is DioException && e.type == DioExceptionType.cancel) return;
|
||||
emit(OrganizationsError(
|
||||
'Erreur lors de la suppression de l\'organisation',
|
||||
details: e.toString(),
|
||||
@@ -361,26 +369,26 @@ class OrganizationsBloc extends Bloc<OrganizationsEvent, OrganizationsState> {
|
||||
ActivateOrganization event,
|
||||
Emitter<OrganizationsState> emit,
|
||||
) async {
|
||||
final previousState = state;
|
||||
emit(OrganizationActivating(event.id));
|
||||
|
||||
try {
|
||||
final activatedOrganization = await _repository.activateOrganization(event.id);
|
||||
emit(OrganizationActivated(activatedOrganization));
|
||||
|
||||
// Mettre à jour la liste si elle était déjà chargée
|
||||
final currentState = state;
|
||||
if (currentState is OrganizationsLoaded) {
|
||||
final updatedList = currentState.organizations.map((org) {
|
||||
if (previousState is OrganizationsLoaded) {
|
||||
final updatedList = previousState.organizations.map((org) {
|
||||
return org.id == event.id ? activatedOrganization : org;
|
||||
}).toList();
|
||||
|
||||
final filteredList = _applyCurrentFilters(updatedList, currentState);
|
||||
emit(currentState.copyWith(
|
||||
final filteredList = _applyCurrentFilters(updatedList, previousState);
|
||||
emit(previousState.copyWith(
|
||||
organizations: updatedList,
|
||||
filteredOrganizations: filteredList,
|
||||
));
|
||||
}
|
||||
} catch (e) {
|
||||
if (e is DioException && e.type == DioExceptionType.cancel) return;
|
||||
emit(OrganizationsError(
|
||||
'Erreur lors de l\'activation de l\'organisation',
|
||||
details: e.toString(),
|
||||
@@ -393,26 +401,26 @@ class OrganizationsBloc extends Bloc<OrganizationsEvent, OrganizationsState> {
|
||||
SuspendOrganization event,
|
||||
Emitter<OrganizationsState> emit,
|
||||
) async {
|
||||
final previousState = state;
|
||||
emit(OrganizationSuspending(event.id));
|
||||
|
||||
try {
|
||||
final suspendedOrganization = await _repository.suspendOrganization(event.id);
|
||||
emit(OrganizationSuspended(suspendedOrganization));
|
||||
|
||||
// Mettre à jour la liste si elle était déjà chargée
|
||||
final currentState = state;
|
||||
if (currentState is OrganizationsLoaded) {
|
||||
final updatedList = currentState.organizations.map((org) {
|
||||
if (previousState is OrganizationsLoaded) {
|
||||
final updatedList = previousState.organizations.map((org) {
|
||||
return org.id == event.id ? suspendedOrganization : org;
|
||||
}).toList();
|
||||
|
||||
final filteredList = _applyCurrentFilters(updatedList, currentState);
|
||||
emit(currentState.copyWith(
|
||||
final filteredList = _applyCurrentFilters(updatedList, previousState);
|
||||
emit(previousState.copyWith(
|
||||
organizations: updatedList,
|
||||
filteredOrganizations: filteredList,
|
||||
));
|
||||
}
|
||||
} catch (e) {
|
||||
if (e is DioException && e.type == DioExceptionType.cancel) return;
|
||||
emit(OrganizationsError(
|
||||
'Erreur lors de la suspension de l\'organisation',
|
||||
details: e.toString(),
|
||||
@@ -508,6 +516,7 @@ class OrganizationsBloc extends Bloc<OrganizationsEvent, OrganizationsState> {
|
||||
final stats = await _repository.getOrganizationsStats();
|
||||
emit(OrganizationsStatsLoaded(stats));
|
||||
} catch (e) {
|
||||
if (e is DioException && e.type == DioExceptionType.cancel) return;
|
||||
emit(const OrganizationsStatsError('Erreur lors du chargement des statistiques'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ class SearchOrganizations extends OrganizationsEvent {
|
||||
/// Événement pour recherche avancée
|
||||
class AdvancedSearchOrganizations extends OrganizationsEvent {
|
||||
final String? nom;
|
||||
final TypeOrganization? type;
|
||||
final String? type;
|
||||
final StatutOrganization? statut;
|
||||
final String? ville;
|
||||
final String? region;
|
||||
@@ -150,7 +150,7 @@ class FilterOrganizationsByStatus extends OrganizationsEvent {
|
||||
|
||||
/// Événement pour filtrer les organisations par type
|
||||
class FilterOrganizationsByType extends OrganizationsEvent {
|
||||
final TypeOrganization? type;
|
||||
final String? type;
|
||||
|
||||
const FilterOrganizationsByType(this.type);
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ class OrganizationsLoaded extends OrganizationsState {
|
||||
final int currentPage;
|
||||
final String? currentSearch;
|
||||
final StatutOrganization? statusFilter;
|
||||
final TypeOrganization? typeFilter;
|
||||
final String? typeFilter;
|
||||
final OrganizationSortType? sortType;
|
||||
final bool sortAscending;
|
||||
final Map<String, dynamic>? stats;
|
||||
@@ -72,7 +72,7 @@ class OrganizationsLoaded extends OrganizationsState {
|
||||
int? currentPage,
|
||||
String? currentSearch,
|
||||
StatutOrganization? statusFilter,
|
||||
TypeOrganization? typeFilter,
|
||||
String? typeFilter,
|
||||
OrganizationSortType? sortType,
|
||||
bool? sortAscending,
|
||||
Map<String, dynamic>? stats,
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import 'package:unionflow_mobile_apps/core/network/api_client.dart';
|
||||
import '../models/type_reference_model.dart';
|
||||
|
||||
@injectable
|
||||
class OrgTypesRemoteDataSource {
|
||||
final ApiClient _apiClient;
|
||||
static const _basePath = '/api/references/types-organisation';
|
||||
|
||||
const OrgTypesRemoteDataSource(this._apiClient);
|
||||
|
||||
Future<List<TypeReferenceModel>> getOrgTypes() async {
|
||||
final response = await _apiClient.get(_basePath);
|
||||
if (response.statusCode == 200) {
|
||||
final dynamic raw = response.data;
|
||||
final List<dynamic> data = raw is List<dynamic>
|
||||
? raw
|
||||
: ((raw as Map<String, dynamic>)['data'] as List<dynamic>? ?? []);
|
||||
return data
|
||||
.map((e) => TypeReferenceModel.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
throw DioException(
|
||||
requestOptions: response.requestOptions,
|
||||
response: response,
|
||||
message: 'Erreur lors de la récupération des types: ${response.statusCode}',
|
||||
);
|
||||
}
|
||||
|
||||
Future<TypeReferenceModel> createOrgType(Map<String, dynamic> body) async {
|
||||
final response = await _apiClient.post(_basePath, data: body);
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
return TypeReferenceModel.fromJson(response.data as Map<String, dynamic>);
|
||||
}
|
||||
throw DioException(
|
||||
requestOptions: response.requestOptions,
|
||||
response: response,
|
||||
message: 'Erreur lors de la création du type: ${response.statusCode}',
|
||||
);
|
||||
}
|
||||
|
||||
Future<TypeReferenceModel> updateOrgType(String id, Map<String, dynamic> body) async {
|
||||
final response = await _apiClient.put('$_basePath/$id', data: body);
|
||||
if (response.statusCode == 200) {
|
||||
return TypeReferenceModel.fromJson(response.data as Map<String, dynamic>);
|
||||
}
|
||||
throw DioException(
|
||||
requestOptions: response.requestOptions,
|
||||
response: response,
|
||||
message: 'Erreur lors de la mise à jour du type: ${response.statusCode}',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> deleteOrgType(String id) async {
|
||||
final response = await _apiClient.delete('$_basePath/$id');
|
||||
if (response.statusCode != 200 && response.statusCode != 204) {
|
||||
throw DioException(
|
||||
requestOptions: response.requestOptions,
|
||||
response: response,
|
||||
message: 'Erreur lors de la suppression du type: ${response.statusCode}',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,26 +7,6 @@ import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'organization_model.g.dart';
|
||||
|
||||
/// Énumération des types d'organisation
|
||||
enum TypeOrganization {
|
||||
@JsonValue('ASSOCIATION')
|
||||
association,
|
||||
@JsonValue('COOPERATIVE')
|
||||
cooperative,
|
||||
@JsonValue('LIONS_CLUB')
|
||||
lionsClub,
|
||||
@JsonValue('ENTREPRISE')
|
||||
entreprise,
|
||||
@JsonValue('ONG')
|
||||
ong,
|
||||
@JsonValue('FONDATION')
|
||||
fondation,
|
||||
@JsonValue('SYNDICAT')
|
||||
syndicat,
|
||||
@JsonValue('AUTRE')
|
||||
autre,
|
||||
}
|
||||
|
||||
/// Énumération des statuts d'organisation
|
||||
enum StatutOrganization {
|
||||
@JsonValue('ACTIVE')
|
||||
@@ -41,51 +21,6 @@ enum StatutOrganization {
|
||||
enCreation,
|
||||
}
|
||||
|
||||
/// Extension pour les types d'organisation
|
||||
extension TypeOrganizationExtension on TypeOrganization {
|
||||
String get displayName {
|
||||
switch (this) {
|
||||
case TypeOrganization.association:
|
||||
return 'Association';
|
||||
case TypeOrganization.cooperative:
|
||||
return 'Coopérative';
|
||||
case TypeOrganization.lionsClub:
|
||||
return 'Lions Club';
|
||||
case TypeOrganization.entreprise:
|
||||
return 'Entreprise';
|
||||
case TypeOrganization.ong:
|
||||
return 'ONG';
|
||||
case TypeOrganization.fondation:
|
||||
return 'Fondation';
|
||||
case TypeOrganization.syndicat:
|
||||
return 'Syndicat';
|
||||
case TypeOrganization.autre:
|
||||
return 'Autre';
|
||||
}
|
||||
}
|
||||
|
||||
String get icon {
|
||||
switch (this) {
|
||||
case TypeOrganization.association:
|
||||
return '🏛️';
|
||||
case TypeOrganization.cooperative:
|
||||
return '🤝';
|
||||
case TypeOrganization.lionsClub:
|
||||
return '🦁';
|
||||
case TypeOrganization.entreprise:
|
||||
return '🏢';
|
||||
case TypeOrganization.ong:
|
||||
return '🌍';
|
||||
case TypeOrganization.fondation:
|
||||
return '🏛️';
|
||||
case TypeOrganization.syndicat:
|
||||
return '⚖️';
|
||||
case TypeOrganization.autre:
|
||||
return '📋';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension pour les statuts d'organisation
|
||||
extension StatutOrganizationExtension on StatutOrganization {
|
||||
String get displayName {
|
||||
@@ -158,9 +93,9 @@ class OrganizationModel extends Equatable {
|
||||
/// Nom court ou sigle
|
||||
final String? nomCourt;
|
||||
|
||||
/// Type d'organisation
|
||||
/// Type d'organisation (code dynamique depuis /api/references/types-organisation)
|
||||
@JsonKey(name: 'typeOrganisation')
|
||||
final TypeOrganization typeOrganisation;
|
||||
final String typeOrganisation;
|
||||
|
||||
/// Statut de l'organisation
|
||||
final StatutOrganization statut;
|
||||
@@ -182,10 +117,22 @@ class OrganizationModel extends Equatable {
|
||||
/// Téléphone
|
||||
final String? telephone;
|
||||
|
||||
/// Téléphone secondaire
|
||||
@JsonKey(name: 'telephoneSecondaire')
|
||||
final String? telephoneSecondaire;
|
||||
|
||||
/// Email secondaire
|
||||
@JsonKey(name: 'emailSecondaire')
|
||||
final String? emailSecondaire;
|
||||
|
||||
/// Site web
|
||||
@JsonKey(name: 'siteWeb')
|
||||
final String? siteWeb;
|
||||
|
||||
/// Réseaux sociaux (JSON string)
|
||||
@JsonKey(name: 'reseauxSociaux')
|
||||
final String? reseauxSociaux;
|
||||
|
||||
/// Adresse complète
|
||||
final String? adresse;
|
||||
|
||||
@@ -245,6 +192,17 @@ class OrganizationModel extends Equatable {
|
||||
/// Partenaires
|
||||
final String? partenaires;
|
||||
|
||||
/// Notes internes
|
||||
final String? notes;
|
||||
|
||||
/// Libellé résolu du type d'organisation (lecture seule, depuis la réponse API)
|
||||
@JsonKey(name: 'typeOrganisationLibelle')
|
||||
final String? typeOrganisationLibelle;
|
||||
|
||||
/// Libellé résolu du statut (lecture seule, depuis la réponse API)
|
||||
@JsonKey(name: 'statutLibelle')
|
||||
final String? statutLibelle;
|
||||
|
||||
/// Organisation publique
|
||||
@JsonKey(name: 'organisationPublique')
|
||||
final bool organisationPublique;
|
||||
@@ -268,14 +226,17 @@ class OrganizationModel extends Equatable {
|
||||
this.id,
|
||||
required this.nom,
|
||||
this.nomCourt,
|
||||
this.typeOrganisation = TypeOrganization.association,
|
||||
this.typeOrganisation = 'ASSOCIATION',
|
||||
this.statut = StatutOrganization.active,
|
||||
this.description,
|
||||
this.dateFondation,
|
||||
this.numeroEnregistrement,
|
||||
this.email,
|
||||
this.telephone,
|
||||
this.telephoneSecondaire,
|
||||
this.emailSecondaire,
|
||||
this.siteWeb,
|
||||
this.reseauxSociaux,
|
||||
this.adresse,
|
||||
this.ville,
|
||||
this.codePostal,
|
||||
@@ -293,6 +254,9 @@ class OrganizationModel extends Equatable {
|
||||
this.activitesPrincipales,
|
||||
this.certifications,
|
||||
this.partenaires,
|
||||
this.notes,
|
||||
this.typeOrganisationLibelle,
|
||||
this.statutLibelle,
|
||||
this.organisationPublique = true,
|
||||
this.accepteNouveauxMembres = true,
|
||||
this.dateCreation,
|
||||
@@ -312,14 +276,17 @@ class OrganizationModel extends Equatable {
|
||||
String? id,
|
||||
String? nom,
|
||||
String? nomCourt,
|
||||
TypeOrganization? typeOrganisation,
|
||||
String? typeOrganisation,
|
||||
StatutOrganization? statut,
|
||||
String? description,
|
||||
DateTime? dateFondation,
|
||||
String? numeroEnregistrement,
|
||||
String? email,
|
||||
String? telephone,
|
||||
String? telephoneSecondaire,
|
||||
String? emailSecondaire,
|
||||
String? siteWeb,
|
||||
String? reseauxSociaux,
|
||||
String? adresse,
|
||||
String? ville,
|
||||
String? codePostal,
|
||||
@@ -337,6 +304,9 @@ class OrganizationModel extends Equatable {
|
||||
String? activitesPrincipales,
|
||||
String? certifications,
|
||||
String? partenaires,
|
||||
String? notes,
|
||||
String? typeOrganisationLibelle,
|
||||
String? statutLibelle,
|
||||
bool? organisationPublique,
|
||||
bool? accepteNouveauxMembres,
|
||||
DateTime? dateCreation,
|
||||
@@ -354,7 +324,10 @@ class OrganizationModel extends Equatable {
|
||||
numeroEnregistrement: numeroEnregistrement ?? this.numeroEnregistrement,
|
||||
email: email ?? this.email,
|
||||
telephone: telephone ?? this.telephone,
|
||||
telephoneSecondaire: telephoneSecondaire ?? this.telephoneSecondaire,
|
||||
emailSecondaire: emailSecondaire ?? this.emailSecondaire,
|
||||
siteWeb: siteWeb ?? this.siteWeb,
|
||||
reseauxSociaux: reseauxSociaux ?? this.reseauxSociaux,
|
||||
adresse: adresse ?? this.adresse,
|
||||
ville: ville ?? this.ville,
|
||||
codePostal: codePostal ?? this.codePostal,
|
||||
@@ -372,6 +345,9 @@ class OrganizationModel extends Equatable {
|
||||
activitesPrincipales: activitesPrincipales ?? this.activitesPrincipales,
|
||||
certifications: certifications ?? this.certifications,
|
||||
partenaires: partenaires ?? this.partenaires,
|
||||
notes: notes ?? this.notes,
|
||||
typeOrganisationLibelle: typeOrganisationLibelle ?? this.typeOrganisationLibelle,
|
||||
statutLibelle: statutLibelle ?? this.statutLibelle,
|
||||
organisationPublique: organisationPublique ?? this.organisationPublique,
|
||||
accepteNouveauxMembres: accepteNouveauxMembres ?? this.accepteNouveauxMembres,
|
||||
dateCreation: dateCreation ?? this.dateCreation,
|
||||
@@ -412,7 +388,10 @@ class OrganizationModel extends Equatable {
|
||||
numeroEnregistrement,
|
||||
email,
|
||||
telephone,
|
||||
telephoneSecondaire,
|
||||
emailSecondaire,
|
||||
siteWeb,
|
||||
reseauxSociaux,
|
||||
adresse,
|
||||
ville,
|
||||
codePostal,
|
||||
@@ -430,6 +409,9 @@ class OrganizationModel extends Equatable {
|
||||
activitesPrincipales,
|
||||
certifications,
|
||||
partenaires,
|
||||
notes,
|
||||
typeOrganisationLibelle,
|
||||
statutLibelle,
|
||||
organisationPublique,
|
||||
accepteNouveauxMembres,
|
||||
dateCreation,
|
||||
|
||||
@@ -11,9 +11,7 @@ OrganizationModel _$OrganizationModelFromJson(Map<String, dynamic> json) =>
|
||||
id: json['id'] as String?,
|
||||
nom: json['nom'] as String,
|
||||
nomCourt: json['nomCourt'] as String?,
|
||||
typeOrganisation: $enumDecodeNullable(
|
||||
_$TypeOrganizationEnumMap, json['typeOrganisation']) ??
|
||||
TypeOrganization.association,
|
||||
typeOrganisation: json['typeOrganisation'] as String? ?? 'ASSOCIATION',
|
||||
statut:
|
||||
$enumDecodeNullable(_$StatutOrganizationEnumMap, json['statut']) ??
|
||||
StatutOrganization.active,
|
||||
@@ -24,7 +22,10 @@ OrganizationModel _$OrganizationModelFromJson(Map<String, dynamic> json) =>
|
||||
numeroEnregistrement: json['numeroEnregistrement'] as String?,
|
||||
email: json['email'] as String?,
|
||||
telephone: json['telephone'] as String?,
|
||||
telephoneSecondaire: json['telephoneSecondaire'] as String?,
|
||||
emailSecondaire: json['emailSecondaire'] as String?,
|
||||
siteWeb: json['siteWeb'] as String?,
|
||||
reseauxSociaux: json['reseauxSociaux'] as String?,
|
||||
adresse: json['adresse'] as String?,
|
||||
ville: json['ville'] as String?,
|
||||
codePostal: json['codePostal'] as String?,
|
||||
@@ -44,6 +45,9 @@ OrganizationModel _$OrganizationModelFromJson(Map<String, dynamic> json) =>
|
||||
activitesPrincipales: json['activitesPrincipales'] as String?,
|
||||
certifications: json['certifications'] as String?,
|
||||
partenaires: json['partenaires'] as String?,
|
||||
notes: json['notes'] as String?,
|
||||
typeOrganisationLibelle: json['typeOrganisationLibelle'] as String?,
|
||||
statutLibelle: json['statutLibelle'] as String?,
|
||||
organisationPublique: json['organisationPublique'] as bool? ?? true,
|
||||
accepteNouveauxMembres: json['accepteNouveauxMembres'] as bool? ?? true,
|
||||
dateCreation: json['dateCreation'] == null
|
||||
@@ -60,14 +64,17 @@ Map<String, dynamic> _$OrganizationModelToJson(OrganizationModel instance) =>
|
||||
'id': instance.id,
|
||||
'nom': instance.nom,
|
||||
'nomCourt': instance.nomCourt,
|
||||
'typeOrganisation': _$TypeOrganizationEnumMap[instance.typeOrganisation]!,
|
||||
'typeOrganisation': instance.typeOrganisation,
|
||||
'statut': _$StatutOrganizationEnumMap[instance.statut]!,
|
||||
'description': instance.description,
|
||||
'dateFondation': instance.dateFondation?.toIso8601String(),
|
||||
'numeroEnregistrement': instance.numeroEnregistrement,
|
||||
'email': instance.email,
|
||||
'telephone': instance.telephone,
|
||||
'telephoneSecondaire': instance.telephoneSecondaire,
|
||||
'emailSecondaire': instance.emailSecondaire,
|
||||
'siteWeb': instance.siteWeb,
|
||||
'reseauxSociaux': instance.reseauxSociaux,
|
||||
'adresse': instance.adresse,
|
||||
'ville': instance.ville,
|
||||
'codePostal': instance.codePostal,
|
||||
@@ -85,6 +92,9 @@ Map<String, dynamic> _$OrganizationModelToJson(OrganizationModel instance) =>
|
||||
'activitesPrincipales': instance.activitesPrincipales,
|
||||
'certifications': instance.certifications,
|
||||
'partenaires': instance.partenaires,
|
||||
'notes': instance.notes,
|
||||
'typeOrganisationLibelle': instance.typeOrganisationLibelle,
|
||||
'statutLibelle': instance.statutLibelle,
|
||||
'organisationPublique': instance.organisationPublique,
|
||||
'accepteNouveauxMembres': instance.accepteNouveauxMembres,
|
||||
'dateCreation': instance.dateCreation?.toIso8601String(),
|
||||
@@ -92,17 +102,6 @@ Map<String, dynamic> _$OrganizationModelToJson(OrganizationModel instance) =>
|
||||
'actif': instance.actif,
|
||||
};
|
||||
|
||||
const _$TypeOrganizationEnumMap = {
|
||||
TypeOrganization.association: 'ASSOCIATION',
|
||||
TypeOrganization.cooperative: 'COOPERATIVE',
|
||||
TypeOrganization.lionsClub: 'LIONS_CLUB',
|
||||
TypeOrganization.entreprise: 'ENTREPRISE',
|
||||
TypeOrganization.ong: 'ONG',
|
||||
TypeOrganization.fondation: 'FONDATION',
|
||||
TypeOrganization.syndicat: 'SYNDICAT',
|
||||
TypeOrganization.autre: 'AUTRE',
|
||||
};
|
||||
|
||||
const _$StatutOrganizationEnumMap = {
|
||||
StatutOrganization.active: 'ACTIVE',
|
||||
StatutOrganization.inactive: 'INACTIVE',
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import '../../domain/entities/type_reference_entity.dart';
|
||||
|
||||
part 'type_reference_model.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class TypeReferenceModel {
|
||||
final String id;
|
||||
final String domaine;
|
||||
final String code;
|
||||
final String libelle;
|
||||
final String? description;
|
||||
final String? icone;
|
||||
final String? couleur;
|
||||
final String? severity;
|
||||
@JsonKey(defaultValue: 0)
|
||||
final int ordreAffichage;
|
||||
@JsonKey(defaultValue: false)
|
||||
final bool estDefaut;
|
||||
@JsonKey(defaultValue: false)
|
||||
final bool estSysteme;
|
||||
@JsonKey(defaultValue: true)
|
||||
final bool actif;
|
||||
|
||||
const TypeReferenceModel({
|
||||
required this.id,
|
||||
required this.domaine,
|
||||
required this.code,
|
||||
required this.libelle,
|
||||
this.description,
|
||||
this.icone,
|
||||
this.couleur,
|
||||
this.severity,
|
||||
this.ordreAffichage = 0,
|
||||
this.estDefaut = false,
|
||||
this.estSysteme = false,
|
||||
this.actif = true,
|
||||
});
|
||||
|
||||
factory TypeReferenceModel.fromJson(Map<String, dynamic> json) =>
|
||||
_$TypeReferenceModelFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$TypeReferenceModelToJson(this);
|
||||
|
||||
TypeReferenceEntity toEntity() => TypeReferenceEntity(
|
||||
id: id,
|
||||
domaine: domaine,
|
||||
code: code,
|
||||
libelle: libelle,
|
||||
description: description,
|
||||
icone: icone,
|
||||
couleur: couleur,
|
||||
severity: severity,
|
||||
ordreAffichage: ordreAffichage,
|
||||
estDefaut: estDefaut,
|
||||
estSysteme: estSysteme,
|
||||
actif: actif,
|
||||
);
|
||||
|
||||
factory TypeReferenceModel.fromEntity(TypeReferenceEntity entity) =>
|
||||
TypeReferenceModel(
|
||||
id: entity.id,
|
||||
domaine: entity.domaine,
|
||||
code: entity.code,
|
||||
libelle: entity.libelle,
|
||||
description: entity.description,
|
||||
icone: entity.icone,
|
||||
couleur: entity.couleur,
|
||||
severity: entity.severity,
|
||||
ordreAffichage: entity.ordreAffichage,
|
||||
estDefaut: entity.estDefaut,
|
||||
estSysteme: entity.estSysteme,
|
||||
actif: entity.actif,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'type_reference_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
TypeReferenceModel _$TypeReferenceModelFromJson(Map<String, dynamic> json) =>
|
||||
TypeReferenceModel(
|
||||
id: json['id'] as String,
|
||||
domaine: json['domaine'] as String,
|
||||
code: json['code'] as String,
|
||||
libelle: json['libelle'] as String,
|
||||
description: json['description'] as String?,
|
||||
icone: json['icone'] as String?,
|
||||
couleur: json['couleur'] as String?,
|
||||
severity: json['severity'] as String?,
|
||||
ordreAffichage: (json['ordreAffichage'] as num?)?.toInt() ?? 0,
|
||||
estDefaut: json['estDefaut'] as bool? ?? false,
|
||||
estSysteme: json['estSysteme'] as bool? ?? false,
|
||||
actif: json['actif'] as bool? ?? true,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$TypeReferenceModelToJson(TypeReferenceModel instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'domaine': instance.domaine,
|
||||
'code': instance.code,
|
||||
'libelle': instance.libelle,
|
||||
'description': instance.description,
|
||||
'icone': instance.icone,
|
||||
'couleur': instance.couleur,
|
||||
'severity': instance.severity,
|
||||
'ordreAffichage': instance.ordreAffichage,
|
||||
'estDefaut': instance.estDefaut,
|
||||
'estSysteme': instance.estSysteme,
|
||||
'actif': instance.actif,
|
||||
};
|
||||
@@ -0,0 +1,81 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:injectable/injectable.dart';
|
||||
import '../../domain/entities/type_reference_entity.dart';
|
||||
import '../../domain/repositories/org_types_repository.dart';
|
||||
import '../datasources/org_types_remote_datasource.dart';
|
||||
|
||||
@LazySingleton(as: IOrgTypesRepository)
|
||||
class OrgTypesRepositoryImpl implements IOrgTypesRepository {
|
||||
final OrgTypesRemoteDataSource _dataSource;
|
||||
|
||||
const OrgTypesRepositoryImpl(this._dataSource);
|
||||
|
||||
@override
|
||||
Future<List<TypeReferenceEntity>> getOrgTypes() async {
|
||||
try {
|
||||
final models = await _dataSource.getOrgTypes();
|
||||
return models.map((m) => m.toEntity()).toList();
|
||||
} on DioException catch (e) {
|
||||
throw Exception('Erreur réseau: ${e.message}');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<TypeReferenceEntity> createOrgType({
|
||||
required String code,
|
||||
required String libelle,
|
||||
String? description,
|
||||
String? couleur,
|
||||
int ordreAffichage = 0,
|
||||
}) async {
|
||||
try {
|
||||
final model = await _dataSource.createOrgType({
|
||||
'domaine': 'TYPE_ORGANISATION',
|
||||
'code': code.toUpperCase().replaceAll(' ', '_'),
|
||||
'libelle': libelle,
|
||||
if (description != null) 'description': description,
|
||||
if (couleur != null) 'couleur': couleur,
|
||||
'ordreAffichage': ordreAffichage,
|
||||
'estDefaut': false,
|
||||
'estSysteme': false,
|
||||
'actif': true,
|
||||
});
|
||||
return model.toEntity();
|
||||
} on DioException catch (e) {
|
||||
throw Exception('Erreur réseau: ${e.message}');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<TypeReferenceEntity> updateOrgType({
|
||||
required String id,
|
||||
required String code,
|
||||
required String libelle,
|
||||
String? description,
|
||||
String? couleur,
|
||||
int ordreAffichage = 0,
|
||||
}) async {
|
||||
try {
|
||||
final model = await _dataSource.updateOrgType(id, {
|
||||
'domaine': 'TYPE_ORGANISATION',
|
||||
'code': code.toUpperCase().replaceAll(' ', '_'),
|
||||
'libelle': libelle,
|
||||
if (description != null) 'description': description,
|
||||
if (couleur != null) 'couleur': couleur,
|
||||
'ordreAffichage': ordreAffichage,
|
||||
});
|
||||
return model.toEntity();
|
||||
} on DioException catch (e) {
|
||||
throw Exception('Erreur réseau: ${e.message}');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteOrgType(String id) async {
|
||||
try {
|
||||
await _dataSource.deleteOrgType(id);
|
||||
} on DioException catch (e) {
|
||||
throw Exception('Erreur réseau: ${e.message}');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,8 +38,9 @@ class OrganizationRepositoryImpl implements IOrganizationRepository {
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
// Le backend retourne directement une liste [...]
|
||||
final List<dynamic> data = response.data as List<dynamic>;
|
||||
// Le backend retourne une réponse paginée {"data":[...],"total":0,...}
|
||||
final responseData = response.data as Map<String, dynamic>;
|
||||
final List<dynamic> data = (responseData['data'] as List<dynamic>?) ?? [];
|
||||
return data
|
||||
.map((json) => OrganizationModel.fromJson(json as Map<String, dynamic>))
|
||||
.toList();
|
||||
@@ -47,6 +48,7 @@ class OrganizationRepositoryImpl implements IOrganizationRepository {
|
||||
throw Exception('Erreur lors de la récupération des organisations: ${response.statusCode}');
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
if (e.type == DioExceptionType.cancel) rethrow;
|
||||
throw Exception('Erreur réseau lors de la récupération des organisations: ${e.message}');
|
||||
} catch (e) {
|
||||
throw Exception('Erreur inattendue lors de la récupération des organisations: $e');
|
||||
@@ -59,13 +61,17 @@ class OrganizationRepositoryImpl implements IOrganizationRepository {
|
||||
const String path = '$_baseUrl/mes';
|
||||
final response = await _apiClient.get(path);
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> data = response.data as List<dynamic>;
|
||||
final dynamic raw = response.data;
|
||||
final List<dynamic> data = raw is List<dynamic>
|
||||
? raw
|
||||
: ((raw as Map<String, dynamic>)['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) {
|
||||
if (e.type == DioExceptionType.cancel) rethrow;
|
||||
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');
|
||||
@@ -85,6 +91,7 @@ class OrganizationRepositoryImpl implements IOrganizationRepository {
|
||||
throw Exception('Erreur lors de la récupération de l\'organisation: ${response.statusCode}');
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
if (e.type == DioExceptionType.cancel) rethrow;
|
||||
if (e.response?.statusCode == 404) {
|
||||
return null;
|
||||
}
|
||||
@@ -108,6 +115,7 @@ class OrganizationRepositoryImpl implements IOrganizationRepository {
|
||||
throw Exception('Erreur lors de la création de l\'organisation: ${response.statusCode}');
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
if (e.type == DioExceptionType.cancel) rethrow;
|
||||
if (e.response?.statusCode == 400) {
|
||||
final errorData = e.response?.data;
|
||||
if (errorData is Map<String, dynamic> && errorData.containsKey('error')) {
|
||||
@@ -136,6 +144,7 @@ class OrganizationRepositoryImpl implements IOrganizationRepository {
|
||||
throw Exception('Erreur lors de la mise à jour de l\'organisation: ${response.statusCode}');
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
if (e.type == DioExceptionType.cancel) rethrow;
|
||||
if (e.response?.statusCode == 404) {
|
||||
throw Exception('Organisation non trouvée');
|
||||
} else if (e.response?.statusCode == 400) {
|
||||
@@ -159,6 +168,7 @@ class OrganizationRepositoryImpl implements IOrganizationRepository {
|
||||
throw Exception('Erreur lors de la suppression de l\'organisation: ${response.statusCode}');
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
if (e.type == DioExceptionType.cancel) rethrow;
|
||||
if (e.response?.statusCode == 404) {
|
||||
throw Exception('Organisation non trouvée');
|
||||
} else if (e.response?.statusCode == 400) {
|
||||
@@ -184,6 +194,7 @@ class OrganizationRepositoryImpl implements IOrganizationRepository {
|
||||
throw Exception('Erreur lors de l\'activation de l\'organisation: ${response.statusCode}');
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
if (e.type == DioExceptionType.cancel) rethrow;
|
||||
if (e.response?.statusCode == 404) {
|
||||
throw Exception('Organisation non trouvée');
|
||||
}
|
||||
@@ -204,6 +215,7 @@ class OrganizationRepositoryImpl implements IOrganizationRepository {
|
||||
throw Exception('Erreur lors de la suspension de l\'organisation: ${response.statusCode}');
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
if (e.type == DioExceptionType.cancel) rethrow;
|
||||
if (e.response?.statusCode == 404) {
|
||||
throw Exception('Organisation non trouvée');
|
||||
}
|
||||
@@ -216,7 +228,7 @@ class OrganizationRepositoryImpl implements IOrganizationRepository {
|
||||
@override
|
||||
Future<List<OrganizationModel>> searchOrganizations({
|
||||
String? nom,
|
||||
TypeOrganization? type,
|
||||
String? type,
|
||||
StatutOrganization? statut,
|
||||
String? ville,
|
||||
String? region,
|
||||
@@ -231,7 +243,7 @@ class OrganizationRepositoryImpl implements IOrganizationRepository {
|
||||
};
|
||||
|
||||
if (nom?.isNotEmpty == true) queryParams['nom'] = nom;
|
||||
if (type != null) queryParams['type'] = type.name.toUpperCase();
|
||||
if (type != null) queryParams['type'] = type.toUpperCase();
|
||||
if (statut != null) queryParams['statut'] = statut.name.toUpperCase();
|
||||
if (ville?.isNotEmpty == true) queryParams['ville'] = ville;
|
||||
if (region?.isNotEmpty == true) queryParams['region'] = region;
|
||||
@@ -243,8 +255,8 @@ class OrganizationRepositoryImpl implements IOrganizationRepository {
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
// Le backend retourne directement une liste [...]
|
||||
final List<dynamic> data = response.data as List<dynamic>;
|
||||
final responseData = response.data as Map<String, dynamic>;
|
||||
final List<dynamic> data = (responseData['data'] as List<dynamic>?) ?? [];
|
||||
return data
|
||||
.map((json) => OrganizationModel.fromJson(json as Map<String, dynamic>))
|
||||
.toList();
|
||||
@@ -252,6 +264,7 @@ class OrganizationRepositoryImpl implements IOrganizationRepository {
|
||||
throw Exception('Erreur lors de la recherche d\'organisations: ${response.statusCode}');
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
if (e.type == DioExceptionType.cancel) rethrow;
|
||||
throw Exception('Erreur réseau lors de la recherche d\'organisations: ${e.message}');
|
||||
} catch (e) {
|
||||
throw Exception('Erreur inattendue lors de la recherche d\'organisations: $e');
|
||||
@@ -264,12 +277,16 @@ class OrganizationRepositoryImpl implements IOrganizationRepository {
|
||||
final response = await _apiClient.get('$_baseUrl/$organizationId/membres');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> data = response.data as List<dynamic>;
|
||||
final dynamic raw = response.data;
|
||||
final List<dynamic> data = raw is List<dynamic>
|
||||
? raw
|
||||
: ((raw as Map<String, dynamic>)['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.type == DioExceptionType.cancel) rethrow;
|
||||
if (e.response?.statusCode == 404) {
|
||||
throw Exception('Organisation non trouvée');
|
||||
}
|
||||
@@ -296,6 +313,7 @@ class OrganizationRepositoryImpl implements IOrganizationRepository {
|
||||
throw Exception('Erreur lors de la mise à jour de la configuration: ${response.statusCode}');
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
if (e.type == DioExceptionType.cancel) rethrow;
|
||||
if (e.response?.statusCode == 404) {
|
||||
throw Exception('Organisation non trouvée');
|
||||
} else if (e.response?.statusCode == 400) {
|
||||
@@ -321,6 +339,7 @@ class OrganizationRepositoryImpl implements IOrganizationRepository {
|
||||
throw Exception('Erreur lors de la récupération des statistiques: ${response.statusCode}');
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
if (e.type == DioExceptionType.cancel) rethrow;
|
||||
throw Exception('Erreur réseau lors de la récupération des statistiques: ${e.message}');
|
||||
} catch (e) {
|
||||
throw Exception('Erreur inattendue lors de la récupération des statistiques: $e');
|
||||
|
||||
@@ -122,7 +122,7 @@ class OrganizationService {
|
||||
/// Recherche avancée d'organisations
|
||||
Future<List<OrganizationModel>> searchOrganizations({
|
||||
String? nom,
|
||||
TypeOrganization? type,
|
||||
String? type,
|
||||
StatutOrganization? statut,
|
||||
String? ville,
|
||||
String? region,
|
||||
@@ -166,7 +166,7 @@ class OrganizationService {
|
||||
/// Filtre les organisations par type
|
||||
List<OrganizationModel> filterByType(
|
||||
List<OrganizationModel> organizations,
|
||||
TypeOrganization type,
|
||||
String type,
|
||||
) {
|
||||
return organizations.where((org) => org.typeOrganisation == type).toList();
|
||||
}
|
||||
@@ -251,7 +251,7 @@ class OrganizationService {
|
||||
// Statistiques par type
|
||||
final parType = <String, int>{};
|
||||
for (final org in organizations) {
|
||||
final type = org.typeOrganisation.displayName;
|
||||
final type = org.typeOrganisation;
|
||||
parType[type] = (parType[type] ?? 0) + 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Entité Type de référence (ex: Type d'organisation)
|
||||
class TypeReferenceEntity extends Equatable {
|
||||
final String id;
|
||||
final String domaine;
|
||||
final String code;
|
||||
final String libelle;
|
||||
final String? description;
|
||||
final String? icone;
|
||||
final String? couleur;
|
||||
final String? severity;
|
||||
final int ordreAffichage;
|
||||
final bool estDefaut;
|
||||
final bool estSysteme;
|
||||
final bool actif;
|
||||
|
||||
const TypeReferenceEntity({
|
||||
required this.id,
|
||||
required this.domaine,
|
||||
required this.code,
|
||||
required this.libelle,
|
||||
this.description,
|
||||
this.icone,
|
||||
this.couleur,
|
||||
this.severity,
|
||||
this.ordreAffichage = 0,
|
||||
this.estDefaut = false,
|
||||
this.estSysteme = false,
|
||||
this.actif = true,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [id, domaine, code, libelle, description, icone, couleur, severity, ordreAffichage, estDefaut, estSysteme, actif];
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import '../entities/type_reference_entity.dart';
|
||||
|
||||
abstract class IOrgTypesRepository {
|
||||
Future<List<TypeReferenceEntity>> getOrgTypes();
|
||||
Future<TypeReferenceEntity> createOrgType({
|
||||
required String code,
|
||||
required String libelle,
|
||||
String? description,
|
||||
String? couleur,
|
||||
int ordreAffichage,
|
||||
});
|
||||
Future<TypeReferenceEntity> updateOrgType({
|
||||
required String id,
|
||||
required String code,
|
||||
required String libelle,
|
||||
String? description,
|
||||
String? couleur,
|
||||
int ordreAffichage,
|
||||
});
|
||||
Future<void> deleteOrgType(String id);
|
||||
}
|
||||
@@ -37,7 +37,7 @@ abstract class IOrganizationRepository {
|
||||
/// Recherche avancée d'organisations
|
||||
Future<List<OrganizationModel>> searchOrganizations({
|
||||
String? nom,
|
||||
TypeOrganization? type,
|
||||
String? type,
|
||||
StatutOrganization? statut,
|
||||
String? ville,
|
||||
String? region,
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import 'package:injectable/injectable.dart';
|
||||
import '../entities/type_reference_entity.dart';
|
||||
import '../repositories/org_types_repository.dart';
|
||||
|
||||
@injectable
|
||||
class CreateOrgType {
|
||||
final IOrgTypesRepository _repository;
|
||||
const CreateOrgType(this._repository);
|
||||
|
||||
Future<TypeReferenceEntity> call({
|
||||
required String code,
|
||||
required String libelle,
|
||||
String? description,
|
||||
String? couleur,
|
||||
int ordreAffichage = 0,
|
||||
}) => _repository.createOrgType(
|
||||
code: code,
|
||||
libelle: libelle,
|
||||
description: description,
|
||||
couleur: couleur,
|
||||
ordreAffichage: ordreAffichage,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import 'package:injectable/injectable.dart';
|
||||
import '../repositories/org_types_repository.dart';
|
||||
|
||||
@injectable
|
||||
class DeleteOrgType {
|
||||
final IOrgTypesRepository _repository;
|
||||
const DeleteOrgType(this._repository);
|
||||
|
||||
Future<void> call(String id) => _repository.deleteOrgType(id);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import 'package:injectable/injectable.dart';
|
||||
import '../entities/type_reference_entity.dart';
|
||||
import '../repositories/org_types_repository.dart';
|
||||
|
||||
@injectable
|
||||
class GetOrgTypes {
|
||||
final IOrgTypesRepository _repository;
|
||||
const GetOrgTypes(this._repository);
|
||||
|
||||
Future<List<TypeReferenceEntity>> call() => _repository.getOrgTypes();
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import 'package:injectable/injectable.dart';
|
||||
import '../entities/type_reference_entity.dart';
|
||||
import '../repositories/org_types_repository.dart';
|
||||
|
||||
@injectable
|
||||
class UpdateOrgType {
|
||||
final IOrgTypesRepository _repository;
|
||||
const UpdateOrgType(this._repository);
|
||||
|
||||
Future<TypeReferenceEntity> call({
|
||||
required String id,
|
||||
required String code,
|
||||
required String libelle,
|
||||
String? description,
|
||||
String? couleur,
|
||||
int ordreAffichage = 0,
|
||||
}) => _repository.updateOrgType(
|
||||
id: id,
|
||||
code: code,
|
||||
libelle: libelle,
|
||||
description: description,
|
||||
couleur: couleur,
|
||||
ordreAffichage: ordreAffichage,
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
/// Page de création d'une nouvelle organisation
|
||||
/// Respecte strictement le design system établi dans l'application
|
||||
/// Page de création d'une nouvelle organisation — tous les champs exhaustifs
|
||||
library create_organisation_page;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -8,8 +7,13 @@ import '../../data/models/organization_model.dart';
|
||||
import '../../bloc/organizations_bloc.dart';
|
||||
import '../../bloc/organizations_event.dart';
|
||||
import '../../bloc/organizations_state.dart';
|
||||
import '../../bloc/org_types_bloc.dart';
|
||||
import '../../domain/entities/type_reference_entity.dart';
|
||||
import '../../../../shared/design_system/tokens/app_colors.dart';
|
||||
import '../../../../core/di/injection_container.dart';
|
||||
|
||||
const List<String> _devises = ['XOF', 'XAF', 'EUR', 'USD', 'GBP', 'CAD', 'CHF', 'MAD', 'GHS', 'NGN', 'CDF', 'KES'];
|
||||
|
||||
/// Page de création d'organisation avec design system cohérent
|
||||
class CreateOrganizationPage extends StatefulWidget {
|
||||
const CreateOrganizationPage({super.key});
|
||||
|
||||
@@ -19,54 +23,99 @@ class CreateOrganizationPage extends StatefulWidget {
|
||||
|
||||
class _CreateOrganizationPageState extends State<CreateOrganizationPage> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
// Informations de base
|
||||
final _nomController = TextEditingController();
|
||||
final _nomCourtController = TextEditingController();
|
||||
final _descriptionController = TextEditingController();
|
||||
|
||||
// Informations légales
|
||||
final _numeroEnregistrementController = TextEditingController();
|
||||
DateTime? _dateFondation;
|
||||
|
||||
// Contact
|
||||
final _emailController = TextEditingController();
|
||||
final _telephoneController = TextEditingController();
|
||||
final _telephoneSecondaireController = TextEditingController();
|
||||
final _emailSecondaireController = TextEditingController();
|
||||
final _siteWebController = TextEditingController();
|
||||
final _reseauxSociauxController = TextEditingController();
|
||||
|
||||
// Localisation
|
||||
final _adresseController = TextEditingController();
|
||||
final _villeController = TextEditingController();
|
||||
final _codePostalController = TextEditingController();
|
||||
final _regionController = TextEditingController();
|
||||
final _paysController = TextEditingController();
|
||||
|
||||
TypeOrganization _selectedType = TypeOrganization.association;
|
||||
// Finances
|
||||
String _selectedDevise = 'XOF';
|
||||
final _budgetAnnuelController = TextEditingController();
|
||||
bool _cotisationObligatoire = false;
|
||||
final _montantCotisationAnnuelleController = TextEditingController();
|
||||
|
||||
// Mission & contenu
|
||||
final _objectifsController = TextEditingController();
|
||||
final _activitesPrincipalesController = TextEditingController();
|
||||
final _certificationsController = TextEditingController();
|
||||
final _partenairesController = TextEditingController();
|
||||
final _notesController = TextEditingController();
|
||||
|
||||
// Configuration
|
||||
String? _selectedTypeCode;
|
||||
StatutOrganization _selectedStatut = StatutOrganization.active;
|
||||
bool _accepteNouveauxMembres = true;
|
||||
bool _organisationPublique = true;
|
||||
|
||||
late final OrgTypesBloc _orgTypesBloc;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_orgTypesBloc = sl<OrgTypesBloc>()..add(const LoadOrgTypes());
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_orgTypesBloc.close();
|
||||
_nomController.dispose();
|
||||
_nomCourtController.dispose();
|
||||
_descriptionController.dispose();
|
||||
_numeroEnregistrementController.dispose();
|
||||
_emailController.dispose();
|
||||
_telephoneController.dispose();
|
||||
_telephoneSecondaireController.dispose();
|
||||
_emailSecondaireController.dispose();
|
||||
_siteWebController.dispose();
|
||||
_reseauxSociauxController.dispose();
|
||||
_adresseController.dispose();
|
||||
_villeController.dispose();
|
||||
_codePostalController.dispose();
|
||||
_regionController.dispose();
|
||||
_paysController.dispose();
|
||||
_budgetAnnuelController.dispose();
|
||||
_montantCotisationAnnuelleController.dispose();
|
||||
_objectifsController.dispose();
|
||||
_activitesPrincipalesController.dispose();
|
||||
_certificationsController.dispose();
|
||||
_partenairesController.dispose();
|
||||
_notesController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF8F9FA), // Background cohérent
|
||||
backgroundColor: AppColors.lightBackground,
|
||||
appBar: AppBar(
|
||||
backgroundColor: const Color(0xFF6C5CE7),
|
||||
backgroundColor: AppColors.primaryGreen,
|
||||
foregroundColor: Colors.white,
|
||||
title: const Text('Nouvelle Organisation'),
|
||||
elevation: 0,
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _isFormValid() ? _saveOrganisation : null,
|
||||
child: const Text(
|
||||
'Enregistrer',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
child: const Text('Enregistrer', style: TextStyle(color: Colors.white, fontWeight: FontWeight.w600)),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -74,36 +123,38 @@ class _CreateOrganizationPageState extends State<CreateOrganizationPage> {
|
||||
listener: (context, state) {
|
||||
if (state is OrganizationCreated) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Organisation créée avec succès'),
|
||||
backgroundColor: Color(0xFF10B981),
|
||||
),
|
||||
const SnackBar(content: Text('Organisation créée avec succès'), backgroundColor: AppColors.success),
|
||||
);
|
||||
Navigator.of(context).pop(true); // Retour avec succès
|
||||
Navigator.of(context).pop(true);
|
||||
} else if (state is OrganizationsError) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.message),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
SnackBar(content: Text(state.message), backgroundColor: Colors.red),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(12), // SpacingTokens cohérent
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildBasicInfoCard(),
|
||||
const SizedBox(height: 16),
|
||||
_buildContactCard(),
|
||||
const SizedBox(height: 16),
|
||||
_buildLocationCard(),
|
||||
const SizedBox(height: 16),
|
||||
_buildConfigurationCard(),
|
||||
const SizedBox(height: 24),
|
||||
_buildSection('Informations de base', Icons.business, _buildBasicInfoFields()),
|
||||
const SizedBox(height: 8),
|
||||
_buildSection('Informations légales', Icons.gavel, _buildLegalFields()),
|
||||
const SizedBox(height: 8),
|
||||
_buildSection('Contact', Icons.contact_phone, _buildContactFields()),
|
||||
const SizedBox(height: 8),
|
||||
_buildSection('Localisation', Icons.location_on, _buildLocationFields()),
|
||||
const SizedBox(height: 8),
|
||||
_buildSection('Configuration', Icons.settings, _buildConfigurationFields()),
|
||||
const SizedBox(height: 8),
|
||||
_buildSection('Finances', Icons.account_balance_wallet, _buildFinancesFields()),
|
||||
const SizedBox(height: 8),
|
||||
_buildSection('Mission & Activités', Icons.flag, _buildMissionFields()),
|
||||
const SizedBox(height: 8),
|
||||
_buildSection('Informations complémentaires', Icons.info_outline, _buildSupplementaryFields()),
|
||||
const SizedBox(height: 8),
|
||||
_buildActionButtons(),
|
||||
],
|
||||
),
|
||||
@@ -113,421 +164,370 @@ class _CreateOrganizationPageState extends State<CreateOrganizationPage> {
|
||||
);
|
||||
}
|
||||
|
||||
/// Carte des informations de base
|
||||
Widget _buildBasicInfoCard() {
|
||||
Widget _buildSection(String title, IconData icon, List<Widget> children) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8), // RadiusTokens cohérent
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(8)),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Informations de base',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF6C5CE7),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _nomController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Nom de l\'organisation *',
|
||||
hintText: 'Ex: Association des Développeurs',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.business),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return 'Le nom est obligatoire';
|
||||
}
|
||||
if (value.trim().length < 3) {
|
||||
return 'Le nom doit contenir au moins 3 caractères';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onChanged: (_) => setState(() {}),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _nomCourtController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Nom court (optionnel)',
|
||||
hintText: 'Ex: AsDev',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.short_text),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value != null && value.trim().isNotEmpty && value.trim().length < 2) {
|
||||
return 'Le nom court doit contenir au moins 2 caractères';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
DropdownButtonFormField<TypeOrganization>(
|
||||
value: _selectedType,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Type d\'organisation *',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.category),
|
||||
),
|
||||
items: TypeOrganization.values.map((type) {
|
||||
return DropdownMenuItem(
|
||||
value: type,
|
||||
child: Row(
|
||||
children: [
|
||||
Text(type.icon, style: const TextStyle(fontSize: 16)),
|
||||
const SizedBox(width: 8),
|
||||
Text(type.displayName),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
setState(() {
|
||||
_selectedType = value;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _descriptionController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Description (optionnel)',
|
||||
hintText: 'Décrivez brièvement l\'organisation...',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.description),
|
||||
),
|
||||
maxLines: 3,
|
||||
validator: (value) {
|
||||
if (value != null && value.trim().isNotEmpty && value.trim().length < 10) {
|
||||
return 'La description doit contenir au moins 10 caractères';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
Row(children: [
|
||||
Icon(icon, size: 16, color: AppColors.primaryGreen),
|
||||
const SizedBox(width: 6),
|
||||
Text(title, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.bold, color: AppColors.primaryGreen)),
|
||||
]),
|
||||
const SizedBox(height: 10),
|
||||
...children,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Carte des informations de contact
|
||||
Widget _buildContactCard() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Contact',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF6C5CE7),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _emailController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Email (optionnel)',
|
||||
hintText: 'contact@organisation.com',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.email),
|
||||
),
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
validator: (value) {
|
||||
if (value != null && value.trim().isNotEmpty) {
|
||||
final emailRegex = RegExp(r'^[^@]+@[^@]+\.[^@]+$');
|
||||
if (!emailRegex.hasMatch(value.trim())) {
|
||||
return 'Format d\'email invalide';
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _telephoneController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Téléphone (optionnel)',
|
||||
hintText: '+225 XX XX XX XX XX',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.phone),
|
||||
),
|
||||
keyboardType: TextInputType.phone,
|
||||
validator: (value) {
|
||||
if (value != null && value.trim().isNotEmpty && value.trim().length < 8) {
|
||||
return 'Numéro de téléphone invalide';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _siteWebController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Site web (optionnel)',
|
||||
hintText: 'https://www.organisation.com',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.web),
|
||||
),
|
||||
keyboardType: TextInputType.url,
|
||||
validator: (value) {
|
||||
if (value != null && value.trim().isNotEmpty) {
|
||||
final urlRegex = RegExp(r'^https?://[^\s]+$');
|
||||
if (!urlRegex.hasMatch(value.trim())) {
|
||||
return 'Format d\'URL invalide (doit commencer par http:// ou https://)';
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
List<Widget> _buildBasicInfoFields() => [
|
||||
TextFormField(
|
||||
controller: _nomController,
|
||||
decoration: const InputDecoration(labelText: 'Nom de l\'organisation *', hintText: 'Ex: Mutuelle des Entrepreneurs', border: OutlineInputBorder(), prefixIcon: Icon(Icons.business)),
|
||||
validator: (v) => v == null || v.trim().length < 3 ? 'Minimum 3 caractères' : null,
|
||||
onChanged: (_) => setState(() {}),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: _nomCourtController,
|
||||
decoration: const InputDecoration(labelText: 'Sigle / Nom court', hintText: 'Ex: MUKEFI', border: OutlineInputBorder(), prefixIcon: Icon(Icons.short_text)),
|
||||
validator: (v) => v != null && v.trim().isNotEmpty && v.trim().length < 2 ? 'Minimum 2 caractères' : null,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
BlocBuilder<OrgTypesBloc, OrgTypesState>(
|
||||
bloc: _orgTypesBloc,
|
||||
builder: (context, orgTypesState) {
|
||||
final types = orgTypesState is OrgTypesLoaded ? orgTypesState.types
|
||||
: orgTypesState is OrgTypeSuccess ? orgTypesState.types
|
||||
: <TypeReferenceEntity>[];
|
||||
if (orgTypesState is OrgTypesLoading || orgTypesState is OrgTypesInitial) {
|
||||
return const InputDecorator(
|
||||
decoration: InputDecoration(labelText: 'Type d\'organisation *', border: OutlineInputBorder(), prefixIcon: Icon(Icons.category)),
|
||||
child: LinearProgressIndicator(),
|
||||
);
|
||||
}
|
||||
return DropdownButtonFormField<String>(
|
||||
value: types.any((t) => t.code == _selectedTypeCode) ? _selectedTypeCode : null,
|
||||
decoration: const InputDecoration(labelText: 'Type d\'organisation *', border: OutlineInputBorder(), prefixIcon: Icon(Icons.category)),
|
||||
items: types.map((t) => DropdownMenuItem(value: t.code, child: Text(t.libelle))).toList(),
|
||||
onChanged: (v) => setState(() => _selectedTypeCode = v),
|
||||
validator: (v) => v == null ? 'Le type est obligatoire' : null,
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: _descriptionController,
|
||||
decoration: const InputDecoration(labelText: 'Description', hintText: 'Décrivez brièvement l\'organisation...', border: OutlineInputBorder(), prefixIcon: Icon(Icons.description)),
|
||||
maxLines: 3,
|
||||
validator: (v) => v != null && v.trim().isNotEmpty && v.trim().length < 10 ? 'Minimum 10 caractères' : null,
|
||||
),
|
||||
];
|
||||
|
||||
/// Carte de localisation
|
||||
Widget _buildLocationCard() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
List<Widget> _buildLegalFields() => [
|
||||
TextFormField(
|
||||
controller: _numeroEnregistrementController,
|
||||
decoration: const InputDecoration(labelText: 'Numéro d\'enregistrement officiel', hintText: 'Ex: CI-ASSOC-2024-001', border: OutlineInputBorder(), prefixIcon: Icon(Icons.assignment)),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
InkWell(
|
||||
onTap: () => _pickDateFondation(context),
|
||||
child: InputDecorator(
|
||||
decoration: const InputDecoration(labelText: 'Date de fondation', border: OutlineInputBorder(), prefixIcon: Icon(Icons.cake)),
|
||||
child: Text(
|
||||
_dateFondation != null ? _formatDate(_dateFondation) : 'Sélectionner une date',
|
||||
style: TextStyle(color: _dateFondation != null ? AppColors.textPrimaryLight : AppColors.textSecondaryLight),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Localisation',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF6C5CE7),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _adresseController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Adresse (optionnel)',
|
||||
hintText: 'Rue, quartier...',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.location_on),
|
||||
),
|
||||
maxLines: 2,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _villeController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Ville',
|
||||
hintText: 'Abidjan',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.location_city),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _regionController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Région',
|
||||
hintText: 'Lagunes',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.map),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _paysController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Pays',
|
||||
hintText: 'Côte d\'Ivoire',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.flag),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
];
|
||||
|
||||
/// Carte de configuration
|
||||
Widget _buildConfigurationCard() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Configuration',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF6C5CE7),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
DropdownButtonFormField<StatutOrganization>(
|
||||
value: _selectedStatut,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Statut initial *',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.toggle_on),
|
||||
),
|
||||
items: StatutOrganization.values.map((statut) {
|
||||
final color = Color(int.parse(statut.color.substring(1), radix: 16) + 0xFF000000);
|
||||
return DropdownMenuItem(
|
||||
value: statut,
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 12,
|
||||
height: 12,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(statut.displayName),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
setState(() {
|
||||
_selectedStatut = value;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
List<Widget> _buildContactFields() => [
|
||||
TextFormField(
|
||||
controller: _emailController,
|
||||
decoration: const InputDecoration(labelText: 'Email principal *', hintText: 'contact@organisation.com', border: OutlineInputBorder(), prefixIcon: Icon(Icons.email)),
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
validator: (v) {
|
||||
if (v == null || v.trim().isEmpty) return 'L\'email est obligatoire';
|
||||
if (!RegExp(r'^[^@]+@[^@]+\.[^@]+$').hasMatch(v.trim())) return 'Format invalide';
|
||||
return null;
|
||||
},
|
||||
onChanged: (_) => setState(() {}),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: _emailSecondaireController,
|
||||
decoration: const InputDecoration(labelText: 'Email secondaire', hintText: 'info@organisation.com', border: OutlineInputBorder(), prefixIcon: Icon(Icons.alternate_email)),
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
validator: (v) {
|
||||
if (v != null && v.trim().isNotEmpty && !RegExp(r'^[^@]+@[^@]+\.[^@]+$').hasMatch(v.trim())) return 'Format invalide';
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(children: [
|
||||
Expanded(child: TextFormField(
|
||||
controller: _telephoneController,
|
||||
decoration: const InputDecoration(labelText: 'Téléphone principal', hintText: '+225 XX XX XX', border: OutlineInputBorder(), prefixIcon: Icon(Icons.phone)),
|
||||
keyboardType: TextInputType.phone,
|
||||
validator: (v) => v != null && v.trim().isNotEmpty && v.trim().length < 8 ? 'Numéro invalide' : null,
|
||||
)),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: TextFormField(
|
||||
controller: _telephoneSecondaireController,
|
||||
decoration: const InputDecoration(labelText: 'Téléphone secondaire', hintText: '+225 XX XX XX', border: OutlineInputBorder(), prefixIcon: Icon(Icons.phone_forwarded)),
|
||||
keyboardType: TextInputType.phone,
|
||||
validator: (v) => v != null && v.trim().isNotEmpty && v.trim().length < 8 ? 'Numéro invalide' : null,
|
||||
)),
|
||||
]),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: _siteWebController,
|
||||
decoration: const InputDecoration(labelText: 'Site web', hintText: 'https://www.organisation.com', border: OutlineInputBorder(), prefixIcon: Icon(Icons.web)),
|
||||
keyboardType: TextInputType.url,
|
||||
validator: (v) {
|
||||
if (v != null && v.trim().isNotEmpty && !RegExp(r'^https?://[^\s]+$').hasMatch(v.trim())) return 'Doit commencer par http:// ou https://';
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: _reseauxSociauxController,
|
||||
decoration: const InputDecoration(labelText: 'Réseaux sociaux', hintText: 'Ex: Facebook, LinkedIn, Twitter...', border: OutlineInputBorder(), prefixIcon: Icon(Icons.share)),
|
||||
maxLines: 2,
|
||||
),
|
||||
];
|
||||
|
||||
List<Widget> _buildLocationFields() => [
|
||||
TextFormField(
|
||||
controller: _adresseController,
|
||||
decoration: const InputDecoration(labelText: 'Adresse', hintText: 'Rue, quartier...', border: OutlineInputBorder(), prefixIcon: Icon(Icons.location_on)),
|
||||
maxLines: 2,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(children: [
|
||||
Expanded(child: TextFormField(
|
||||
controller: _villeController,
|
||||
decoration: const InputDecoration(labelText: 'Ville', hintText: 'Abidjan', border: OutlineInputBorder(), prefixIcon: Icon(Icons.location_city)),
|
||||
)),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: TextFormField(
|
||||
controller: _codePostalController,
|
||||
decoration: const InputDecoration(labelText: 'Code postal', hintText: '01 BP 1234', border: OutlineInputBorder(), prefixIcon: Icon(Icons.markunread_mailbox)),
|
||||
)),
|
||||
]),
|
||||
const SizedBox(height: 8),
|
||||
Row(children: [
|
||||
Expanded(child: TextFormField(
|
||||
controller: _regionController,
|
||||
decoration: const InputDecoration(labelText: 'Région', hintText: 'Lagunes', border: OutlineInputBorder(), prefixIcon: Icon(Icons.map)),
|
||||
)),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: TextFormField(
|
||||
controller: _paysController,
|
||||
decoration: const InputDecoration(labelText: 'Pays', hintText: 'Côte d\'Ivoire', border: OutlineInputBorder(), prefixIcon: Icon(Icons.flag)),
|
||||
)),
|
||||
]),
|
||||
];
|
||||
|
||||
List<Widget> _buildConfigurationFields() => [
|
||||
DropdownButtonFormField<StatutOrganization>(
|
||||
value: _selectedStatut,
|
||||
decoration: const InputDecoration(labelText: 'Statut initial *', border: OutlineInputBorder(), prefixIcon: Icon(Icons.toggle_on)),
|
||||
items: StatutOrganization.values.map((s) {
|
||||
final color = Color(int.parse(s.color.substring(1), radix: 16) + 0xFF000000);
|
||||
return DropdownMenuItem(value: s, child: Row(children: [
|
||||
Container(width: 10, height: 10, decoration: BoxDecoration(color: color, shape: BoxShape.circle)),
|
||||
const SizedBox(width: 8),
|
||||
Text(s.displayName),
|
||||
]));
|
||||
}).toList(),
|
||||
onChanged: (v) { if (v != null) setState(() => _selectedStatut = v); },
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SwitchListTile(
|
||||
dense: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: const Text('Organisation publique', style: TextStyle(fontSize: 14)),
|
||||
subtitle: const Text('Visible par tous les utilisateurs', style: TextStyle(fontSize: 12)),
|
||||
value: _organisationPublique,
|
||||
onChanged: (v) => setState(() => _organisationPublique = v),
|
||||
activeColor: AppColors.primaryGreen,
|
||||
),
|
||||
SwitchListTile(
|
||||
dense: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: const Text('Accepte de nouveaux membres', style: TextStyle(fontSize: 14)),
|
||||
subtitle: const Text('Les demandes d\'adhésion sont ouvertes', style: TextStyle(fontSize: 12)),
|
||||
value: _accepteNouveauxMembres,
|
||||
onChanged: (v) => setState(() => _accepteNouveauxMembres = v),
|
||||
activeColor: AppColors.primaryGreen,
|
||||
),
|
||||
];
|
||||
|
||||
List<Widget> _buildFinancesFields() => [
|
||||
DropdownButtonFormField<String>(
|
||||
value: _selectedDevise,
|
||||
decoration: const InputDecoration(labelText: 'Devise', border: OutlineInputBorder(), prefixIcon: Icon(Icons.currency_exchange)),
|
||||
items: _devises.map((d) => DropdownMenuItem(value: d, child: Text(d))).toList(),
|
||||
onChanged: (v) { if (v != null) setState(() => _selectedDevise = v); },
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: _budgetAnnuelController,
|
||||
decoration: const InputDecoration(labelText: 'Budget annuel', hintText: 'Ex: 5000000', border: OutlineInputBorder(), prefixIcon: Icon(Icons.account_balance)),
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
validator: (v) {
|
||||
if (v != null && v.trim().isNotEmpty && double.tryParse(v.trim()) == null) return 'Montant invalide';
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SwitchListTile(
|
||||
dense: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: const Text('Cotisation obligatoire', style: TextStyle(fontSize: 14)),
|
||||
value: _cotisationObligatoire,
|
||||
onChanged: (v) => setState(() => _cotisationObligatoire = v),
|
||||
activeColor: AppColors.primaryGreen,
|
||||
),
|
||||
if (_cotisationObligatoire) ...[
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: _montantCotisationAnnuelleController,
|
||||
decoration: InputDecoration(labelText: 'Montant cotisation annuelle ($_selectedDevise)', hintText: 'Ex: 25000', border: const OutlineInputBorder(), prefixIcon: const Icon(Icons.payments)),
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
validator: (v) {
|
||||
if (_cotisationObligatoire && (v == null || v.trim().isEmpty)) return 'Obligatoire si cotisation requise';
|
||||
if (v != null && v.trim().isNotEmpty && double.tryParse(v.trim()) == null) return 'Montant invalide';
|
||||
return null;
|
||||
},
|
||||
),
|
||||
],
|
||||
];
|
||||
|
||||
List<Widget> _buildMissionFields() => [
|
||||
TextFormField(
|
||||
controller: _objectifsController,
|
||||
decoration: const InputDecoration(labelText: 'Objectifs', hintText: 'Décrire les objectifs principaux...', border: OutlineInputBorder(), prefixIcon: Icon(Icons.track_changes)),
|
||||
maxLines: 3,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: _activitesPrincipalesController,
|
||||
decoration: const InputDecoration(labelText: 'Activités principales', hintText: 'Lister les activités clés...', border: OutlineInputBorder(), prefixIcon: Icon(Icons.work)),
|
||||
maxLines: 3,
|
||||
),
|
||||
];
|
||||
|
||||
List<Widget> _buildSupplementaryFields() => [
|
||||
TextFormField(
|
||||
controller: _certificationsController,
|
||||
decoration: const InputDecoration(labelText: 'Certifications / Agréments', hintText: 'Ex: ISO 9001, Agrément ministériel...', border: OutlineInputBorder(), prefixIcon: Icon(Icons.verified)),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: _partenairesController,
|
||||
decoration: const InputDecoration(labelText: 'Partenaires', hintText: 'Ex: Banque Mondiale, Ministère...', border: OutlineInputBorder(), prefixIcon: Icon(Icons.handshake)),
|
||||
maxLines: 2,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: _notesController,
|
||||
decoration: const InputDecoration(labelText: 'Notes internes', hintText: 'Informations internes non visibles publiquement...', border: OutlineInputBorder(), prefixIcon: Icon(Icons.sticky_note_2)),
|
||||
maxLines: 2,
|
||||
),
|
||||
];
|
||||
|
||||
/// Boutons d'action
|
||||
Widget _buildActionButtons() {
|
||||
return Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: _isFormValid() ? _saveOrganisation : null,
|
||||
icon: const Icon(Icons.save),
|
||||
label: const Text('Créer l\'organisation'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF6C5CE7),
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
textStyle: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
return Column(children: [
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: _isFormValid() ? _saveOrganisation : null,
|
||||
icon: const Icon(Icons.save),
|
||||
label: const Text('Créer l\'organisation'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryGreen,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
textStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
icon: const Icon(Icons.cancel),
|
||||
label: const Text('Annuler'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: const Color(0xFF6B7280),
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
textStyle: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
icon: const Icon(Icons.cancel),
|
||||
label: const Text('Annuler'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppColors.textSecondaryLight,
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
bool _isFormValid() =>
|
||||
_nomController.text.trim().isNotEmpty &&
|
||||
_emailController.text.trim().isNotEmpty;
|
||||
|
||||
Future<void> _pickDateFondation(BuildContext context) async {
|
||||
final picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: _dateFondation ?? DateTime(2000),
|
||||
firstDate: DateTime(1800),
|
||||
lastDate: DateTime.now(),
|
||||
);
|
||||
if (picked != null) setState(() => _dateFondation = picked);
|
||||
}
|
||||
|
||||
/// Vérifie si le formulaire est valide
|
||||
bool _isFormValid() {
|
||||
return _nomController.text.trim().isNotEmpty;
|
||||
String _formatDate(DateTime? date) {
|
||||
if (date == null) return '';
|
||||
return '${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year}';
|
||||
}
|
||||
|
||||
/// Sauvegarde l'organisation
|
||||
void _saveOrganisation() {
|
||||
if (_formKey.currentState?.validate() ?? false) {
|
||||
final organisation = OrganizationModel(
|
||||
final org = OrganizationModel(
|
||||
nom: _nomController.text.trim(),
|
||||
nomCourt: _nomCourtController.text.trim().isEmpty ? null : _nomCourtController.text.trim(),
|
||||
description: _descriptionController.text.trim().isEmpty ? null : _descriptionController.text.trim(),
|
||||
typeOrganisation: _selectedType,
|
||||
typeOrganisation: _selectedTypeCode ?? 'ASSOCIATION',
|
||||
statut: _selectedStatut,
|
||||
dateFondation: _dateFondation,
|
||||
numeroEnregistrement: _numeroEnregistrementController.text.trim().isEmpty ? null : _numeroEnregistrementController.text.trim(),
|
||||
email: _emailController.text.trim().isEmpty ? null : _emailController.text.trim(),
|
||||
telephone: _telephoneController.text.trim().isEmpty ? null : _telephoneController.text.trim(),
|
||||
telephoneSecondaire: _telephoneSecondaireController.text.trim().isEmpty ? null : _telephoneSecondaireController.text.trim(),
|
||||
emailSecondaire: _emailSecondaireController.text.trim().isEmpty ? null : _emailSecondaireController.text.trim(),
|
||||
siteWeb: _siteWebController.text.trim().isEmpty ? null : _siteWebController.text.trim(),
|
||||
reseauxSociaux: _reseauxSociauxController.text.trim().isEmpty ? null : _reseauxSociauxController.text.trim(),
|
||||
adresse: _adresseController.text.trim().isEmpty ? null : _adresseController.text.trim(),
|
||||
ville: _villeController.text.trim().isEmpty ? null : _villeController.text.trim(),
|
||||
codePostal: _codePostalController.text.trim().isEmpty ? null : _codePostalController.text.trim(),
|
||||
region: _regionController.text.trim().isEmpty ? null : _regionController.text.trim(),
|
||||
pays: _paysController.text.trim().isEmpty ? null : _paysController.text.trim(),
|
||||
dateCreation: DateTime.now(),
|
||||
devise: _selectedDevise,
|
||||
budgetAnnuel: _budgetAnnuelController.text.trim().isEmpty ? null : double.tryParse(_budgetAnnuelController.text.trim()),
|
||||
cotisationObligatoire: _cotisationObligatoire,
|
||||
montantCotisationAnnuelle: _montantCotisationAnnuelleController.text.trim().isEmpty ? null : double.tryParse(_montantCotisationAnnuelleController.text.trim()),
|
||||
objectifs: _objectifsController.text.trim().isEmpty ? null : _objectifsController.text.trim(),
|
||||
activitesPrincipales: _activitesPrincipalesController.text.trim().isEmpty ? null : _activitesPrincipalesController.text.trim(),
|
||||
certifications: _certificationsController.text.trim().isEmpty ? null : _certificationsController.text.trim(),
|
||||
partenaires: _partenairesController.text.trim().isEmpty ? null : _partenairesController.text.trim(),
|
||||
notes: _notesController.text.trim().isEmpty ? null : _notesController.text.trim(),
|
||||
organisationPublique: _organisationPublique,
|
||||
accepteNouveauxMembres: _accepteNouveauxMembres,
|
||||
nombreMembres: 0,
|
||||
);
|
||||
|
||||
context.read<OrganizationsBloc>().add(CreateOrganization(organisation));
|
||||
context.read<OrganizationsBloc>().add(CreateOrganization(org));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,500 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../../shared/design_system/unionflow_design_system.dart';
|
||||
import '../../../../shared/widgets/core_card.dart';
|
||||
import '../../../../core/di/injection.dart';
|
||||
import '../../bloc/org_types_bloc.dart';
|
||||
import '../../domain/entities/type_reference_entity.dart';
|
||||
|
||||
class OrgTypesPage extends StatelessWidget {
|
||||
const OrgTypesPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (_) => getIt<OrgTypesBloc>()..add(const LoadOrgTypes()),
|
||||
child: const _OrgTypesView(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _OrgTypesView extends StatefulWidget {
|
||||
const _OrgTypesView();
|
||||
@override
|
||||
State<_OrgTypesView> createState() => _OrgTypesViewState();
|
||||
}
|
||||
|
||||
class _OrgTypesViewState extends State<_OrgTypesView> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.lightBackground,
|
||||
appBar: const UFAppBar(
|
||||
title: 'TYPES D\'ORGANISATIONS',
|
||||
automaticallyImplyLeading: true,
|
||||
),
|
||||
body: BlocConsumer<OrgTypesBloc, OrgTypesState>(
|
||||
listener: (context, state) {
|
||||
if (state is OrgTypeSuccess) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.message),
|
||||
backgroundColor: Colors.green,
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
} else if (state is OrgTypeOperationError) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.message),
|
||||
backgroundColor: Colors.red,
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
if (state is OrgTypesLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (state is OrgTypesError) {
|
||||
return _buildErrorState(context, state.message);
|
||||
}
|
||||
|
||||
final types = _getTypes(state);
|
||||
if (types.isEmpty && state is! OrgTypeOperating) {
|
||||
return _buildEmptyState(context);
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async => context.read<OrgTypesBloc>().add(const LoadOrgTypes()),
|
||||
child: ListView.separated(
|
||||
padding: const EdgeInsets.all(12),
|
||||
itemCount: types.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(height: 6),
|
||||
itemBuilder: (context, index) => _buildTypeCard(context, types[index], state),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.small(
|
||||
onPressed: () => _showTypeForm(context, null),
|
||||
backgroundColor: AppColors.primaryGreen,
|
||||
child: const Icon(Icons.add, color: Colors.white),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<TypeReferenceEntity> _getTypes(OrgTypesState state) {
|
||||
if (state is OrgTypesLoaded) return state.types;
|
||||
if (state is OrgTypeOperating) return state.types;
|
||||
if (state is OrgTypeSuccess) return state.types;
|
||||
if (state is OrgTypeOperationError) return state.types;
|
||||
return [];
|
||||
}
|
||||
|
||||
Widget _buildTypeCard(BuildContext context, TypeReferenceEntity type, OrgTypesState state) {
|
||||
final isOperating = state is OrgTypeOperating;
|
||||
final color = _parseColor(type.couleur) ?? AppColors.primaryGreen;
|
||||
|
||||
return Opacity(
|
||||
opacity: isOperating ? 0.6 : 1.0,
|
||||
child: CoreCard(
|
||||
margin: EdgeInsets.zero,
|
||||
onTap: (!type.estSysteme && !isOperating) ? () => _showTypeForm(context, type) : null,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(left: BorderSide(color: color, width: 3)),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
padding: const EdgeInsets.fromLTRB(10, 8, 8, 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.12),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
type.code,
|
||||
style: TextStyle(
|
||||
fontSize: 9,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: color,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (type.estDefaut) ...[
|
||||
const SizedBox(width: 6),
|
||||
const Icon(Icons.star_rounded, size: 13, color: Color(0xFFF59E0B)),
|
||||
],
|
||||
if (type.estSysteme) ...[
|
||||
const SizedBox(width: 6),
|
||||
Icon(Icons.lock_outline, size: 12, color: Colors.grey[500]),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
type.libelle,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.textPrimaryLight,
|
||||
),
|
||||
),
|
||||
if (type.description != null && type.description!.isNotEmpty) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
type.description!,
|
||||
style: const TextStyle(fontSize: 11, color: AppColors.textSecondaryLight),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
if (!type.estSysteme && !isOperating) ...[
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit_outlined, size: 16),
|
||||
color: AppColors.textSecondaryLight,
|
||||
onPressed: () => _showTypeForm(context, type),
|
||||
padding: const EdgeInsets.all(4),
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete_outline, size: 16),
|
||||
color: Colors.red[400],
|
||||
onPressed: () => _confirmDelete(context, type),
|
||||
padding: const EdgeInsets.all(4),
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState(BuildContext context) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.category_outlined, size: 48, color: Colors.grey[400]),
|
||||
const SizedBox(height: 12),
|
||||
const Text(
|
||||
'Aucun type défini',
|
||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w700, color: AppColors.textPrimaryLight),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'Créez votre premier type d\'organisation',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey[500]),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => _showTypeForm(context, null),
|
||||
icon: const Icon(Icons.add, size: 16),
|
||||
label: const Text('Créer un type'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryGreen,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildErrorState(BuildContext context, String message) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.error_outline, size: 40, color: Colors.red[400]),
|
||||
const SizedBox(height: 12),
|
||||
const Text('Erreur de chargement', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w700)),
|
||||
const SizedBox(height: 6),
|
||||
Text(message, style: const TextStyle(fontSize: 11, color: AppColors.textSecondaryLight), textAlign: TextAlign.center),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () => context.read<OrgTypesBloc>().add(const LoadOrgTypes()),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryGreen,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
child: const Text('Réessayer'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showTypeForm(BuildContext context, TypeReferenceEntity? existing) {
|
||||
final bloc = context.read<OrgTypesBloc>();
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (_) => _OrgTypeFormSheet(existing: existing, bloc: bloc),
|
||||
);
|
||||
}
|
||||
|
||||
void _confirmDelete(BuildContext context, TypeReferenceEntity type) {
|
||||
final bloc = context.read<OrgTypesBloc>();
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
title: const Text('Supprimer ce type ?', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w700)),
|
||||
content: Text(
|
||||
'Supprimer "${type.libelle}" (${type.code}) ?\nLes organisations associées devront être mises à jour.',
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(ctx);
|
||||
bloc.add(DeleteOrgTypeEvent(type.id));
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)),
|
||||
),
|
||||
child: const Text('Supprimer', style: TextStyle(fontSize: 12)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Color? _parseColor(String? hex) {
|
||||
if (hex == null || hex.isEmpty) return null;
|
||||
try {
|
||||
final clean = hex.replaceAll('#', '');
|
||||
return Color(int.parse('FF$clean', radix: 16));
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Bottom sheet form for create / edit
|
||||
class _OrgTypeFormSheet extends StatefulWidget {
|
||||
final TypeReferenceEntity? existing;
|
||||
final OrgTypesBloc bloc;
|
||||
|
||||
const _OrgTypeFormSheet({this.existing, required this.bloc});
|
||||
|
||||
@override
|
||||
State<_OrgTypeFormSheet> createState() => _OrgTypeFormSheetState();
|
||||
}
|
||||
|
||||
class _OrgTypeFormSheetState extends State<_OrgTypeFormSheet> {
|
||||
late final TextEditingController _codeCtrl;
|
||||
late final TextEditingController _libelleCtrl;
|
||||
late final TextEditingController _descCtrl;
|
||||
String _selectedColor = '#22C55E';
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
static const _colorOptions = [
|
||||
'#22C55E', '#3B82F6', '#F59E0B', '#EF4444',
|
||||
'#8B5CF6', '#EC4899', '#14B8A6', '#F97316',
|
||||
'#64748B', '#A16207',
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_codeCtrl = TextEditingController(text: widget.existing?.code ?? '');
|
||||
_libelleCtrl = TextEditingController(text: widget.existing?.libelle ?? '');
|
||||
_descCtrl = TextEditingController(text: widget.existing?.description ?? '');
|
||||
_selectedColor = widget.existing?.couleur ?? '#22C55E';
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_codeCtrl.dispose();
|
||||
_libelleCtrl.dispose();
|
||||
_descCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isEdit = widget.existing != null;
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 24),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Handle bar
|
||||
Center(
|
||||
child: Container(
|
||||
width: 36,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(color: Colors.grey[300], borderRadius: BorderRadius.circular(2)),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Text(
|
||||
isEdit ? 'Modifier le type' : 'Nouveau type d\'organisation',
|
||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w800, color: AppColors.textPrimaryLight),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
|
||||
// Code
|
||||
TextFormField(
|
||||
controller: _codeCtrl,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Code technique *',
|
||||
hintText: 'Ex: ASSOCIATION',
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
isDense: true,
|
||||
),
|
||||
textCapitalization: TextCapitalization.characters,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
validator: (v) => (v == null || v.trim().isEmpty) ? 'Code requis' : null,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// Libellé
|
||||
TextFormField(
|
||||
controller: _libelleCtrl,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Libellé *',
|
||||
hintText: 'Ex: Association',
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
isDense: true,
|
||||
),
|
||||
style: const TextStyle(fontSize: 12),
|
||||
validator: (v) => (v == null || v.trim().isEmpty) ? 'Libellé requis' : null,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// Description
|
||||
TextFormField(
|
||||
controller: _descCtrl,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Description',
|
||||
hintText: 'Optionnelle',
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
isDense: true,
|
||||
),
|
||||
style: const TextStyle(fontSize: 12),
|
||||
maxLines: 2,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Color picker
|
||||
const Text('Couleur', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: AppColors.textSecondaryLight)),
|
||||
const SizedBox(height: 6),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: _colorOptions.map((hex) {
|
||||
final color = Color(int.parse('FF${hex.replaceAll('#', '')}', radix: 16));
|
||||
final selected = _selectedColor == hex;
|
||||
return GestureDetector(
|
||||
onTap: () => setState(() => _selectedColor = hex),
|
||||
child: Container(
|
||||
width: 28,
|
||||
height: 28,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
shape: BoxShape.circle,
|
||||
border: selected ? Border.all(color: Colors.black, width: 2) : null,
|
||||
),
|
||||
child: selected ? const Icon(Icons.check, size: 14, color: Colors.white) : null,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Submit
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: _submit,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryGreen,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
child: Text(
|
||||
isEdit ? 'Enregistrer' : 'Créer le type',
|
||||
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w700),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _submit() {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
Navigator.pop(context);
|
||||
if (widget.existing != null) {
|
||||
widget.bloc.add(UpdateOrgTypeEvent(
|
||||
id: widget.existing!.id,
|
||||
code: _codeCtrl.text.trim(),
|
||||
libelle: _libelleCtrl.text.trim(),
|
||||
description: _descCtrl.text.trim().isEmpty ? null : _descCtrl.text.trim(),
|
||||
couleur: _selectedColor,
|
||||
ordreAffichage: widget.existing!.ordreAffichage,
|
||||
));
|
||||
} else {
|
||||
widget.bloc.add(CreateOrgTypeEvent(
|
||||
code: _codeCtrl.text.trim(),
|
||||
libelle: _libelleCtrl.text.trim(),
|
||||
description: _descCtrl.text.trim().isEmpty ? null : _descCtrl.text.trim(),
|
||||
couleur: _selectedColor,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,13 +8,14 @@ import '../widgets/organization_card.dart';
|
||||
import '../widgets/create_organization_dialog.dart';
|
||||
import '../widgets/edit_organization_dialog.dart';
|
||||
import 'organization_detail_page.dart';
|
||||
import 'org_types_page.dart';
|
||||
import '../../../../shared/design_system/unionflow_design_system.dart';
|
||||
import '../../../../shared/design_system/tokens/unionflow_colors.dart';
|
||||
import '../../../../shared/design_system/unionflow_design_v2.dart';
|
||||
import '../../../../shared/design_system/components/animated_fade_in.dart';
|
||||
import '../../../../shared/design_system/components/animated_slide_in.dart';
|
||||
import '../../../../shared/design_system/components/african_pattern_background.dart';
|
||||
import '../../../../shared/design_system/components/uf_app_bar.dart';
|
||||
import '../../../../features/authentication/presentation/bloc/auth_bloc.dart';
|
||||
import '../../../../features/authentication/data/models/user_role.dart';
|
||||
|
||||
/// Page de gestion des organisations - Interface sophistiquée et exhaustive
|
||||
///
|
||||
@@ -36,7 +37,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
TabController? _tabController;
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
List<TypeOrganization?> _availableTypes = [];
|
||||
List<String?> _availableTypes = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -62,21 +63,21 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
|
||||
}
|
||||
|
||||
/// Calcule les types d'organisations disponibles dans les données
|
||||
List<TypeOrganization?> _calculateAvailableTypes(List<OrganizationModel> organizations) {
|
||||
List<String?> _calculateAvailableTypes(List<OrganizationModel> organizations) {
|
||||
if (organizations.isEmpty) {
|
||||
return [null]; // Seulement "Toutes"
|
||||
}
|
||||
|
||||
// Extraire tous les types uniques
|
||||
final typesSet = organizations.map((org) => org.typeOrganisation).toSet();
|
||||
final types = typesSet.toList()..sort((a, b) => a.displayName.compareTo(b.displayName));
|
||||
final types = typesSet.toList()..sort((a, b) => a.compareTo(b));
|
||||
|
||||
// null en premier pour "Toutes", puis les types triés alphabétiquement
|
||||
return [null, ...types];
|
||||
}
|
||||
|
||||
/// Initialise ou met à jour le TabController si les types ont changé
|
||||
void _updateTabController(List<TypeOrganization?> newTypes) {
|
||||
void _updateTabController(List<String?> newTypes) {
|
||||
if (_availableTypes.length != newTypes.length ||
|
||||
!_availableTypes.every((type) => newTypes.contains(type))) {
|
||||
_availableTypes = newTypes;
|
||||
@@ -94,7 +95,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.message),
|
||||
backgroundColor: UnionFlowColors.error,
|
||||
backgroundColor: AppColors.error,
|
||||
duration: const Duration(seconds: 4),
|
||||
action: SnackBarAction(
|
||||
label: 'Réessayer',
|
||||
@@ -109,7 +110,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Organisation créée avec succès'),
|
||||
backgroundColor: UnionFlowColors.success,
|
||||
backgroundColor: AppColors.success,
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
@@ -117,7 +118,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Organisation mise à jour avec succès'),
|
||||
backgroundColor: UnionFlowColors.success,
|
||||
backgroundColor: AppColors.success,
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
@@ -125,7 +126,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Organisation supprimée avec succès'),
|
||||
backgroundColor: UnionFlowColors.success,
|
||||
backgroundColor: AppColors.success,
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
@@ -137,10 +138,21 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
|
||||
backgroundColor: Colors.transparent,
|
||||
appBar: UFAppBar(
|
||||
title: 'Gestion des Organisations',
|
||||
backgroundColor: UnionFlowColors.surface,
|
||||
foregroundColor: UnionFlowColors.textPrimary,
|
||||
backgroundColor: AppColors.lightSurface,
|
||||
foregroundColor: AppColors.textPrimaryLight,
|
||||
elevation: 0,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.category_outlined),
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => const OrgTypesPage(),
|
||||
),
|
||||
);
|
||||
},
|
||||
tooltip: 'Types d\'organisations',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
onPressed: () {
|
||||
@@ -246,7 +258,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
|
||||
padding: EdgeInsets.all(SpacingTokens.md),
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: UnionFlowColors.unionGreen,
|
||||
color: AppColors.primaryGreen,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -287,10 +299,16 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
|
||||
if (state is! OrganizationsLoaded && state is! OrganizationsLoadingMore) {
|
||||
return null;
|
||||
}
|
||||
// Réservé au Super Admin uniquement
|
||||
final authState = context.read<AuthBloc>().state;
|
||||
if (authState is! AuthAuthenticated ||
|
||||
authState.effectiveRole != UserRole.superAdmin) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return FloatingActionButton.extended(
|
||||
onPressed: _showCreateOrganizationDialog,
|
||||
backgroundColor: UnionFlowColors.unionGreen,
|
||||
backgroundColor: AppColors.primaryGreen,
|
||||
elevation: 8,
|
||||
icon: const Icon(Icons.add, color: Colors.white),
|
||||
label: const Text(
|
||||
@@ -308,9 +326,19 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(SpacingTokens.md),
|
||||
decoration: BoxDecoration(
|
||||
gradient: UnionFlowColors.primaryGradient,
|
||||
gradient: const LinearGradient(
|
||||
colors: [AppColors.brandGreen, AppColors.primaryGreen],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(RadiusTokens.lg),
|
||||
boxShadow: UnionFlowColors.greenGlowShadow,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.primaryGreen.withOpacity(0.3),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
@@ -334,7 +362,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
|
||||
const Text(
|
||||
'Gestion des Organisations',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
@@ -370,9 +398,9 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(SpacingTokens.md),
|
||||
decoration: BoxDecoration(
|
||||
color: UnionFlowColors.surface,
|
||||
color: AppColors.lightSurface,
|
||||
borderRadius: BorderRadius.circular(RadiusTokens.lg),
|
||||
boxShadow: UnionFlowColors.softShadow,
|
||||
boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 8, offset: Offset(0, 2))],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -381,16 +409,16 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.analytics_outlined,
|
||||
color: UnionFlowColors.textSecondary,
|
||||
color: AppColors.textSecondaryLight,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: SpacingTokens.xs),
|
||||
const Text(
|
||||
'Statistiques',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: UnionFlowColors.textPrimary,
|
||||
color: AppColors.textPrimaryLight,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -403,7 +431,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
|
||||
'Total',
|
||||
totalOrgs.toString(),
|
||||
Icons.business_outlined,
|
||||
UnionFlowColors.unionGreen,
|
||||
AppColors.primaryGreen,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: SpacingTokens.sm),
|
||||
@@ -412,7 +440,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
|
||||
'Actives',
|
||||
activeOrgs.toString(),
|
||||
Icons.check_circle_outline,
|
||||
UnionFlowColors.success,
|
||||
AppColors.success,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: SpacingTokens.sm),
|
||||
@@ -421,7 +449,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
|
||||
'Membres',
|
||||
totalMembers.toString(),
|
||||
Icons.people_outline,
|
||||
UnionFlowColors.info,
|
||||
AppColors.primaryGreen,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -452,14 +480,14 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: UnionFlowColors.textPrimary,
|
||||
color: AppColors.textPrimaryLight,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: UnionFlowColors.textSecondary,
|
||||
color: AppColors.textSecondaryLight,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
@@ -473,16 +501,16 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(SpacingTokens.md),
|
||||
decoration: BoxDecoration(
|
||||
color: UnionFlowColors.surface,
|
||||
color: AppColors.lightSurface,
|
||||
borderRadius: BorderRadius.circular(RadiusTokens.lg),
|
||||
boxShadow: UnionFlowColors.softShadow,
|
||||
boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 8, offset: Offset(0, 2))],
|
||||
),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: UnionFlowColors.surfaceVariant,
|
||||
color: AppColors.lightBackground,
|
||||
borderRadius: BorderRadius.circular(RadiusTokens.md),
|
||||
border: Border.all(
|
||||
color: UnionFlowColors.border,
|
||||
color: AppColors.lightBorder,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
@@ -494,17 +522,17 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Rechercher par nom, type, localisation...',
|
||||
hintStyle: const TextStyle(
|
||||
color: UnionFlowColors.textSecondary,
|
||||
color: AppColors.textSecondaryLight,
|
||||
fontSize: 14,
|
||||
),
|
||||
prefixIcon: const Icon(Icons.search, color: UnionFlowColors.unionGreen),
|
||||
prefixIcon: const Icon(Icons.search, color: AppColors.primaryGreen),
|
||||
suffixIcon: _searchController.text.isNotEmpty
|
||||
? IconButton(
|
||||
onPressed: () {
|
||||
_searchController.clear();
|
||||
context.read<OrganizationsBloc>().add(const SearchOrganizations(''));
|
||||
},
|
||||
icon: const Icon(Icons.clear, color: UnionFlowColors.textSecondary),
|
||||
icon: const Icon(Icons.clear, color: AppColors.textSecondaryLight),
|
||||
)
|
||||
: null,
|
||||
border: InputBorder.none,
|
||||
@@ -519,7 +547,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
|
||||
}
|
||||
|
||||
/// Onglets de catégories générés dynamiquement selon les types disponibles
|
||||
Widget _buildCategoryTabs(List<TypeOrganization?> availableTypes) {
|
||||
Widget _buildCategoryTabs(List<String?> availableTypes) {
|
||||
if (_tabController == null || availableTypes.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
@@ -528,16 +556,16 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
|
||||
builder: (context, state) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: UnionFlowColors.surface,
|
||||
color: AppColors.lightSurface,
|
||||
borderRadius: BorderRadius.circular(RadiusTokens.lg),
|
||||
boxShadow: UnionFlowColors.softShadow,
|
||||
boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 8, offset: Offset(0, 2))],
|
||||
),
|
||||
child: TabBar(
|
||||
controller: _tabController!,
|
||||
isScrollable: availableTypes.length > 4, // Scrollable si plus de 4 types
|
||||
labelColor: UnionFlowColors.unionGreen,
|
||||
unselectedLabelColor: UnionFlowColors.textSecondary,
|
||||
indicatorColor: UnionFlowColors.unionGreen,
|
||||
labelColor: AppColors.primaryGreen,
|
||||
unselectedLabelColor: AppColors.textSecondaryLight,
|
||||
indicatorColor: AppColors.primaryGreen,
|
||||
indicatorWeight: 3,
|
||||
indicatorSize: TabBarIndicatorSize.tab,
|
||||
labelStyle: const TextStyle(
|
||||
@@ -560,22 +588,8 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
|
||||
}
|
||||
},
|
||||
tabs: availableTypes.map((type) {
|
||||
// null = "Toutes", sinon utiliser le displayName du type
|
||||
final label = type == null ? 'Toutes' : type.displayName;
|
||||
final icon = type?.icon; // Emoji du type
|
||||
|
||||
return Tab(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (icon != null) ...[
|
||||
Text(icon, style: const TextStyle(fontSize: 16)),
|
||||
const SizedBox(width: SpacingTokens.xs),
|
||||
],
|
||||
Text(label),
|
||||
],
|
||||
),
|
||||
);
|
||||
final label = type == null ? 'Toutes' : type;
|
||||
return Tab(text: label);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
@@ -623,13 +637,13 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
|
||||
Container(
|
||||
padding: const EdgeInsets.all(SpacingTokens.xl),
|
||||
decoration: BoxDecoration(
|
||||
color: UnionFlowColors.unionGreenPale,
|
||||
color: AppColors.primaryGreen.withOpacity(0.12),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.business_outlined,
|
||||
size: 64,
|
||||
color: UnionFlowColors.unionGreen,
|
||||
color: AppColors.primaryGreen,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: SpacingTokens.lg),
|
||||
@@ -638,7 +652,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: UnionFlowColors.textPrimary,
|
||||
color: AppColors.textPrimaryLight,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: SpacingTokens.xs),
|
||||
@@ -646,7 +660,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
|
||||
'Essayez de modifier vos critères de recherche\nou créez une nouvelle organisation',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: UnionFlowColors.textSecondary,
|
||||
color: AppColors.textSecondaryLight,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
@@ -659,7 +673,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
|
||||
icon: const Icon(Icons.clear_all),
|
||||
label: const Text('Réinitialiser les filtres'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: UnionFlowColors.unionGreen,
|
||||
backgroundColor: AppColors.primaryGreen,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: SpacingTokens.lg,
|
||||
@@ -683,7 +697,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(
|
||||
color: UnionFlowColors.unionGreen,
|
||||
color: AppColors.primaryGreen,
|
||||
strokeWidth: 3,
|
||||
),
|
||||
const SizedBox(height: SpacingTokens.md),
|
||||
@@ -691,7 +705,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
|
||||
'Chargement des organisations...',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: UnionFlowColors.textSecondary,
|
||||
color: AppColors.textSecondaryLight,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -706,10 +720,10 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
|
||||
margin: const EdgeInsets.all(SpacingTokens.xl),
|
||||
padding: const EdgeInsets.all(SpacingTokens.xl),
|
||||
decoration: BoxDecoration(
|
||||
color: UnionFlowColors.errorPale,
|
||||
color: AppColors.error.withOpacity(0.08),
|
||||
borderRadius: BorderRadius.circular(RadiusTokens.lg),
|
||||
border: Border.all(
|
||||
color: UnionFlowColors.errorLight,
|
||||
color: AppColors.error.withOpacity(0.3),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
@@ -719,7 +733,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
|
||||
const Icon(
|
||||
Icons.error_outline,
|
||||
size: 64,
|
||||
color: UnionFlowColors.error,
|
||||
color: AppColors.error,
|
||||
),
|
||||
const SizedBox(height: SpacingTokens.md),
|
||||
Text(
|
||||
@@ -727,7 +741,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: UnionFlowColors.textPrimary,
|
||||
color: AppColors.textPrimaryLight,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
@@ -737,7 +751,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
|
||||
state.details!,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: UnionFlowColors.textSecondary,
|
||||
color: AppColors.textSecondaryLight,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
@@ -750,7 +764,7 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: const Text('Réessayer'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: UnionFlowColors.unionGreen,
|
||||
backgroundColor: AppColors.primaryGreen,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: SpacingTokens.lg,
|
||||
@@ -771,10 +785,11 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
|
||||
void _showOrganizationDetails(OrganizationModel org) {
|
||||
final orgId = org.id;
|
||||
if (orgId == null || orgId.isEmpty) return;
|
||||
final bloc = context.read<OrganizationsBloc>();
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (context) => BlocProvider.value(
|
||||
value: context.read<OrganizationsBloc>(),
|
||||
builder: (_) => BlocProvider.value(
|
||||
value: bloc,
|
||||
child: OrganizationDetailPage(organizationId: orgId),
|
||||
),
|
||||
),
|
||||
@@ -782,39 +797,48 @@ class _OrganizationsPageState extends State<OrganizationsPage> with TickerProvid
|
||||
}
|
||||
|
||||
void _showCreateOrganizationDialog() {
|
||||
final bloc = context.read<OrganizationsBloc>();
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => const CreateOrganizationDialog(),
|
||||
builder: (_) => BlocProvider.value(
|
||||
value: bloc,
|
||||
child: const CreateOrganizationDialog(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showEditOrganizationDialog(OrganizationModel org) {
|
||||
final bloc = context.read<OrganizationsBloc>();
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => EditOrganizationDialog(organization: org),
|
||||
builder: (_) => BlocProvider.value(
|
||||
value: bloc,
|
||||
child: EditOrganizationDialog(organization: org),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _confirmDeleteOrganization(OrganizationModel org) {
|
||||
final bloc = context.read<OrganizationsBloc>();
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: const Text('Supprimer l\'organisation'),
|
||||
content: Text('Voulez-vous vraiment supprimer "${org.nom}" ?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
if (org.id != null) {
|
||||
context.read<OrganizationsBloc>().add(DeleteOrganization(org.id!));
|
||||
bloc.add(DeleteOrganization(org.id!));
|
||||
}
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(dialogContext).pop();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: UnionFlowColors.error,
|
||||
backgroundColor: AppColors.error,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('Supprimer'),
|
||||
|
||||
@@ -6,7 +6,10 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../bloc/organizations_bloc.dart';
|
||||
import '../../bloc/organizations_event.dart';
|
||||
import '../../bloc/org_types_bloc.dart';
|
||||
import '../../domain/entities/type_reference_entity.dart';
|
||||
import '../../data/models/organization_model.dart';
|
||||
import '../../../../core/di/injection_container.dart';
|
||||
|
||||
/// Dialogue de création d'organisation
|
||||
class CreateOrganizationDialog extends StatefulWidget {
|
||||
@@ -34,12 +37,20 @@ class _CreateOrganizationDialogState extends State<CreateOrganizationDialog> {
|
||||
final _objectifsController = TextEditingController();
|
||||
|
||||
// Valeurs sélectionnées
|
||||
TypeOrganization _selectedType = TypeOrganization.association;
|
||||
String? _selectedTypeCode;
|
||||
bool _accepteNouveauxMembres = true;
|
||||
late final OrgTypesBloc _orgTypesBloc;
|
||||
bool _organisationPublique = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_orgTypesBloc = sl<OrgTypesBloc>()..add(const LoadOrgTypes());
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_orgTypesBloc.close();
|
||||
_nomController.dispose();
|
||||
_nomCourtController.dispose();
|
||||
_descriptionController.dispose();
|
||||
@@ -146,24 +157,39 @@ class _CreateOrganizationDialogState extends State<CreateOrganizationDialog> {
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Type d'organisation
|
||||
DropdownButtonFormField<TypeOrganization>(
|
||||
value: _selectedType,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Type d\'organisation *',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.category),
|
||||
),
|
||||
items: TypeOrganization.values.map((type) {
|
||||
return DropdownMenuItem(
|
||||
value: type,
|
||||
child: Text(type.displayName),
|
||||
// Type d'organisation dynamique
|
||||
BlocBuilder<OrgTypesBloc, OrgTypesState>(
|
||||
bloc: _orgTypesBloc,
|
||||
builder: (context, orgTypesState) {
|
||||
final types = orgTypesState is OrgTypesLoaded
|
||||
? orgTypesState.types
|
||||
: orgTypesState is OrgTypeSuccess
|
||||
? orgTypesState.types
|
||||
: <TypeReferenceEntity>[];
|
||||
if (orgTypesState is OrgTypesLoading || orgTypesState is OrgTypesInitial) {
|
||||
return const InputDecorator(
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Type d\'organisation *',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.category),
|
||||
),
|
||||
child: LinearProgressIndicator(),
|
||||
);
|
||||
}
|
||||
return DropdownButtonFormField<String>(
|
||||
value: types.any((t) => t.code == _selectedTypeCode) ? _selectedTypeCode : null,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Type d\'organisation *',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.category),
|
||||
),
|
||||
items: types.map((type) => DropdownMenuItem<String>(
|
||||
value: type.code,
|
||||
child: Text(type.libelle),
|
||||
)).toList(),
|
||||
onChanged: (value) => setState(() => _selectedTypeCode = value),
|
||||
validator: (value) => value == null ? 'Le type est obligatoire' : null,
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_selectedType = value!;
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
@@ -378,7 +404,7 @@ class _CreateOrganizationDialogState extends State<CreateOrganizationDialog> {
|
||||
pays: _paysController.text.isNotEmpty ? _paysController.text : null,
|
||||
siteWeb: _siteWebController.text.isNotEmpty ? _siteWebController.text : null,
|
||||
objectifs: _objectifsController.text.isNotEmpty ? _objectifsController.text : null,
|
||||
typeOrganisation: _selectedType,
|
||||
typeOrganisation: _selectedTypeCode ?? 'ASSOCIATION',
|
||||
statut: StatutOrganization.active,
|
||||
accepteNouveauxMembres: _accepteNouveauxMembres,
|
||||
organisationPublique: _organisationPublique,
|
||||
|
||||
@@ -5,7 +5,11 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../bloc/organizations_bloc.dart';
|
||||
import '../../bloc/organizations_event.dart';
|
||||
import '../../bloc/organizations_state.dart';
|
||||
import '../../bloc/org_types_bloc.dart';
|
||||
import '../../domain/entities/type_reference_entity.dart';
|
||||
import '../../data/models/organization_model.dart';
|
||||
import '../../../../core/di/injection_container.dart';
|
||||
|
||||
class EditOrganizationDialog extends StatefulWidget {
|
||||
final OrganizationModel organization;
|
||||
@@ -35,8 +39,10 @@ class _EditOrganizationDialogState extends State<EditOrganizationDialog> {
|
||||
late final TextEditingController _siteWebController;
|
||||
late final TextEditingController _objectifsController;
|
||||
|
||||
late TypeOrganization _selectedType;
|
||||
late String _selectedTypeCode;
|
||||
late StatutOrganization _selectedStatut;
|
||||
late final OrgTypesBloc _orgTypesBloc;
|
||||
late final OrganizationsBloc _detailBloc;
|
||||
late bool _accepteNouveauxMembres;
|
||||
late bool _organisationPublique;
|
||||
|
||||
@@ -57,14 +63,44 @@ class _EditOrganizationDialogState extends State<EditOrganizationDialog> {
|
||||
_siteWebController = TextEditingController(text: widget.organization.siteWeb ?? '');
|
||||
_objectifsController = TextEditingController(text: widget.organization.objectifs ?? '');
|
||||
|
||||
_selectedType = widget.organization.typeOrganisation;
|
||||
_selectedTypeCode = widget.organization.typeOrganisation;
|
||||
_selectedStatut = widget.organization.statut;
|
||||
_orgTypesBloc = sl<OrgTypesBloc>()..add(const LoadOrgTypes());
|
||||
_accepteNouveauxMembres = widget.organization.accepteNouveauxMembres;
|
||||
_organisationPublique = widget.organization.organisationPublique;
|
||||
|
||||
// Charge le détail complet depuis l'API (la liste retourne un DTO allégé)
|
||||
_detailBloc = sl<OrganizationsBloc>();
|
||||
if (widget.organization.id != null) {
|
||||
_detailBloc.add(LoadOrganizationById(widget.organization.id!));
|
||||
}
|
||||
}
|
||||
|
||||
void _refillForm(OrganizationModel org) {
|
||||
_nomController.text = org.nom;
|
||||
_nomCourtController.text = org.nomCourt ?? '';
|
||||
_descriptionController.text = org.description ?? '';
|
||||
_emailController.text = org.email ?? '';
|
||||
_telephoneController.text = org.telephone ?? '';
|
||||
_siteWebController.text = org.siteWeb ?? '';
|
||||
_adresseController.text = org.adresse ?? '';
|
||||
_villeController.text = org.ville ?? '';
|
||||
_codePostalController.text = org.codePostal ?? '';
|
||||
_regionController.text = org.region ?? '';
|
||||
_paysController.text = org.pays ?? '';
|
||||
_objectifsController.text = org.objectifs ?? '';
|
||||
setState(() {
|
||||
_selectedTypeCode = org.typeOrganisation;
|
||||
_selectedStatut = org.statut;
|
||||
_accepteNouveauxMembres = org.accepteNouveauxMembres;
|
||||
_organisationPublique = org.organisationPublique;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_orgTypesBloc.close();
|
||||
_detailBloc.close();
|
||||
_nomController.dispose();
|
||||
_nomCourtController.dispose();
|
||||
_descriptionController.dispose();
|
||||
@@ -82,8 +118,15 @@ class _EditOrganizationDialogState extends State<EditOrganizationDialog> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Dialog(
|
||||
child: Container(
|
||||
return BlocListener<OrganizationsBloc, OrganizationsState>(
|
||||
bloc: _detailBloc,
|
||||
listener: (context, state) {
|
||||
if (state is OrganizationLoaded) {
|
||||
_refillForm(state.organization);
|
||||
}
|
||||
},
|
||||
child: Dialog(
|
||||
child: Container(
|
||||
width: MediaQuery.of(context).size.width * 0.9,
|
||||
constraints: const BoxConstraints(maxHeight: 600),
|
||||
child: Column(
|
||||
@@ -149,6 +192,7 @@ class _EditOrganizationDialogState extends State<EditOrganizationDialog> {
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -237,23 +281,40 @@ class _EditOrganizationDialogState extends State<EditOrganizationDialog> {
|
||||
}
|
||||
|
||||
Widget _buildTypeDropdown() {
|
||||
return DropdownButtonFormField<TypeOrganization>(
|
||||
value: _selectedType,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Type d\'organisation *',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.category),
|
||||
),
|
||||
items: TypeOrganization.values.map((type) {
|
||||
return DropdownMenuItem(
|
||||
value: type,
|
||||
child: Text(type.displayName),
|
||||
return BlocBuilder<OrgTypesBloc, OrgTypesState>(
|
||||
bloc: _orgTypesBloc,
|
||||
builder: (context, orgTypesState) {
|
||||
final types = orgTypesState is OrgTypesLoaded
|
||||
? orgTypesState.types
|
||||
: orgTypesState is OrgTypeSuccess
|
||||
? orgTypesState.types
|
||||
: <TypeReferenceEntity>[];
|
||||
if (orgTypesState is OrgTypesLoading || orgTypesState is OrgTypesInitial) {
|
||||
return const InputDecorator(
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Type d\'organisation *',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.category),
|
||||
),
|
||||
child: LinearProgressIndicator(),
|
||||
);
|
||||
}
|
||||
return DropdownButtonFormField<String>(
|
||||
value: types.any((t) => t.code == _selectedTypeCode) ? _selectedTypeCode : null,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Type d\'organisation *',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.category),
|
||||
),
|
||||
items: types.map((type) => DropdownMenuItem<String>(
|
||||
value: type.code,
|
||||
child: Text(type.libelle),
|
||||
)).toList(),
|
||||
onChanged: (value) {
|
||||
if (value != null) setState(() => _selectedTypeCode = value);
|
||||
},
|
||||
validator: (value) => value == null ? 'Le type est obligatoire' : null,
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_selectedType = value!;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -453,7 +514,7 @@ class _EditOrganizationDialogState extends State<EditOrganizationDialog> {
|
||||
pays: _paysController.text.isNotEmpty ? _paysController.text : null,
|
||||
siteWeb: _siteWebController.text.isNotEmpty ? _siteWebController.text : null,
|
||||
objectifs: _objectifsController.text.isNotEmpty ? _objectifsController.text : null,
|
||||
typeOrganisation: _selectedType,
|
||||
typeOrganisation: _selectedTypeCode,
|
||||
statut: _selectedStatut,
|
||||
accepteNouveauxMembres: _accepteNouveauxMembres,
|
||||
organisationPublique: _organisationPublique,
|
||||
|
||||
@@ -68,10 +68,7 @@ class OrganizationCard extends StatelessWidget {
|
||||
color: const Color(0xFF6C5CE7).withOpacity(0.1), // ColorTokens cohérent
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(
|
||||
organization.typeOrganisation.icon,
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
child: const Icon(Icons.business_outlined, size: 18, color: Color(0xFF6C5CE7)),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// Nom et nom court
|
||||
@@ -144,7 +141,7 @@ class OrganizationCard extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
organization.typeOrganisation.displayName,
|
||||
organization.typeOrganisation,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF6B7280),
|
||||
|
||||
@@ -169,7 +169,7 @@ class OrganizationFilterWidget extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<TypeOrganization?>(
|
||||
child: DropdownButton<String?>(
|
||||
value: state.typeFilter,
|
||||
hint: const Text(
|
||||
'Type',
|
||||
@@ -185,30 +185,17 @@ class OrganizationFilterWidget extends StatelessWidget {
|
||||
color: Color(0xFF374151),
|
||||
),
|
||||
items: [
|
||||
const DropdownMenuItem<TypeOrganization?>(
|
||||
const DropdownMenuItem<String?>(
|
||||
value: null,
|
||||
child: Text('Tous les types'),
|
||||
),
|
||||
...TypeOrganization.values.map((type) {
|
||||
return DropdownMenuItem<TypeOrganization?>(
|
||||
value: type,
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
type.icon,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
type.displayName,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
...state.organizations
|
||||
.map((org) => org.typeOrganisation)
|
||||
.toSet()
|
||||
.map((code) => DropdownMenuItem<String?>(
|
||||
value: code,
|
||||
child: Text(code, overflow: TextOverflow.ellipsis),
|
||||
)),
|
||||
],
|
||||
onChanged: (value) {
|
||||
context.read<OrganizationsBloc>().add(
|
||||
|
||||
Reference in New Issue
Block a user