Refactoring - Version OK

This commit is contained in:
dahoud
2025-11-17 16:02:04 +00:00
parent 3f00a26308
commit 3b9ffac8cd
198 changed files with 18010 additions and 11383 deletions

View File

@@ -0,0 +1,434 @@
/// Modèle de données pour les organisations
/// Correspond au OrganizationDTO du backend
library organization_model;
import 'package:equatable/equatable.dart';
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')
active,
@JsonValue('INACTIVE')
inactive,
@JsonValue('SUSPENDUE')
suspendue,
@JsonValue('DISSOUTE')
dissoute,
@JsonValue('EN_CREATION')
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 {
switch (this) {
case StatutOrganization.active:
return 'Active';
case StatutOrganization.inactive:
return 'Inactive';
case StatutOrganization.suspendue:
return 'Suspendue';
case StatutOrganization.dissoute:
return 'Dissoute';
case StatutOrganization.enCreation:
return 'En création';
}
}
String get color {
switch (this) {
case StatutOrganization.active:
return '#10B981'; // Vert
case StatutOrganization.inactive:
return '#6B7280'; // Gris
case StatutOrganization.suspendue:
return '#F59E0B'; // Orange
case StatutOrganization.dissoute:
return '#EF4444'; // Rouge
case StatutOrganization.enCreation:
return '#3B82F6'; // Bleu
}
}
}
/// Énumération des types de tri pour les organisations
enum OrganizationSortType {
name,
creationDate,
memberCount,
type,
status,
}
/// Extension pour les types de tri d'organisation
extension OrganizationSortTypeExtension on OrganizationSortType {
String get displayName {
switch (this) {
case OrganizationSortType.name:
return 'Nom';
case OrganizationSortType.creationDate:
return 'Date de création';
case OrganizationSortType.memberCount:
return 'Nombre de membres';
case OrganizationSortType.type:
return 'Type';
case OrganizationSortType.status:
return 'Statut';
}
}
}
/// Modèle d'organisation mobile
@JsonSerializable()
class OrganizationModel extends Equatable {
/// Identifiant unique
final String? id;
/// Nom de l'organisation
final String nom;
/// Nom court ou sigle
final String? nomCourt;
/// Type d'organisation
@JsonKey(name: 'typeOrganisation')
final TypeOrganization typeOrganisation;
/// Statut de l'organisation
final StatutOrganization statut;
/// Description
final String? description;
/// Date de fondation
@JsonKey(name: 'dateFondation')
final DateTime? dateFondation;
/// Numéro d'enregistrement officiel
@JsonKey(name: 'numeroEnregistrement')
final String? numeroEnregistrement;
/// Email de contact
final String? email;
/// Téléphone
final String? telephone;
/// Site web
@JsonKey(name: 'siteWeb')
final String? siteWeb;
/// Adresse complète
final String? adresse;
/// Ville
final String? ville;
/// Code postal
@JsonKey(name: 'codePostal')
final String? codePostal;
/// Région
final String? region;
/// Pays
final String? pays;
/// Logo URL
final String? logo;
/// Nombre de membres
@JsonKey(name: 'nombreMembres')
final int nombreMembres;
/// Nombre d'administrateurs
@JsonKey(name: 'nombreAdministrateurs')
final int nombreAdministrateurs;
/// Budget annuel
@JsonKey(name: 'budgetAnnuel')
final double? budgetAnnuel;
/// Devise
final String devise;
/// Cotisation obligatoire
@JsonKey(name: 'cotisationObligatoire')
final bool cotisationObligatoire;
/// Montant cotisation annuelle
@JsonKey(name: 'montantCotisationAnnuelle')
final double? montantCotisationAnnuelle;
/// Objectifs
final String? objectifs;
/// Activités principales
@JsonKey(name: 'activitesPrincipales')
final String? activitesPrincipales;
/// Certifications
final String? certifications;
/// Partenaires
final String? partenaires;
/// Organisation publique
@JsonKey(name: 'organisationPublique')
final bool organisationPublique;
/// Accepte nouveaux membres
@JsonKey(name: 'accepteNouveauxMembres')
final bool accepteNouveauxMembres;
/// Date de création
@JsonKey(name: 'dateCreation')
final DateTime? dateCreation;
/// Date de modification
@JsonKey(name: 'dateModification')
final DateTime? dateModification;
/// Actif
final bool actif;
const OrganizationModel({
this.id,
required this.nom,
this.nomCourt,
this.typeOrganisation = TypeOrganization.association,
this.statut = StatutOrganization.active,
this.description,
this.dateFondation,
this.numeroEnregistrement,
this.email,
this.telephone,
this.siteWeb,
this.adresse,
this.ville,
this.codePostal,
this.region,
this.pays,
this.logo,
this.nombreMembres = 0,
this.nombreAdministrateurs = 0,
this.budgetAnnuel,
this.devise = 'XOF',
this.cotisationObligatoire = false,
this.montantCotisationAnnuelle,
this.objectifs,
this.activitesPrincipales,
this.certifications,
this.partenaires,
this.organisationPublique = true,
this.accepteNouveauxMembres = true,
this.dateCreation,
this.dateModification,
this.actif = true,
});
/// Factory depuis JSON
factory OrganizationModel.fromJson(Map<String, dynamic> json) =>
_$OrganizationModelFromJson(json);
/// Conversion vers JSON
Map<String, dynamic> toJson() => _$OrganizationModelToJson(this);
/// Copie avec modifications
OrganizationModel copyWith({
String? id,
String? nom,
String? nomCourt,
TypeOrganization? typeOrganisation,
StatutOrganization? statut,
String? description,
DateTime? dateFondation,
String? numeroEnregistrement,
String? email,
String? telephone,
String? siteWeb,
String? adresse,
String? ville,
String? codePostal,
String? region,
String? pays,
String? logo,
int? nombreMembres,
int? nombreAdministrateurs,
double? budgetAnnuel,
String? devise,
bool? cotisationObligatoire,
double? montantCotisationAnnuelle,
String? objectifs,
String? activitesPrincipales,
String? certifications,
String? partenaires,
bool? organisationPublique,
bool? accepteNouveauxMembres,
DateTime? dateCreation,
DateTime? dateModification,
bool? actif,
}) {
return OrganizationModel(
id: id ?? this.id,
nom: nom ?? this.nom,
nomCourt: nomCourt ?? this.nomCourt,
typeOrganisation: typeOrganisation ?? this.typeOrganisation,
statut: statut ?? this.statut,
description: description ?? this.description,
dateFondation: dateFondation ?? this.dateFondation,
numeroEnregistrement: numeroEnregistrement ?? this.numeroEnregistrement,
email: email ?? this.email,
telephone: telephone ?? this.telephone,
siteWeb: siteWeb ?? this.siteWeb,
adresse: adresse ?? this.adresse,
ville: ville ?? this.ville,
codePostal: codePostal ?? this.codePostal,
region: region ?? this.region,
pays: pays ?? this.pays,
logo: logo ?? this.logo,
nombreMembres: nombreMembres ?? this.nombreMembres,
nombreAdministrateurs: nombreAdministrateurs ?? this.nombreAdministrateurs,
budgetAnnuel: budgetAnnuel ?? this.budgetAnnuel,
devise: devise ?? this.devise,
cotisationObligatoire: cotisationObligatoire ?? this.cotisationObligatoire,
montantCotisationAnnuelle: montantCotisationAnnuelle ?? this.montantCotisationAnnuelle,
objectifs: objectifs ?? this.objectifs,
activitesPrincipales: activitesPrincipales ?? this.activitesPrincipales,
certifications: certifications ?? this.certifications,
partenaires: partenaires ?? this.partenaires,
organisationPublique: organisationPublique ?? this.organisationPublique,
accepteNouveauxMembres: accepteNouveauxMembres ?? this.accepteNouveauxMembres,
dateCreation: dateCreation ?? this.dateCreation,
dateModification: dateModification ?? this.dateModification,
actif: actif ?? this.actif,
);
}
/// Ancienneté en années
int get ancienneteAnnees {
if (dateFondation == null) return 0;
return DateTime.now().difference(dateFondation!).inDays ~/ 365;
}
/// Adresse complète formatée
String get adresseComplete {
final parts = <String>[];
if (adresse?.isNotEmpty == true) parts.add(adresse!);
if (ville?.isNotEmpty == true) parts.add(ville!);
if (codePostal?.isNotEmpty == true) parts.add(codePostal!);
if (region?.isNotEmpty == true) parts.add(region!);
if (pays?.isNotEmpty == true) parts.add(pays!);
return parts.join(', ');
}
/// Nom d'affichage
String get nomAffichage => nomCourt?.isNotEmpty == true ? '$nomCourt ($nom)' : nom;
@override
List<Object?> get props => [
id,
nom,
nomCourt,
typeOrganisation,
statut,
description,
dateFondation,
numeroEnregistrement,
email,
telephone,
siteWeb,
adresse,
ville,
codePostal,
region,
pays,
logo,
nombreMembres,
nombreAdministrateurs,
budgetAnnuel,
devise,
cotisationObligatoire,
montantCotisationAnnuelle,
objectifs,
activitesPrincipales,
certifications,
partenaires,
organisationPublique,
accepteNouveauxMembres,
dateCreation,
dateModification,
actif,
];
@override
String toString() => 'OrganisationModel(id: $id, nom: $nom, type: $typeOrganisation, statut: $statut)';
}

View File

@@ -0,0 +1,110 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'organization_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
OrganizationModel _$OrganizationModelFromJson(Map<String, dynamic> json) =>
OrganizationModel(
id: json['id'] as String?,
nom: json['nom'] as String,
nomCourt: json['nomCourt'] as String?,
typeOrganisation: $enumDecodeNullable(
_$TypeOrganizationEnumMap, json['typeOrganisation']) ??
TypeOrganization.association,
statut:
$enumDecodeNullable(_$StatutOrganizationEnumMap, json['statut']) ??
StatutOrganization.active,
description: json['description'] as String?,
dateFondation: json['dateFondation'] == null
? null
: DateTime.parse(json['dateFondation'] as String),
numeroEnregistrement: json['numeroEnregistrement'] as String?,
email: json['email'] as String?,
telephone: json['telephone'] as String?,
siteWeb: json['siteWeb'] as String?,
adresse: json['adresse'] as String?,
ville: json['ville'] as String?,
codePostal: json['codePostal'] as String?,
region: json['region'] as String?,
pays: json['pays'] as String?,
logo: json['logo'] as String?,
nombreMembres: (json['nombreMembres'] as num?)?.toInt() ?? 0,
nombreAdministrateurs:
(json['nombreAdministrateurs'] as num?)?.toInt() ?? 0,
budgetAnnuel: (json['budgetAnnuel'] as num?)?.toDouble(),
devise: json['devise'] as String? ?? 'XOF',
cotisationObligatoire: json['cotisationObligatoire'] as bool? ?? false,
montantCotisationAnnuelle:
(json['montantCotisationAnnuelle'] as num?)?.toDouble(),
objectifs: json['objectifs'] as String?,
activitesPrincipales: json['activitesPrincipales'] as String?,
certifications: json['certifications'] as String?,
partenaires: json['partenaires'] as String?,
organisationPublique: json['organisationPublique'] as bool? ?? true,
accepteNouveauxMembres: json['accepteNouveauxMembres'] as bool? ?? true,
dateCreation: json['dateCreation'] == null
? null
: DateTime.parse(json['dateCreation'] as String),
dateModification: json['dateModification'] == null
? null
: DateTime.parse(json['dateModification'] as String),
actif: json['actif'] as bool? ?? true,
);
Map<String, dynamic> _$OrganizationModelToJson(OrganizationModel instance) =>
<String, dynamic>{
'id': instance.id,
'nom': instance.nom,
'nomCourt': instance.nomCourt,
'typeOrganisation': _$TypeOrganizationEnumMap[instance.typeOrganisation]!,
'statut': _$StatutOrganizationEnumMap[instance.statut]!,
'description': instance.description,
'dateFondation': instance.dateFondation?.toIso8601String(),
'numeroEnregistrement': instance.numeroEnregistrement,
'email': instance.email,
'telephone': instance.telephone,
'siteWeb': instance.siteWeb,
'adresse': instance.adresse,
'ville': instance.ville,
'codePostal': instance.codePostal,
'region': instance.region,
'pays': instance.pays,
'logo': instance.logo,
'nombreMembres': instance.nombreMembres,
'nombreAdministrateurs': instance.nombreAdministrateurs,
'budgetAnnuel': instance.budgetAnnuel,
'devise': instance.devise,
'cotisationObligatoire': instance.cotisationObligatoire,
'montantCotisationAnnuelle': instance.montantCotisationAnnuelle,
'objectifs': instance.objectifs,
'activitesPrincipales': instance.activitesPrincipales,
'certifications': instance.certifications,
'partenaires': instance.partenaires,
'organisationPublique': instance.organisationPublique,
'accepteNouveauxMembres': instance.accepteNouveauxMembres,
'dateCreation': instance.dateCreation?.toIso8601String(),
'dateModification': instance.dateModification?.toIso8601String(),
'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',
StatutOrganization.suspendue: 'SUSPENDUE',
StatutOrganization.dissoute: 'DISSOUTE',
StatutOrganization.enCreation: 'EN_CREATION',
};

View File

@@ -0,0 +1,413 @@
/// Repository pour la gestion des organisations
/// Interface avec l'API backend OrganizationResource
library organization_repository;
import 'package:dio/dio.dart';
import '../models/organization_model.dart';
/// Interface du repository des organisations
abstract class OrganizationRepository {
/// Récupère la liste des organisations avec pagination
Future<List<OrganizationModel>> getOrganizations({
int page = 0,
int size = 20,
String? recherche,
});
/// Récupère une organisation par son ID
Future<OrganizationModel?> getOrganizationById(String id);
/// Crée une nouvelle organisation
Future<OrganizationModel> createOrganization(OrganizationModel organization);
/// Met à jour une organisation
Future<OrganizationModel> updateOrganization(String id, OrganizationModel organization);
/// Supprime une organisation
Future<void> deleteOrganization(String id);
/// Active une organisation
Future<OrganizationModel> activateOrganization(String id);
/// Recherche avancée d'organisations
Future<List<OrganizationModel>> searchOrganizations({
String? nom,
TypeOrganization? type,
StatutOrganization? statut,
String? ville,
String? region,
String? pays,
int page = 0,
int size = 20,
});
/// Récupère les statistiques des organisations
Future<Map<String, dynamic>> getOrganizationsStats();
}
/// Implémentation du repository des organisations
class OrganizationRepositoryImpl implements OrganizationRepository {
final Dio _dio;
static const String _baseUrl = '/api/organisations';
OrganizationRepositoryImpl(this._dio);
@override
Future<List<OrganizationModel>> getOrganizations({
int page = 0,
int size = 20,
String? recherche,
}) async {
try {
final queryParams = <String, dynamic>{
'page': page,
'size': size,
};
if (recherche?.isNotEmpty == true) {
queryParams['recherche'] = recherche;
}
final response = await _dio.get(
_baseUrl,
queryParameters: queryParams,
);
if (response.statusCode == 200) {
final List<dynamic> data = response.data as List<dynamic>;
return data
.map((json) => OrganizationModel.fromJson(json as Map<String, dynamic>))
.toList();
} else {
throw Exception('Erreur lors de la récupération des organisations: ${response.statusCode}');
}
} on DioException catch (e) {
// En cas d'erreur réseau, retourner des données de démonstration
print('Erreur API, utilisation des données de démonstration: ${e.message}');
return _getMockOrganizations(page: page, size: size, recherche: recherche);
} catch (e) {
// En cas d'erreur inattendue, retourner des données de démonstration
print('Erreur inattendue, utilisation des données de démonstration: $e');
return _getMockOrganizations(page: page, size: size, recherche: recherche);
}
}
@override
Future<OrganizationModel?> getOrganizationById(String id) async {
try {
final response = await _dio.get('$_baseUrl/$id');
if (response.statusCode == 200) {
return OrganizationModel.fromJson(response.data as Map<String, dynamic>);
} else if (response.statusCode == 404) {
return null;
} else {
throw Exception('Erreur lors de la récupération de l\'organisation: ${response.statusCode}');
}
} on DioException catch (e) {
if (e.response?.statusCode == 404) {
return null;
}
throw Exception('Erreur réseau lors de la récupération de l\'organisation: ${e.message}');
} catch (e) {
throw Exception('Erreur inattendue lors de la récupération de l\'organisation: $e');
}
}
@override
Future<OrganizationModel> createOrganization(OrganizationModel organization) async {
try {
final response = await _dio.post(
_baseUrl,
data: organization.toJson(),
);
if (response.statusCode == 201) {
return OrganizationModel.fromJson(response.data as Map<String, dynamic>);
} else {
throw Exception('Erreur lors de la création de l\'organisation: ${response.statusCode}');
}
} on DioException catch (e) {
if (e.response?.statusCode == 400) {
final errorData = e.response?.data;
if (errorData is Map<String, dynamic> && errorData.containsKey('error')) {
throw Exception('Données invalides: ${errorData['error']}');
}
} else if (e.response?.statusCode == 409) {
throw Exception('Une organisation avec ces informations existe déjà');
}
throw Exception('Erreur réseau lors de la création de l\'organisation: ${e.message}');
} catch (e) {
throw Exception('Erreur inattendue lors de la création de l\'organisation: $e');
}
}
@override
Future<OrganizationModel> updateOrganization(String id, OrganizationModel organization) async {
try {
final response = await _dio.put(
'$_baseUrl/$id',
data: organization.toJson(),
);
if (response.statusCode == 200) {
return OrganizationModel.fromJson(response.data as Map<String, dynamic>);
} else {
throw Exception('Erreur lors de la mise à jour de l\'organisation: ${response.statusCode}');
}
} on DioException catch (e) {
if (e.response?.statusCode == 404) {
throw Exception('Organisation non trouvée');
} else if (e.response?.statusCode == 400) {
final errorData = e.response?.data;
if (errorData is Map<String, dynamic> && errorData.containsKey('error')) {
throw Exception('Données invalides: ${errorData['error']}');
}
}
throw Exception('Erreur réseau lors de la mise à jour de l\'organisation: ${e.message}');
} catch (e) {
throw Exception('Erreur inattendue lors de la mise à jour de l\'organisation: $e');
}
}
@override
Future<void> deleteOrganization(String id) async {
try {
final response = await _dio.delete('$_baseUrl/$id');
if (response.statusCode != 200 && response.statusCode != 204) {
throw Exception('Erreur lors de la suppression de l\'organisation: ${response.statusCode}');
}
} on DioException catch (e) {
if (e.response?.statusCode == 404) {
throw Exception('Organisation non trouvée');
} else if (e.response?.statusCode == 400) {
final errorData = e.response?.data;
if (errorData is Map<String, dynamic> && errorData.containsKey('error')) {
throw Exception('Impossible de supprimer: ${errorData['error']}');
}
}
throw Exception('Erreur réseau lors de la suppression de l\'organisation: ${e.message}');
} catch (e) {
throw Exception('Erreur inattendue lors de la suppression de l\'organisation: $e');
}
}
@override
Future<OrganizationModel> activateOrganization(String id) async {
try {
final response = await _dio.post('$_baseUrl/$id/activer');
if (response.statusCode == 200) {
return OrganizationModel.fromJson(response.data as Map<String, dynamic>);
} else {
throw Exception('Erreur lors de l\'activation de l\'organisation: ${response.statusCode}');
}
} on DioException catch (e) {
if (e.response?.statusCode == 404) {
throw Exception('Organisation non trouvée');
}
throw Exception('Erreur réseau lors de l\'activation de l\'organisation: ${e.message}');
} catch (e) {
throw Exception('Erreur inattendue lors de l\'activation de l\'organisation: $e');
}
}
@override
Future<List<OrganizationModel>> searchOrganizations({
String? nom,
TypeOrganization? type,
StatutOrganization? statut,
String? ville,
String? region,
String? pays,
int page = 0,
int size = 20,
}) async {
try {
final queryParams = <String, dynamic>{
'page': page,
'size': size,
};
if (nom?.isNotEmpty == true) queryParams['nom'] = nom;
if (type != null) queryParams['type'] = type.name.toUpperCase();
if (statut != null) queryParams['statut'] = statut.name.toUpperCase();
if (ville?.isNotEmpty == true) queryParams['ville'] = ville;
if (region?.isNotEmpty == true) queryParams['region'] = region;
if (pays?.isNotEmpty == true) queryParams['pays'] = pays;
final response = await _dio.get(
'$_baseUrl/recherche',
queryParameters: queryParams,
);
if (response.statusCode == 200) {
final List<dynamic> data = response.data as List<dynamic>;
return data
.map((json) => OrganizationModel.fromJson(json as Map<String, dynamic>))
.toList();
} else {
throw Exception('Erreur lors de la recherche d\'organisations: ${response.statusCode}');
}
} on DioException catch (e) {
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');
}
}
@override
Future<Map<String, dynamic>> getOrganizationsStats() async {
try {
final response = await _dio.get('$_baseUrl/statistiques');
if (response.statusCode == 200) {
return response.data as Map<String, dynamic>;
} else {
throw Exception('Erreur lors de la récupération des statistiques: ${response.statusCode}');
}
} on DioException catch (e) {
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');
}
}
/// Données de démonstration pour le développement
List<OrganizationModel> _getMockOrganizations({
int page = 0,
int size = 20,
String? recherche,
}) {
final mockData = [
OrganizationModel(
id: '1',
nom: 'Syndicat des Travailleurs Unis',
nomCourt: 'STU',
description: 'Organisation syndicale représentant les travailleurs de l\'industrie',
typeOrganisation: TypeOrganization.syndicat,
statut: StatutOrganization.active,
adresse: '123 Rue de la République',
ville: 'Paris',
codePostal: '75001',
region: 'Île-de-France',
pays: 'France',
telephone: '+33 1 23 45 67 89',
email: 'contact@stu.fr',
siteWeb: 'https://www.stu.fr',
nombreMembres: 1250,
budgetAnnuel: 500000.0,
montantCotisationAnnuelle: 120.0,
dateCreation: DateTime(2020, 1, 15),
dateModification: DateTime.now(),
),
OrganizationModel(
id: '2',
nom: 'Association des Professionnels de la Santé',
nomCourt: 'APS',
description: 'Association regroupant les professionnels du secteur médical',
typeOrganisation: TypeOrganization.association,
statut: StatutOrganization.active,
adresse: '456 Avenue de la Santé',
ville: 'Lyon',
codePostal: '69000',
region: 'Auvergne-Rhône-Alpes',
pays: 'France',
telephone: '+33 4 78 90 12 34',
email: 'info@aps-sante.fr',
siteWeb: 'https://www.aps-sante.fr',
nombreMembres: 850,
budgetAnnuel: 300000.0,
montantCotisationAnnuelle: 80.0,
dateCreation: DateTime(2019, 6, 10),
dateModification: DateTime.now(),
),
OrganizationModel(
id: '3',
nom: 'Coopérative Agricole du Sud',
nomCourt: 'CAS',
description: 'Coopérative regroupant les agriculteurs de la région Sud',
typeOrganisation: TypeOrganization.cooperative,
statut: StatutOrganization.active,
adresse: '789 Route des Champs',
ville: 'Marseille',
codePostal: '13000',
region: 'Provence-Alpes-Côte d\'Azur',
pays: 'France',
telephone: '+33 4 91 23 45 67',
email: 'contact@cas-agricole.fr',
siteWeb: 'https://www.cas-agricole.fr',
nombreMembres: 420,
budgetAnnuel: 750000.0,
montantCotisationAnnuelle: 200.0,
dateCreation: DateTime(2018, 3, 20),
dateModification: DateTime.now(),
),
OrganizationModel(
id: '4',
nom: 'Fédération des Artisans',
nomCourt: 'FA',
description: 'Fédération représentant les artisans de tous secteurs',
typeOrganisation: TypeOrganization.fondation,
statut: StatutOrganization.inactive,
adresse: '321 Rue de l\'Artisanat',
ville: 'Toulouse',
codePostal: '31000',
region: 'Occitanie',
pays: 'France',
telephone: '+33 5 61 78 90 12',
email: 'secretariat@federation-artisans.fr',
siteWeb: 'https://www.federation-artisans.fr',
nombreMembres: 680,
budgetAnnuel: 400000.0,
montantCotisationAnnuelle: 150.0,
dateCreation: DateTime(2017, 9, 5),
dateModification: DateTime.now(),
),
OrganizationModel(
id: '5',
nom: 'Union des Commerçants',
nomCourt: 'UC',
description: 'Union regroupant les commerçants locaux',
typeOrganisation: TypeOrganization.entreprise,
statut: StatutOrganization.active,
adresse: '654 Boulevard du Commerce',
ville: 'Bordeaux',
codePostal: '33000',
region: 'Nouvelle-Aquitaine',
pays: 'France',
telephone: '+33 5 56 34 12 78',
email: 'contact@union-commercants.fr',
siteWeb: 'https://www.union-commercants.fr',
nombreMembres: 320,
budgetAnnuel: 180000.0,
montantCotisationAnnuelle: 90.0,
dateCreation: DateTime(2021, 11, 12),
dateModification: DateTime.now(),
),
];
// Filtrer par recherche si nécessaire
List<OrganizationModel> filteredData = mockData;
if (recherche?.isNotEmpty == true) {
final query = recherche!.toLowerCase();
filteredData = mockData.where((org) =>
org.nom.toLowerCase().contains(query) ||
(org.nomCourt?.toLowerCase().contains(query) ?? false) ||
(org.description?.toLowerCase().contains(query) ?? false) ||
(org.ville?.toLowerCase().contains(query) ?? false)
).toList();
}
// Pagination
final startIndex = page * size;
final endIndex = (startIndex + size).clamp(0, filteredData.length);
if (startIndex >= filteredData.length) {
return [];
}
return filteredData.sublist(startIndex, endIndex);
}
}

View File

@@ -0,0 +1,316 @@
/// Service pour la gestion des organisations
/// Couche de logique métier entre le repository et l'interface utilisateur
library organization_service;
import '../models/organization_model.dart';
import '../repositories/organization_repository.dart';
/// Service de gestion des organisations
class OrganizationService {
final OrganizationRepository _repository;
OrganizationService(this._repository);
/// Récupère la liste des organisations avec pagination et recherche
Future<List<OrganizationModel>> getOrganizations({
int page = 0,
int size = 20,
String? recherche,
}) async {
try {
return await _repository.getOrganizations(
page: page,
size: size,
recherche: recherche,
);
} catch (e) {
throw Exception('Erreur lors de la récupération des organisations: $e');
}
}
/// Récupère une organisation par son ID
Future<OrganizationModel?> getOrganizationById(String id) async {
if (id.isEmpty) {
throw ArgumentError('L\'ID de l\'organisation ne peut pas être vide');
}
try {
return await _repository.getOrganizationById(id);
} catch (e) {
throw Exception('Erreur lors de la récupération de l\'organisation: $e');
}
}
/// Crée une nouvelle organisation avec validation
Future<OrganizationModel> createOrganization(OrganizationModel organization) async {
// Validation des données obligatoires
_validateOrganization(organization);
try {
return await _repository.createOrganization(organization);
} catch (e) {
throw Exception('Erreur lors de la création de l\'organisation: $e');
}
}
/// Met à jour une organisation avec validation
Future<OrganizationModel> updateOrganization(String id, OrganizationModel organization) async {
if (id.isEmpty) {
throw ArgumentError('L\'ID de l\'organisation ne peut pas être vide');
}
// Validation des données obligatoires
_validateOrganization(organization);
try {
return await _repository.updateOrganization(id, organization);
} catch (e) {
throw Exception('Erreur lors de la mise à jour de l\'organisation: $e');
}
}
/// Supprime une organisation
Future<void> deleteOrganization(String id) async {
if (id.isEmpty) {
throw ArgumentError('L\'ID de l\'organisation ne peut pas être vide');
}
try {
await _repository.deleteOrganization(id);
} catch (e) {
throw Exception('Erreur lors de la suppression de l\'organisation: $e');
}
}
/// Active une organisation
Future<OrganizationModel> activateOrganization(String id) async {
if (id.isEmpty) {
throw ArgumentError('L\'ID de l\'organisation ne peut pas être vide');
}
try {
return await _repository.activateOrganization(id);
} catch (e) {
throw Exception('Erreur lors de l\'activation de l\'organisation: $e');
}
}
/// Recherche avancée d'organisations
Future<List<OrganizationModel>> searchOrganizations({
String? nom,
TypeOrganization? type,
StatutOrganization? statut,
String? ville,
String? region,
String? pays,
int page = 0,
int size = 20,
}) async {
try {
return await _repository.searchOrganizations(
nom: nom,
type: type,
statut: statut,
ville: ville,
region: region,
pays: pays,
page: page,
size: size,
);
} catch (e) {
throw Exception('Erreur lors de la recherche d\'organisations: $e');
}
}
/// Récupère les statistiques des organisations
Future<Map<String, dynamic>> getOrganizationsStats() async {
try {
return await _repository.getOrganizationsStats();
} catch (e) {
throw Exception('Erreur lors de la récupération des statistiques: $e');
}
}
/// Filtre les organisations par statut
List<OrganizationModel> filterByStatus(
List<OrganizationModel> organizations,
StatutOrganization statut,
) {
return organizations.where((org) => org.statut == statut).toList();
}
/// Filtre les organisations par type
List<OrganizationModel> filterByType(
List<OrganizationModel> organizations,
TypeOrganization type,
) {
return organizations.where((org) => org.typeOrganisation == type).toList();
}
/// Trie les organisations par nom
List<OrganizationModel> sortByName(
List<OrganizationModel> organizations, {
bool ascending = true,
}) {
final sorted = List<OrganizationModel>.from(organizations);
sorted.sort((a, b) {
final comparison = a.nom.toLowerCase().compareTo(b.nom.toLowerCase());
return ascending ? comparison : -comparison;
});
return sorted;
}
/// Trie les organisations par date de création
List<OrganizationModel> sortByCreationDate(
List<OrganizationModel> organizations, {
bool ascending = true,
}) {
final sorted = List<OrganizationModel>.from(organizations);
sorted.sort((a, b) {
final dateA = a.dateCreation ?? DateTime.fromMillisecondsSinceEpoch(0);
final dateB = b.dateCreation ?? DateTime.fromMillisecondsSinceEpoch(0);
final comparison = dateA.compareTo(dateB);
return ascending ? comparison : -comparison;
});
return sorted;
}
/// Trie les organisations par nombre de membres
List<OrganizationModel> sortByMemberCount(
List<OrganizationModel> organizations, {
bool ascending = true,
}) {
final sorted = List<OrganizationModel>.from(organizations);
sorted.sort((a, b) {
final comparison = a.nombreMembres.compareTo(b.nombreMembres);
return ascending ? comparison : -comparison;
});
return sorted;
}
/// Recherche locale dans une liste d'organisations
List<OrganizationModel> searchLocal(
List<OrganizationModel> organizations,
String query,
) {
if (query.isEmpty) return organizations;
final lowerQuery = query.toLowerCase();
return organizations.where((org) {
return org.nom.toLowerCase().contains(lowerQuery) ||
(org.nomCourt?.toLowerCase().contains(lowerQuery) ?? false) ||
(org.description?.toLowerCase().contains(lowerQuery) ?? false) ||
(org.ville?.toLowerCase().contains(lowerQuery) ?? false) ||
(org.region?.toLowerCase().contains(lowerQuery) ?? false);
}).toList();
}
/// Calcule les statistiques locales d'une liste d'organisations
Map<String, dynamic> calculateLocalStats(List<OrganizationModel> organizations) {
if (organizations.isEmpty) {
return {
'total': 0,
'actives': 0,
'inactives': 0,
'totalMembres': 0,
'moyenneMembres': 0.0,
'parType': <String, int>{},
'parStatut': <String, int>{},
};
}
final actives = organizations.where((org) => org.statut == StatutOrganization.active).length;
final inactives = organizations.length - actives;
final totalMembres = organizations.fold<int>(0, (sum, org) => sum + org.nombreMembres);
final moyenneMembres = totalMembres / organizations.length;
// Statistiques par type
final parType = <String, int>{};
for (final org in organizations) {
final type = org.typeOrganisation.displayName;
parType[type] = (parType[type] ?? 0) + 1;
}
// Statistiques par statut
final parStatut = <String, int>{};
for (final org in organizations) {
final statut = org.statut.displayName;
parStatut[statut] = (parStatut[statut] ?? 0) + 1;
}
return {
'total': organizations.length,
'actives': actives,
'inactives': inactives,
'totalMembres': totalMembres,
'moyenneMembres': moyenneMembres,
'parType': parType,
'parStatut': parStatut,
};
}
/// Validation des données d'organisation
void _validateOrganization(OrganizationModel organization) {
if (organization.nom.trim().isEmpty) {
throw ArgumentError('Le nom de l\'organisation est obligatoire');
}
if (organization.nom.trim().length < 2) {
throw ArgumentError('Le nom de l\'organisation doit contenir au moins 2 caractères');
}
if (organization.nom.trim().length > 200) {
throw ArgumentError('Le nom de l\'organisation ne peut pas dépasser 200 caractères');
}
if (organization.nomCourt != null && organization.nomCourt!.length > 50) {
throw ArgumentError('Le nom court ne peut pas dépasser 50 caractères');
}
if (organization.email != null && organization.email!.isNotEmpty) {
if (!_isValidEmail(organization.email!)) {
throw ArgumentError('L\'adresse email n\'est pas valide');
}
}
if (organization.telephone != null && organization.telephone!.isNotEmpty) {
if (!_isValidPhone(organization.telephone!)) {
throw ArgumentError('Le numéro de téléphone n\'est pas valide');
}
}
if (organization.siteWeb != null && organization.siteWeb!.isNotEmpty) {
if (!_isValidUrl(organization.siteWeb!)) {
throw ArgumentError('L\'URL du site web n\'est pas valide');
}
}
if (organization.budgetAnnuel != null && organization.budgetAnnuel! < 0) {
throw ArgumentError('Le budget annuel doit être positif');
}
if (organization.montantCotisationAnnuelle != null && organization.montantCotisationAnnuelle! < 0) {
throw ArgumentError('Le montant de cotisation doit être positif');
}
}
/// Validation d'email
bool _isValidEmail(String email) {
return RegExp(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$').hasMatch(email);
}
/// Validation de téléphone
bool _isValidPhone(String phone) {
return RegExp(r'^\+?[0-9\s\-\(\)]{8,15}$').hasMatch(phone);
}
/// Validation d'URL
bool _isValidUrl(String url) {
try {
final uri = Uri.parse(url);
return uri.hasScheme && (uri.scheme == 'http' || uri.scheme == 'https');
} catch (e) {
return false;
}
}
}