feat: PHASE 1 - Adresses et Rôles/Permissions

PHASE 1.1 - Entité Adresse:
- Création entité Adresse avec types (SIEGE_SOCIAL, BUREAU, DOMICILE, AUTRE)
- Relations flexibles: Organisation, Membre, Evenement
- Enum TypeAdresse dans module API (DRY/WOU)
- Repository et Service AdresseService
- Relations bidirectionnelles mises à jour

PHASE 1.2 - Système Rôles et Permissions:
- Entité Role avec types (SYSTEME, ORGANISATION, PERSONNALISE)
- Entité Permission avec structure MODULE > RESSOURCE > ACTION
- Tables de liaison MembreRole et RolePermission
- Repositories pour toutes les entités
- Services RoleService et PermissionService
- Relations bidirectionnelles dans Membre

Respect strict DRY/WOU:
- Enums dans module API réutilisables
- Patterns de service cohérents
- Relations JPA standardisées
This commit is contained in:
dahoud
2025-11-30 01:31:12 +00:00
parent e26ae459e4
commit f930ae7341
25 changed files with 3583 additions and 91 deletions

View File

@@ -0,0 +1,310 @@
# Plan d'Implémentation - Architecture UnionFlow v3.0
**Date** : 2025-01-29
**Objectif** : Aligner le code actuel avec l'architecture cible (union-flow.puml)
---
## 📊 État Actuel vs Architecture Cible
### ✅ Entités Existantes
- ✅ BaseEntity
- ✅ Organisation
- ✅ TypeOrganisationEntity
- ✅ Membre
- ✅ Cotisation
- ✅ Adhesion
- ✅ Evenement
- ✅ InscriptionEvenement
- ✅ DemandeAide
- ✅ AuditLog
### ❌ Entités Manquantes
1. **Paiements** : Paiement, PaiementCotisation, PaiementAdhesion, PaiementEvenement, PaiementAide
2. **Wave** : CompteWave, TransactionWave, WebhookWave, ConfigurationWave
3. **Comptabilité** : CompteComptable, JournalComptable, EcritureComptable, LigneEcriture
4. **Documents** : Document, PieceJointe
5. **Notifications** : Notification, TemplateNotification
6. **Rôles/Permissions** : Role, Permission, MembreRole, RolePermission
7. **Adresses** : Adresse (séparée)
---
## 🎯 Plan d'Implémentation par Étapes
### **PHASE 1 : FONDATIONS - Adresses et Rôles** (Priorité HAUTE)
**Durée estimée** : 2-3 jours
#### Étape 1.1 : Entité Adresse
- [ ] Créer `Adresse.java` (entité séparée)
- [ ] Types d'adresse : SIEGE_SOCIAL, BUREAU, DOMICILE, AUTRE
- [ ] Relations : Organisation ↔ Adresse (0..*), Membre ↔ Adresse (0..*)
- [ ] Migration : Extraire adresses de Organisation et Membre
- [ ] Repository : `AdresseRepository`
- [ ] Service : `AdresseService`
- [ ] DTO : `AdresseDTO`
#### Étape 1.2 : Système de Rôles et Permissions
- [ ] Créer `Role.java` (entité)
- [ ] Créer `Permission.java` (entité)
- [ ] Créer `MembreRole.java` (table de liaison)
- [ ] Créer `RolePermission.java` (table de liaison)
- [ ] Enums : TypeRole, TypePermission
- [ ] Repository : `RoleRepository`, `PermissionRepository`, `MembreRoleRepository`, `RolePermissionRepository`
- [ ] Service : `RoleService`, `PermissionService`
- [ ] DTOs : `RoleDTO`, `PermissionDTO`, `MembreRoleDTO`
---
### **PHASE 2 : SYSTÈME DE PAIEMENTS CENTRALISÉ** (Priorité CRITIQUE)
**Durée estimée** : 3-4 jours
#### Étape 2.1 : Entité Paiement
- [ ] Créer `Paiement.java` (entité centrale)
- [ ] Enum : `MethodePaiement` (WAVE_MOBILE_MONEY, ORANGE_MONEY, MTN_MOBILE_MONEY, etc.)
- [ ] Enum : `StatutPaiement` (EN_ATTENTE, EN_COURS, VALIDE, ECHOUE, ANNULE, REMBOURSE)
- [ ] Champs : montant, devise, datePaiement, dateValidation, validateur, references externes
- [ ] Relation : Paiement → Membre (1-N)
- [ ] Repository : `PaiementRepository`
- [ ] Service : `PaiementService`
- [ ] DTO : `PaiementDTO`
#### Étape 2.2 : Tables de Liaison Paiements
- [ ] Créer `PaiementCotisation.java` (table de liaison)
- [ ] Créer `PaiementAdhesion.java` (table de liaison)
- [ ] Créer `PaiementEvenement.java` (table de liaison)
- [ ] Créer `PaiementAide.java` (table de liaison)
- [ ] Champs communs : montantApplique, dateApplication
- [ ] Relations : Paiement ↔ Cotisation/Adhesion/Evenement/Aide
- [ ] Repositories : `PaiementCotisationRepository`, etc.
- [ ] Services : Logique d'application des paiements
#### Étape 2.3 : Refactoring Cotisation et Adhesion
- [ ] Modifier `Cotisation.java` : Retirer montantPaye, utiliser PaiementCotisation
- [ ] Modifier `Adhesion.java` : Retirer montantPaye, utiliser PaiementAdhesion
- [ ] Mettre à jour `CotisationService` : Utiliser PaiementService
- [ ] Mettre à jour `AdhesionService` : Utiliser PaiementService
- [ ] Migration des données existantes
---
### **PHASE 3 : INTÉGRATION WAVE MOBILE MONEY** (Priorité CRITIQUE)
**Durée estimée** : 4-5 jours
#### Étape 3.1 : Entités Wave
- [ ] Créer `CompteWave.java`
- Numéro téléphone (+225XXXXXXXX)
- Statut : NON_VERIFIE, VERIFIE, SUSPENDU, BLOQUE
- Relations : Organisation (1-N), Membre (0..1)
- Identifiants API encryptés
- [ ] Créer `TransactionWave.java`
- Identifiants Wave (transactionId, requestId, reference)
- Type : DEPOT, RETRAIT, TRANSFERT, PAIEMENT, REMBOURSEMENT
- Statut : INITIALISE, EN_ATTENTE, EN_COURS, REUSSIE, ECHOUE, ANNULEE, EXPIRED
- Montant, frais, montant net
- Métadonnées JSON
- Relation : CompteWave (1-N), Paiement (0..1)
- [ ] Créer `WebhookWave.java`
- Wave event ID
- Type d'événement
- Statut traitement : EN_ATTENTE, EN_TRAITEMENT, TRAITE, ECHOUE, IGNORE
- Payload JSON
- Relation : TransactionWave (0..1), Paiement (0..1)
- [ ] Créer `ConfigurationWave.java`
- Clé-valeur pour configuration
- Support sandbox/production
#### Étape 3.2 : Repositories et Services Wave
- [ ] Repositories : `CompteWaveRepository`, `TransactionWaveRepository`, `WebhookWaveRepository`, `ConfigurationWaveRepository`
- [ ] Service : `WaveService` (intégration API Wave)
- Méthodes : initierPaiement, verifierTransaction, traiterWebhook
- Gestion retry avec backoff exponentiel
- Validation de signature webhook
- Chiffrement des clés API
#### Étape 3.3 : Intégration avec PaiementService
- [ ] Modifier `PaiementService` : Support Wave
- [ ] Workflow : Initiation → TransactionWave → Webhook → Validation
- [ ] Gestion des erreurs et retry
---
### **PHASE 4 : COMPTABILITÉ** (Priorité MOYENNE)
**Durée estimée** : 3-4 jours
#### Étape 4.1 : Plan Comptable
- [ ] Créer `CompteComptable.java`
- Numéro compte unique
- Type : ACTIF, PASSIF, CHARGES, PRODUITS, TRESORERIE, AUTRE
- Classe comptable (1-7)
- Solde initial, solde actuel
- [ ] Repository : `CompteComptableRepository`
- [ ] Service : `CompteComptableService`
- [ ] DTO : `CompteComptableDTO`
#### Étape 4.2 : Journaux et Écritures
- [ ] Créer `JournalComptable.java`
- Code unique
- Type : ACHATS, VENTES, BANQUE, CAISSE, OD
- Période, statut
- [ ] Créer `EcritureComptable.java`
- Numéro pièce unique
- Date, libellé, référence
- Lettrage, pointage
- Relation : JournalComptable (1-N), Organisation (1-N), Paiement (0..1)
- [ ] Créer `LigneEcriture.java`
- Numéro ligne
- Compte débiteur/créditeur
- Montant débit/crédit
- Relation : EcritureComptable (1-N), CompteComptable (1-N)
- Validation : Débit = Crédit
#### Étape 4.3 : Service Comptable
- [ ] Service : `ComptabiliteService`
- Génération automatique d'écritures pour paiements
- Rapprochement bancaire
- Pointage et lettrage
- [ ] Intégration avec PaiementService
---
### **PHASE 5 : GESTION DOCUMENTAIRE** (Priorité MOYENNE)
**Durée estimée** : 2-3 jours
#### Étape 5.1 : Entités Documents
- [ ] Créer `Document.java`
- Nom fichier, nom original
- Chemin stockage
- Type MIME, taille
- Hash MD5, SHA256
- Type : IDENTITE, JUSTIFICATIF_DOMICILE, PHOTO, CONTRAT, FACTURE, RECU, RAPPORT, AUTRE
- [ ] Créer `PieceJointe.java`
- Ordre d'affichage
- Libellé, commentaire
- Relations flexibles : Membre, Organisation, Cotisation, Adhesion, DemandeAide, TransactionWave
#### Étape 5.2 : Services Documents
- [ ] Repositories : `DocumentRepository`, `PieceJointeRepository`
- [ ] Service : `DocumentService`
- Upload sécurisé
- Vérification intégrité (hash)
- Contrôle d'accès
- Audit téléchargements
---
### **PHASE 6 : SYSTÈME DE NOTIFICATIONS** (Priorité MOYENNE)
**Durée estimée** : 2-3 jours
#### Étape 6.1 : Entités Notifications
- [ ] Créer `TemplateNotification.java`
- Code unique
- Sujet, corps (texte et HTML)
- Variables disponibles (JSON)
- Canaux supportés
- Support multi-langues
- [ ] Créer `Notification.java`
- Type : EMAIL, SMS, PUSH, IN_APP, SYSTEME
- Priorité : CRITIQUE, HAUTE, NORMALE, BASSE
- Statut : EN_ATTENTE, ENVOYEE, LUE, ECHOUE, ANNULEE
- Relations : Membre (1-N), Organisation (0..1), TemplateNotification (0..1)
#### Étape 6.2 : Service Notifications
- [ ] Repositories : `NotificationRepository`, `TemplateNotificationRepository`
- [ ] Service : `NotificationService`
- Envoi multi-canaux
- Retry automatique
- Priorisation
- Templates réutilisables
---
### **PHASE 7 : MISE À JOUR MEMBRE** (Priorité HAUTE)
**Durée estimée** : 1-2 jours
#### Étape 7.1 : Ajout Champs Membre
- [ ] Ajouter `telephoneWave` (String, format +225XXXXXXXX)
- [ ] Ajouter `photoUrl` (String)
- [ ] Relation : Membre → CompteWave (0..1)
- [ ] Relation : Membre → Adresse (0..*)
- [ ] Relation : Membre → MembreRole (1-N)
#### Étape 7.2 : Migration Données
- [ ] Script de migration pour extraire adresses
- [ ] Attribution rôles par défaut
- [ ] Validation format téléphone Wave
---
### **PHASE 8 : MISE À JOUR ORGANISATION** (Priorité MOYENNE)
**Durée estimée** : 1 jour
#### Étape 8.1 : Relations Organisation
- [ ] Relation : Organisation → CompteWave (1-N)
- [ ] Relation : Organisation → Adresse (0..*)
- [ ] Migration : Extraire adresses vers entité Adresse
---
### **PHASE 9 : MISE À JOUR ÉVÉNEMENT** (Priorité MOYENNE)
**Durée estimée** : 1 jour
#### Étape 9.1 : Relations Evenement
- [ ] Relation : Evenement → Adresse (0..1)
- [ ] Relation : Evenement → PaiementEvenement (0..*)
- [ ] Migration : Extraire adresse vers entité Adresse
---
### **PHASE 10 : RESSOURCES REST ET DTOs** (Priorité HAUTE)
**Durée estimée** : 3-4 jours
#### Étape 10.1 : DTOs API
- [ ] Créer tous les DTOs manquants dans `unionflow-server-api`
- [ ] Enums dans `unionflow-server-api`
#### Étape 10.2 : Resources REST
- [ ] `PaiementResource`
- [ ] `WaveResource` (CompteWave, TransactionWave, WebhookWave)
- [ ] `ComptabiliteResource` (CompteComptable, JournalComptable, EcritureComptable)
- [ ] `DocumentResource`
- [ ] `NotificationResource`
- [ ] `RoleResource`, `PermissionResource`
- [ ] `AdresseResource`
---
### **PHASE 11 : TESTS ET VALIDATION** (Priorité HAUTE)
**Durée estimée** : 2-3 jours
#### Étape 11.1 : Tests Unitaires
- [ ] Tests pour toutes les nouvelles entités
- [ ] Tests pour tous les services
- [ ] Tests d'intégration Wave (mock)
#### Étape 11.2 : Tests d'Intégration
- [ ] Tests de workflow complet paiement
- [ ] Tests webhooks Wave
- [ ] Tests génération écritures comptables
---
## 📋 Ordre d'Implémentation Recommandé
1. **PHASE 1** : Adresses et Rôles (fondations)
2. **PHASE 2** : Système de Paiements (critique)
3. **PHASE 7** : Mise à jour Membre (dépend de Phase 1)
4. **PHASE 3** : Intégration Wave (dépend de Phase 2)
5. **PHASE 4** : Comptabilité (dépend de Phase 2)
6. **PHASE 5** : Documents
7. **PHASE 6** : Notifications
8. **PHASE 8-9** : Mises à jour Organisation/Evenement
9. **PHASE 10** : Resources REST
10. **PHASE 11** : Tests
---
## 🚀 Démarrage de l'Implémentation
**Prochaine étape** : Commencer par la PHASE 1 - Étape 1.1 : Création de l'entité Adresse

275
union-flow.puml Normal file
View File

@@ -0,0 +1,275 @@
@startuml MCD_UnionFlow
!theme plain
skinparam linetype ortho
skinparam packageStyle rectangle
skinparam classAttributeIconSize 0
title UnionFlow - MCD avec Wave Mobile Money
package Base {
abstract class BaseEntity {
+ id : UUID
+ dateCreation : LocalDateTime
+ version : Long
+ actif : Boolean
}
}
package Orgs {
class Organisation {
+ nom : String
+ email : String
+ statut : StatutOrg
}
class TypeOrganisation {
+ code : String
}
class Adresse {
+ ville : String
+ pays : String
}
}
package Membres {
class Membre {
+ numeroMembre : String
+ email : String
+ telephoneWave : String
}
class Role {
+ code : String
}
class Permission {
+ code : String
}
class MembreRole {
}
class RolePermission {
}
}
package Paiements {
class Paiement {
+ montant : BigDecimal
+ methodePaiement : MethodePmt
+ statutPaiement : StatutPmt
}
class Cotisation {
+ montantDu : BigDecimal
+ statut : StatutCot
}
class PaiementCotisation {
+ montantApplique : BigDecimal
}
class Adhesion {
+ fraisAdhesion : BigDecimal
+ statut : StatutAdh
}
class PaiementAdhesion {
+ montantApplique : BigDecimal
}
}
package Wave {
class CompteWave {
+ numeroTelephone : String
+ statutCompte : StatutWave
}
class TransactionWave {
+ waveTransactionId : String
+ montant : BigDecimal
+ statutTransaction : StatutTxWave
}
class WebhookWave {
+ waveEventId : String
+ statutTraitement : StatutWebhook
}
class ConfigurationWave {
+ cle : String
+ valeur : String
}
}
package Compta {
class CompteComptable {
+ numeroCompte : String
+ soldeActuel : BigDecimal
}
class JournalComptable {
+ code : String
}
class EcritureComptable {
+ montantDebit : BigDecimal
+ montantCredit : BigDecimal
}
class LigneEcriture {
+ montantDebit : BigDecimal
+ montantCredit : BigDecimal
}
}
package Evenements {
class Evenement {
+ titre : String
+ typeEvenement : TypeEvt
+ statut : StatutEvt
}
class InscriptionEvenement {
+ statut : StatutInsc
}
class PaiementEvenement {
+ montantApplique : BigDecimal
}
}
package Solidarite {
class DemandeAide {
+ typeAide : TypeAide
+ statut : StatutAide
+ montantDemande : BigDecimal
}
class PaiementAide {
+ montantApplique : BigDecimal
}
}
package Docs {
class Document {
+ nomFichier : String
+ hashMd5 : String
}
class PieceJointe {
+ ordre : Integer
}
}
package Notifs {
class Notification {
+ typeNotification : TypeNotif
+ statut : StatutNotif
}
class TemplateNotification {
+ code : String
}
}
package Audit {
class AuditLog {
+ typeAction : TypeAction
+ severite : Severite
}
class ParametreSysteme {
+ cle : String
+ valeur : String
}
}
BaseEntity <|-- Organisation
BaseEntity <|-- TypeOrganisation
BaseEntity <|-- Adresse
BaseEntity <|-- Membre
BaseEntity <|-- Role
BaseEntity <|-- Permission
BaseEntity <|-- MembreRole
BaseEntity <|-- RolePermission
BaseEntity <|-- Paiement
BaseEntity <|-- Cotisation
BaseEntity <|-- PaiementCotisation
BaseEntity <|-- Adhesion
BaseEntity <|-- PaiementAdhesion
BaseEntity <|-- CompteWave
BaseEntity <|-- TransactionWave
BaseEntity <|-- WebhookWave
BaseEntity <|-- ConfigurationWave
BaseEntity <|-- CompteComptable
BaseEntity <|-- JournalComptable
BaseEntity <|-- EcritureComptable
BaseEntity <|-- LigneEcriture
BaseEntity <|-- Evenement
BaseEntity <|-- InscriptionEvenement
BaseEntity <|-- PaiementEvenement
BaseEntity <|-- DemandeAide
BaseEntity <|-- PaiementAide
BaseEntity <|-- Document
BaseEntity <|-- PieceJointe
BaseEntity <|-- Notification
BaseEntity <|-- TemplateNotification
BaseEntity <|-- AuditLog
BaseEntity <|-- ParametreSysteme
Organisation "1" *-- "0..*" Membre
Organisation "0..1" --o "0..*" Organisation
Organisation "1" *-- "1" TypeOrganisation
Organisation "0..1" --o "0..*" Adresse
Organisation "1" *-- "0..*" CompteWave
Membre "1" *-- "0..*" MembreRole
MembreRole "1" *-- "1" Role
Membre "0..1" --o "0..*" Adresse
Membre "0..1" --o "0..*" CompteWave
Role "1" *-- "0..*" RolePermission
RolePermission "1" *-- "1" Permission
Paiement "1" *-- "0..*" PaiementCotisation
Paiement "1" *-- "0..*" PaiementAdhesion
Paiement "1" *-- "0..*" PaiementEvenement
Paiement "1" *-- "0..*" PaiementAide
Paiement "0..1" --o "1" TransactionWave
Paiement "1" *-- "1" Membre
Cotisation "1" *-- "0..*" PaiementCotisation
PaiementCotisation "1" *-- "1" Paiement
Cotisation "1" *-- "1" Membre
Cotisation "1" *-- "1" Organisation
Adhesion "1" *-- "0..*" PaiementAdhesion
PaiementAdhesion "1" *-- "1" Paiement
Adhesion "1" *-- "1" Membre
Adhesion "1" *-- "1" Organisation
CompteWave "1" *-- "0..*" TransactionWave
TransactionWave "1" *-- "0..*" WebhookWave
WebhookWave "0..1" --o "1" TransactionWave
WebhookWave "0..1" --o "1" Paiement
CompteWave "1" *-- "1" Organisation
CompteWave "0..1" --o "1" Membre
JournalComptable "1" *-- "0..*" EcritureComptable
EcritureComptable "1" *-- "1..*" LigneEcriture
LigneEcriture "1" *-- "1" CompteComptable
EcritureComptable "0..1" --o "1" Paiement
EcritureComptable "1" *-- "1" Organisation
Evenement "1" *-- "0..*" InscriptionEvenement
InscriptionEvenement "1" *-- "1" Membre
InscriptionEvenement "1" *-- "1" Evenement
Evenement "1" *-- "1" Organisation
Evenement "0..1" --o "1" Adresse
Evenement "0..1" --o "0..*" PaiementEvenement
PaiementEvenement "1" *-- "1" Paiement
PaiementEvenement "1" *-- "1" InscriptionEvenement
DemandeAide "1" *-- "0..*" PaiementAide
PaiementAide "1" *-- "1" Paiement
DemandeAide "1" *-- "1" Membre
DemandeAide "0..1" --o "1" Membre
DemandeAide "1" *-- "1" Organisation
PieceJointe "1" *-- "1" Document
Document "0..1" --o "0..*" PieceJointe
PieceJointe "0..1" --o "1" Membre
PieceJointe "0..1" --o "1" Organisation
PieceJointe "0..1" --o "1" Cotisation
PieceJointe "0..1" --o "1" Adhesion
PieceJointe "0..1" --o "1" DemandeAide
PieceJointe "0..1" --o "1" TransactionWave
Notification "0..1" --o "1" TemplateNotification
Notification "1" *-- "1" Membre
Notification "0..1" --o "1" Organisation
AuditLog "0..1" --o "1" Membre
AuditLog "0..1" --o "1" Organisation
@enduml

View File

@@ -42,8 +42,6 @@ public class MembreInscriptionBean implements Serializable {
// Propriétés système // Propriétés système
private String numeroGenere; private String numeroGenere;
private String membreIdString; // ID du membre en mode modification
private boolean modeModification = false;
// Informations personnelles // Informations personnelles
private String prenom; private String prenom;
@@ -105,10 +103,8 @@ public class MembreInscriptionBean implements Serializable {
@PostConstruct @PostConstruct
public void init() { public void init() {
// Générer un numéro de membre automatiquement (seulement en mode création) // Générer un numéro de membre automatiquement
if (!modeModification) { this.numeroGenere = "M" + System.currentTimeMillis();
this.numeroGenere = "M" + System.currentTimeMillis();
}
// Charger les organisations actives // Charger les organisations actives
try { try {
@@ -120,61 +116,6 @@ public class MembreInscriptionBean implements Serializable {
} }
} }
// Getters/Setters pour mode modification
public String getMembreIdString() {
return membreIdString;
}
public void setMembreIdString(String membreIdString) {
this.membreIdString = membreIdString;
}
public boolean isModeModification() {
return modeModification;
}
// Méthode appelée par f:viewAction pour charger le membre en mode modification
public void chargerMembreSiModification() {
if (membreIdString != null && !membreIdString.isEmpty()) {
try {
java.util.UUID id = java.util.UUID.fromString(membreIdString);
MembreDTO membre = membreService.obtenirParId(id);
if (membre != null) {
modeModification = true;
// Remplir tous les champs avec les données du membre
this.numeroGenere = membre.getNumeroMembre();
this.prenom = membre.getPrenom();
this.nom = membre.getNom();
this.email = membre.getEmail();
this.telephone = membre.getTelephone();
this.dateNaissance = membre.getDateNaissance();
this.adresse = membre.getAdresse();
this.ville = membre.getVille();
this.nationalite = membre.getNationalite();
this.profession = membre.getProfession();
this.situationMatrimoniale = membre.getStatutMatrimonial();
this.organisationId = membre.getAssociationId() != null ? membre.getAssociationId().toString() : null;
// TODO: Charger les autres champs si disponibles
LOGGER.info("Membre chargé pour modification: " + membre.getNomComplet());
} else {
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur", "Membre introuvable"));
}
} catch (IllegalArgumentException e) {
LOGGER.severe("ID invalide: " + membreIdString);
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur", "Identifiant de membre invalide"));
} catch (Exception e) {
LOGGER.severe("Erreur lors du chargement du membre: " + e.getMessage());
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur",
"Impossible de charger le membre: " + e.getMessage()));
}
}
}
// Actions // Actions
public String inscrire() { public String inscrire() {
try { try {

View File

@@ -196,15 +196,15 @@ public class MembreListeBean implements Serializable {
} }
// Propriétés pour la page de modification // Propriétés pour la page de modification
private String membreSelectionneIdString; private UUID membreSelectionneId;
private MembreDTO membreSelectionne; private MembreDTO membreSelectionne;
public String getMembreSelectionneIdString() { public UUID getMembreSelectionneId() {
return membreSelectionneIdString; return membreSelectionneId;
} }
public void setMembreSelectionneIdString(String membreSelectionneIdString) { public void setMembreSelectionneId(UUID membreSelectionneId) {
this.membreSelectionneIdString = membreSelectionneIdString; this.membreSelectionneId = membreSelectionneId;
} }
public MembreDTO getMembreSelectionne() { public MembreDTO getMembreSelectionne() {
@@ -216,16 +216,10 @@ public class MembreListeBean implements Serializable {
} }
public void chargerMembreSelectionne() { public void chargerMembreSelectionne() {
if (membreSelectionneIdString != null && !membreSelectionneIdString.isEmpty()) { if (membreSelectionneId != null) {
try { try {
UUID id = UUID.fromString(membreSelectionneIdString); membreSelectionne = membreService.obtenirParId(membreSelectionneId);
membreSelectionne = membreService.obtenirParId(id);
LOGGER.info("Membre chargé pour modification: " + membreSelectionne.getNomComplet()); LOGGER.info("Membre chargé pour modification: " + membreSelectionne.getNomComplet());
} catch (IllegalArgumentException e) {
LOGGER.severe("ID invalide: " + membreSelectionneIdString);
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur",
"Identifiant de membre invalide"));
} catch (Exception e) { } catch (Exception e) {
LOGGER.severe("Erreur lors du chargement du membre: " + e.getMessage()); LOGGER.severe("Erreur lors du chargement du membre: " + e.getMessage());
FacesContext.getCurrentInstance().addMessage(null, FacesContext.getCurrentInstance().addMessage(null,

View File

@@ -6,24 +6,18 @@
xmlns:p="http://primefaces.org/ui" xmlns:p="http://primefaces.org/ui"
template="/templates/main-template.xhtml"> template="/templates/main-template.xhtml">
<ui:define name="title">#{empty param.id ? 'Inscription' : 'Modification'} Membre - UnionFlow</ui:define> <ui:define name="title">Inscription Membre - UnionFlow</ui:define>
<!-- Charger le membre si ID fourni (mode modification) -->
<f:metadata>
<f:viewParam name="id" value="#{membreInscriptionBean.membreIdString}" />
<f:viewAction action="#{membreInscriptionBean.chargerMembreSiModification}" />
</f:metadata>
<ui:define name="content"> <ui:define name="content">
<!-- En-tête --> <!-- En-tête -->
<ui:include src="/templates/components/layout/page-header.xhtml"> <ui:include src="/templates/components/layout/page-header.xhtml">
<ui:param name="icon" value="#{empty param.id ? 'pi pi-user-plus text-primary' : 'pi pi-pencil text-warning'}" /> <ui:param name="icon" value="pi pi-user-plus text-primary" />
<ui:param name="title" value="#{empty param.id ? 'Inscription Nouveau Membre' : 'Modifier le Membre'}" /> <ui:param name="title" value="Inscription Nouveau Membre" />
<ui:param name="description" value="#{empty param.id ? 'Formulaire complet d\'inscription avec photo et documents' : 'Modifiez les informations du membre'}" /> <ui:param name="description" value="Formulaire complet d'inscription avec photo et documents" />
<ui:define name="actions"> <ui:define name="actions">
<div> <div>
<div class="text-900 font-medium">Numéro: #{membreInscriptionBean.numeroGenere}</div> <div class="text-900 font-medium">Numéro: #{membreInscriptionBean.numeroGenere}</div>
<small class="text-600">#{empty param.id ? 'Généré automatiquement' : 'Existant'}</small> <small class="text-600">Généré automatiquement</small>
</div> </div>
</ui:define> </ui:define>
</ui:include> </ui:include>
@@ -100,7 +94,7 @@
<div class="field"> <div class="field">
<p:outputLabel for="dateNaissance" value="Date de naissance" /> <p:outputLabel for="dateNaissance" value="Date de naissance" />
<p:calendar id="dateNaissance" value="#{membreInscriptionBean.dateNaissance}" <p:calendar id="dateNaissance" value="#{membreInscriptionBean.dateNaissance}" required="true"
pattern="dd/MM/yyyy" showIcon="true" yearNavigator="true" yearRange="1920:2030" pattern="dd/MM/yyyy" showIcon="true" yearNavigator="true" yearRange="1920:2030"
monthNavigator="true" requiredMessage="La date de naissance est obligatoire" monthNavigator="true" requiredMessage="La date de naissance est obligatoire"
styleClass="w-full" /> styleClass="w-full" />
@@ -113,7 +107,7 @@
</div> </div>
<div class="field"> <div class="field">
<p:outputLabel for="sexe" value="Sexe" /> <p:outputLabel for="sexe" value="Sexe" />
<p:selectOneMenu id="sexe" value="#{membreInscriptionBean.sexe}" styleClass="w-full"> <p:selectOneMenu id="sexe" value="#{membreInscriptionBean.sexe}" required="true" styleClass="w-full">
<f:selectItem itemLabel="Sélectionner..." itemValue="" noSelectionOption="true" /> <f:selectItem itemLabel="Sélectionner..." itemValue="" noSelectionOption="true" />
<f:selectItems value="#{membreInscriptionBean.sexeOptions}" /> <f:selectItems value="#{membreInscriptionBean.sexeOptions}" />
</p:selectOneMenu> </p:selectOneMenu>
@@ -148,6 +142,7 @@
<div class="field"> <div class="field">
<p:outputLabel for="contactUrgenceNom" value="Nom complet" /> <p:outputLabel for="contactUrgenceNom" value="Nom complet" />
<p:inputText id="contactUrgenceNom" value="#{membreInscriptionBean.contactUrgenceNom}" <p:inputText id="contactUrgenceNom" value="#{membreInscriptionBean.contactUrgenceNom}"
required="true" requiredMessage="Le nom du contact d'urgence est obligatoire"
styleClass="w-full" /> styleClass="w-full" />
<p:message for="contactUrgenceNom" /> <p:message for="contactUrgenceNom" />
</div> </div>
@@ -155,12 +150,13 @@
<div class="field"> <div class="field">
<p:outputLabel for="contactUrgenceTelephone" value="Téléphone" /> <p:outputLabel for="contactUrgenceTelephone" value="Téléphone" />
<p:inputText id="contactUrgenceTelephone" value="#{membreInscriptionBean.contactUrgenceTelephone}" <p:inputText id="contactUrgenceTelephone" value="#{membreInscriptionBean.contactUrgenceTelephone}"
required="true" requiredMessage="Le téléphone du contact d'urgence est obligatoire"
styleClass="w-full" /> styleClass="w-full" />
<p:message for="contactUrgenceTelephone" /> <p:message for="contactUrgenceTelephone" />
</div> </div>
<div class="field"> <div class="field">
<p:outputLabel for="contactUrgenceLien" value="Lien de parenté" /> <p:outputLabel for="contactUrgenceLien" value="Lien de parenté" />
<p:selectOneMenu id="contactUrgenceLien" value="#{membreInscriptionBean.contactUrgenceLien}" styleClass="w-full"> <p:selectOneMenu id="contactUrgenceLien" value="#{membreInscriptionBean.contactUrgenceLien}" required="true" styleClass="w-full">
<f:selectItem itemLabel="Sélectionner..." itemValue="" noSelectionOption="true" /> <f:selectItem itemLabel="Sélectionner..." itemValue="" noSelectionOption="true" />
<f:selectItems value="#{membreInscriptionBean.contactUrgenceLienOptions}" /> <f:selectItems value="#{membreInscriptionBean.contactUrgenceLienOptions}" />
</p:selectOneMenu> </p:selectOneMenu>
@@ -221,6 +217,7 @@
<div class="field"> <div class="field">
<p:outputLabel for="adresse" value="Adresse complète" /> <p:outputLabel for="adresse" value="Adresse complète" />
<p:inputTextarea id="adresse" value="#{membreInscriptionBean.adresse}" rows="4" <p:inputTextarea id="adresse" value="#{membreInscriptionBean.adresse}" rows="4"
required="true" requiredMessage="L'adresse est obligatoire"
styleClass="w-full" /> styleClass="w-full" />
<p:message for="adresse" /> <p:message for="adresse" />
</div> </div>
@@ -228,7 +225,8 @@
<div class="formgrid grid"> <div class="formgrid grid">
<div class="field col"> <div class="field col">
<p:outputLabel for="ville" value="Ville" /> <p:outputLabel for="ville" value="Ville" />
<p:inputText id="ville" value="#{membreInscriptionBean.ville}" styleClass="w-full" /> <p:inputText id="ville" value="#{membreInscriptionBean.ville}" required="true"
requiredMessage="La ville est obligatoire" styleClass="w-full" />
<p:message for="ville" /> <p:message for="ville" />
</div> </div>
<div class="field col"> <div class="field col">
@@ -255,6 +253,7 @@
<div class="field col"> <div class="field col">
<p:outputLabel for="telephoneMobile" value="Téléphone mobile" /> <p:outputLabel for="telephoneMobile" value="Téléphone mobile" />
<p:inputText id="telephoneMobile" value="#{membreInscriptionBean.telephoneMobile}" <p:inputText id="telephoneMobile" value="#{membreInscriptionBean.telephoneMobile}"
required="true" requiredMessage="Le téléphone mobile est obligatoire"
styleClass="w-full" /> styleClass="w-full" />
<p:message for="telephoneMobile" /> <p:message for="telephoneMobile" />
</div> </div>
@@ -279,7 +278,7 @@
</div> </div>
<div class="field"> <div class="field">
<p:outputLabel for="typeAdhesion" value="Type d'adhésion" /> <p:outputLabel for="typeAdhesion" value="Type d'adhésion" />
<p:selectOneMenu id="typeAdhesion" value="#{membreInscriptionBean.typeAdhesion}" styleClass="w-full"> <p:selectOneMenu id="typeAdhesion" value="#{membreInscriptionBean.typeAdhesion}" required="true" styleClass="w-full">
<f:selectItem itemLabel="Sélectionner..." itemValue="" noSelectionOption="true" /> <f:selectItem itemLabel="Sélectionner..." itemValue="" noSelectionOption="true" />
<f:selectItems value="#{membreInscriptionBean.typeAdhesionOptions}" /> <f:selectItems value="#{membreInscriptionBean.typeAdhesionOptions}" />
</p:selectOneMenu> </p:selectOneMenu>

View File

@@ -197,7 +197,7 @@
iconOnly="true"/> iconOnly="true"/>
<!-- DRY/WOU: Composite Component action-button-edit-nav --> <!-- DRY/WOU: Composite Component action-button-edit-nav -->
<uf:action-button-edit-nav itemId="#{membre.id}" <uf:action-button-edit-nav itemId="#{membre.id}"
editPage="/pages/secure/membre/inscription.xhtml" editPage="/pages/secure/membre/modifier.xhtml"
iconOnly="true"/> iconOnly="true"/>
<ui:include src="/templates/components/buttons/button-icon.xhtml"> <ui:include src="/templates/components/buttons/button-icon.xhtml">
<ui:param name="icon" value="pi pi-dollar" /> <ui:param name="icon" value="pi pi-dollar" />

View File

@@ -0,0 +1,82 @@
package dev.lions.unionflow.server.api.dto.adresse;
import dev.lions.unionflow.server.api.dto.base.BaseDTO;
import dev.lions.unionflow.server.api.enums.adresse.TypeAdresse;
import jakarta.validation.constraints.DecimalMax;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Digits;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.util.UUID;
import lombok.Getter;
import lombok.Setter;
/**
* DTO pour la gestion des adresses
*
* @author UnionFlow Team
* @version 3.0
* @since 2025-01-29
*/
@Getter
@Setter
public class AdresseDTO extends BaseDTO {
private static final long serialVersionUID = 1L;
/** Type d'adresse */
@NotNull(message = "Le type d'adresse est obligatoire")
private TypeAdresse typeAdresse;
/** Adresse complète */
private String adresse;
/** Complément d'adresse */
private String complementAdresse;
/** Code postal */
private String codePostal;
/** Ville */
private String ville;
/** Région */
private String region;
/** Pays */
private String pays;
/** Latitude */
@DecimalMin(value = "-90.0", message = "La latitude doit être comprise entre -90 et 90")
@DecimalMax(value = "90.0", message = "La latitude doit être comprise entre -90 et 90")
@Digits(integer = 3, fraction = 6)
private BigDecimal latitude;
/** Longitude */
@DecimalMin(value = "-180.0", message = "La longitude doit être comprise entre -180 et 180")
@DecimalMax(value = "180.0", message = "La longitude doit être comprise entre -180 et 180")
@Digits(integer = 3, fraction = 6)
private BigDecimal longitude;
/** Adresse principale */
private Boolean principale;
/** Libellé personnalisé */
private String libelle;
/** Notes et commentaires */
private String notes;
/** ID de l'organisation (si adresse d'organisation) */
private UUID organisationId;
/** ID du membre (si adresse de membre) */
private UUID membreId;
/** ID de l'événement (si adresse d'événement) */
private UUID evenementId;
/** Adresse complète formatée (calculée) */
private String adresseComplete;
}

View File

@@ -0,0 +1,26 @@
package dev.lions.unionflow.server.api.enums.adresse;
/**
* Énumération des types d'adresse dans UnionFlow
*
* @author UnionFlow Team
* @version 3.0
* @since 2025-01-29
*/
public enum TypeAdresse {
SIEGE_SOCIAL("Siège Social"),
BUREAU("Bureau"),
DOMICILE("Domicile"),
AUTRE("Autre");
private final String libelle;
TypeAdresse(String libelle) {
this.libelle = libelle;
}
public String getLibelle() {
return libelle;
}
}

View File

@@ -0,0 +1,154 @@
package dev.lions.unionflow.server.entity;
import dev.lions.unionflow.server.api.enums.adresse.TypeAdresse;
import jakarta.persistence.*;
import jakarta.validation.constraints.*;
import java.math.BigDecimal;
import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/**
* Entité Adresse pour la gestion des adresses des organisations, membres et événements
*
* @author UnionFlow Team
* @version 3.0
* @since 2025-01-29
*/
@Entity
@Table(
name = "adresses",
indexes = {
@Index(name = "idx_adresse_ville", columnList = "ville"),
@Index(name = "idx_adresse_pays", columnList = "pays"),
@Index(name = "idx_adresse_type", columnList = "type_adresse"),
@Index(name = "idx_adresse_organisation", columnList = "organisation_id"),
@Index(name = "idx_adresse_membre", columnList = "membre_id"),
@Index(name = "idx_adresse_evenement", columnList = "evenement_id")
})
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@EqualsAndHashCode(callSuper = true)
public class Adresse extends BaseEntity {
/** Type d'adresse */
@Enumerated(EnumType.STRING)
@Column(name = "type_adresse", nullable = false, length = 50)
private dev.lions.unionflow.server.api.enums.adresse.TypeAdresse typeAdresse;
/** Adresse complète */
@Column(name = "adresse", length = 500)
private String adresse;
/** Complément d'adresse */
@Column(name = "complement_adresse", length = 200)
private String complementAdresse;
/** Code postal */
@Column(name = "code_postal", length = 20)
private String codePostal;
/** Ville */
@Column(name = "ville", length = 100)
private String ville;
/** Région */
@Column(name = "region", length = 100)
private String region;
/** Pays */
@Column(name = "pays", length = 100)
private String pays;
/** Coordonnées géographiques - Latitude */
@DecimalMin(value = "-90.0", message = "La latitude doit être comprise entre -90 et 90")
@DecimalMax(value = "90.0", message = "La latitude doit être comprise entre -90 et 90")
@Digits(integer = 3, fraction = 6)
@Column(name = "latitude", precision = 9, scale = 6)
private BigDecimal latitude;
/** Coordonnées géographiques - Longitude */
@DecimalMin(value = "-180.0", message = "La longitude doit être comprise entre -180 et 180")
@DecimalMax(value = "180.0", message = "La longitude doit être comprise entre -180 et 180")
@Digits(integer = 3, fraction = 6)
@Column(name = "longitude", precision = 9, scale = 6)
private BigDecimal longitude;
/** Adresse principale (une seule par entité) */
@Builder.Default
@Column(name = "principale", nullable = false)
private Boolean principale = false;
/** Libellé personnalisé */
@Column(name = "libelle", length = 100)
private String libelle;
/** Notes et commentaires */
@Column(name = "notes", length = 500)
private String notes;
// Relations
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "organisation_id")
private Organisation organisation;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "membre_id")
private Membre membre;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "evenement_id")
private Evenement evenement;
/** Méthode métier pour obtenir l'adresse complète formatée */
public String getAdresseComplete() {
StringBuilder sb = new StringBuilder();
if (adresse != null && !adresse.isEmpty()) {
sb.append(adresse);
}
if (complementAdresse != null && !complementAdresse.isEmpty()) {
if (sb.length() > 0) sb.append(", ");
sb.append(complementAdresse);
}
if (codePostal != null && !codePostal.isEmpty()) {
if (sb.length() > 0) sb.append(", ");
sb.append(codePostal);
}
if (ville != null && !ville.isEmpty()) {
if (sb.length() > 0) sb.append(" ");
sb.append(ville);
}
if (region != null && !region.isEmpty()) {
if (sb.length() > 0) sb.append(", ");
sb.append(region);
}
if (pays != null && !pays.isEmpty()) {
if (sb.length() > 0) sb.append(", ");
sb.append(pays);
}
return sb.toString();
}
/** Méthode métier pour vérifier si l'adresse a des coordonnées GPS */
public boolean hasCoordinates() {
return latitude != null && longitude != null;
}
/** Callback JPA avant la persistance */
@PrePersist
protected void onCreate() {
super.onCreate(); // Appelle le onCreate de BaseEntity
if (typeAdresse == null) {
typeAdresse = dev.lions.unionflow.server.api.enums.adresse.TypeAdresse.AUTRE;
}
if (principale == null) {
principale = false;
}
}
}

View File

@@ -118,6 +118,10 @@ public class Evenement extends BaseEntity {
@Builder.Default @Builder.Default
private List<InscriptionEvenement> inscriptions = new ArrayList<>(); private List<InscriptionEvenement> inscriptions = new ArrayList<>();
@OneToMany(mappedBy = "evenement", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@Builder.Default
private List<Adresse> adresses = new ArrayList<>();
/** Types d'événements */ /** Types d'événements */
public enum TypeEvenement { public enum TypeEvenement {
ASSEMBLEE_GENERALE("Assemblée Générale"), ASSEMBLEE_GENERALE("Assemblée Générale"),

View File

@@ -5,6 +5,8 @@ import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID; import java.util.UUID;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
@@ -67,6 +69,14 @@ public class Membre extends BaseEntity {
@JoinColumn(name = "organisation_id") @JoinColumn(name = "organisation_id")
private Organisation organisation; private Organisation organisation;
@OneToMany(mappedBy = "membre", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@Builder.Default
private List<Adresse> adresses = new ArrayList<>();
@OneToMany(mappedBy = "membre", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@Builder.Default
private List<MembreRole> roles = new ArrayList<>();
/** Méthode métier pour obtenir le nom complet */ /** Méthode métier pour obtenir le nom complet */
public String getNomComplet() { public String getNomComplet() {
return prenom + " " + nom; return prenom + " " + nom;

View File

@@ -0,0 +1,88 @@
package dev.lions.unionflow.server.entity;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDate;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/**
* Table de liaison entre Membre et Role
* Permet à un membre d'avoir plusieurs rôles avec dates de début/fin
*
* @author UnionFlow Team
* @version 3.0
* @since 2025-01-29
*/
@Entity
@Table(
name = "membres_roles",
indexes = {
@Index(name = "idx_membre_role_membre", columnList = "membre_id"),
@Index(name = "idx_membre_role_role", columnList = "role_id"),
@Index(name = "idx_membre_role_actif", columnList = "actif")
},
uniqueConstraints = {
@UniqueConstraint(
name = "uk_membre_role",
columnNames = {"membre_id", "role_id"})
})
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@EqualsAndHashCode(callSuper = true)
public class MembreRole extends BaseEntity {
/** Membre */
@NotNull
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "membre_id", nullable = false)
private Membre membre;
/** Rôle */
@NotNull
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "role_id", nullable = false)
private Role role;
/** Date de début d'attribution */
@Column(name = "date_debut")
private LocalDate dateDebut;
/** Date de fin d'attribution (null = permanent) */
@Column(name = "date_fin")
private LocalDate dateFin;
/** Commentaire sur l'attribution */
@Column(name = "commentaire", length = 500)
private String commentaire;
/** Méthode métier pour vérifier si l'attribution est active */
public boolean isActif() {
if (!Boolean.TRUE.equals(getActif())) {
return false;
}
LocalDate aujourdhui = LocalDate.now();
if (dateDebut != null && aujourdhui.isBefore(dateDebut)) {
return false;
}
if (dateFin != null && aujourdhui.isAfter(dateFin)) {
return false;
}
return true;
}
/** Callback JPA avant la persistance */
@PrePersist
protected void onCreate() {
super.onCreate();
if (dateDebut == null) {
dateDebut = LocalDate.now();
}
}
}

View File

@@ -191,6 +191,10 @@ public class Organisation extends BaseEntity {
@Builder.Default @Builder.Default
private List<Membre> membres = new ArrayList<>(); private List<Membre> membres = new ArrayList<>();
@OneToMany(mappedBy = "organisation", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@Builder.Default
private List<Adresse> adresses = new ArrayList<>();
/** Méthode métier pour obtenir le nom complet avec sigle */ /** Méthode métier pour obtenir le nom complet avec sigle */
public String getNomComplet() { public String getNomComplet() {
if (nomCourt != null && !nomCourt.isEmpty()) { if (nomCourt != null && !nomCourt.isEmpty()) {

View File

@@ -0,0 +1,90 @@
package dev.lions.unionflow.server.entity;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank;
import java.util.ArrayList;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/**
* Entité Permission pour la gestion des permissions granulaires
*
* @author UnionFlow Team
* @version 3.0
* @since 2025-01-29
*/
@Entity
@Table(
name = "permissions",
indexes = {
@Index(name = "idx_permission_code", columnList = "code", unique = true),
@Index(name = "idx_permission_module", columnList = "module"),
@Index(name = "idx_permission_ressource", columnList = "ressource")
})
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@EqualsAndHashCode(callSuper = true)
public class Permission extends BaseEntity {
/** Code unique de la permission (format: MODULE > RESSOURCE > ACTION) */
@NotBlank
@Column(name = "code", unique = true, nullable = false, length = 100)
private String code;
/** Module (ex: ORGANISATION, MEMBRE, COTISATION) */
@NotBlank
@Column(name = "module", nullable = false, length = 50)
private String module;
/** Ressource (ex: MEMBRE, COTISATION, ADHESION) */
@NotBlank
@Column(name = "ressource", nullable = false, length = 50)
private String ressource;
/** Action (ex: CREATE, READ, UPDATE, DELETE, VALIDATE) */
@NotBlank
@Column(name = "action", nullable = false, length = 50)
private String action;
/** Libellé de la permission */
@Column(name = "libelle", length = 200)
private String libelle;
/** Description de la permission */
@Column(name = "description", length = 500)
private String description;
/** Rôles associés */
@OneToMany(mappedBy = "permission", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@Builder.Default
private List<RolePermission> roles = new ArrayList<>();
/** Méthode métier pour générer le code à partir des composants */
public static String genererCode(String module, String ressource, String action) {
return String.format("%s > %s > %s", module.toUpperCase(), ressource.toUpperCase(), action.toUpperCase());
}
/** Méthode métier pour vérifier si le code est valide */
public boolean isCodeValide() {
return code != null && code.contains(" > ") && code.split(" > ").length == 3;
}
/** Callback JPA avant la persistance */
@PrePersist
protected void onCreate() {
super.onCreate();
// Générer le code si non fourni
if (code == null || code.isEmpty()) {
if (module != null && ressource != null && action != null) {
code = genererCode(module, ressource, action);
}
}
}
}

View File

@@ -0,0 +1,105 @@
package dev.lions.unionflow.server.entity;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.util.ArrayList;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/**
* Entité Role pour la gestion des rôles dans le système
*
* @author UnionFlow Team
* @version 3.0
* @since 2025-01-29
*/
@Entity
@Table(
name = "roles",
indexes = {
@Index(name = "idx_role_code", columnList = "code", unique = true),
@Index(name = "idx_role_actif", columnList = "actif"),
@Index(name = "idx_role_niveau", columnList = "niveau_hierarchique")
})
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@EqualsAndHashCode(callSuper = true)
public class Role extends BaseEntity {
/** Code unique du rôle */
@NotBlank
@Column(name = "code", unique = true, nullable = false, length = 50)
private String code;
/** Libellé du rôle */
@NotBlank
@Column(name = "libelle", nullable = false, length = 100)
private String libelle;
/** Description du rôle */
@Column(name = "description", length = 500)
private String description;
/** Niveau hiérarchique (plus bas = plus prioritaire) */
@NotNull
@Builder.Default
@Column(name = "niveau_hierarchique", nullable = false)
private Integer niveauHierarchique = 100;
/** Type de rôle */
@Enumerated(EnumType.STRING)
@Column(name = "type_role", nullable = false, length = 50)
private TypeRole typeRole;
/** Organisation propriétaire (null pour rôles système) */
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "organisation_id")
private Organisation organisation;
/** Permissions associées */
@OneToMany(mappedBy = "role", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@Builder.Default
private List<RolePermission> permissions = new ArrayList<>();
/** Énumération des types de rôle */
public enum TypeRole {
SYSTEME("Rôle Système"),
ORGANISATION("Rôle Organisation"),
PERSONNALISE("Rôle Personnalisé");
private final String libelle;
TypeRole(String libelle) {
this.libelle = libelle;
}
public String getLibelle() {
return libelle;
}
}
/** Méthode métier pour vérifier si c'est un rôle système */
public boolean isRoleSysteme() {
return TypeRole.SYSTEME.equals(typeRole);
}
/** Callback JPA avant la persistance */
@PrePersist
protected void onCreate() {
super.onCreate();
if (typeRole == null) {
typeRole = TypeRole.PERSONNALISE;
}
if (niveauHierarchique == null) {
niveauHierarchique = 100;
}
}
}

View File

@@ -0,0 +1,54 @@
package dev.lions.unionflow.server.entity;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/**
* Table de liaison entre Role et Permission
* Permet à un rôle d'avoir plusieurs permissions
*
* @author UnionFlow Team
* @version 3.0
* @since 2025-01-29
*/
@Entity
@Table(
name = "roles_permissions",
indexes = {
@Index(name = "idx_role_permission_role", columnList = "role_id"),
@Index(name = "idx_role_permission_permission", columnList = "permission_id")
},
uniqueConstraints = {
@UniqueConstraint(
name = "uk_role_permission",
columnNames = {"role_id", "permission_id"})
})
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@EqualsAndHashCode(callSuper = true)
public class RolePermission extends BaseEntity {
/** Rôle */
@NotNull
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "role_id", nullable = false)
private Role role;
/** Permission */
@NotNull
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "permission_id", nullable = false)
private Permission permission;
/** Commentaire sur l'association */
@Column(name = "commentaire", length = 500)
private String commentaire;
}

View File

@@ -0,0 +1,101 @@
package dev.lions.unionflow.server.repository;
import dev.lions.unionflow.server.entity.Adresse;
import dev.lions.unionflow.server.entity.Adresse.TypeAdresse;
import io.quarkus.hibernate.orm.panache.PanacheRepository;
import jakarta.enterprise.context.ApplicationScoped;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
/**
* Repository pour l'entité Adresse
*
* @author UnionFlow Team
* @version 3.0
* @since 2025-01-29
*/
@ApplicationScoped
public class AdresseRepository implements PanacheRepository<Adresse> {
/**
* Trouve toutes les adresses d'une organisation
*
* @param organisationId ID de l'organisation
* @return Liste des adresses
*/
public List<Adresse> findByOrganisationId(UUID organisationId) {
return find("organisation.id", organisationId).list();
}
/**
* Trouve l'adresse principale d'une organisation
*
* @param organisationId ID de l'organisation
* @return Adresse principale ou Optional.empty()
*/
public Optional<Adresse> findPrincipaleByOrganisationId(UUID organisationId) {
return find("organisation.id = ?1 AND principale = true", organisationId).firstResultOptional();
}
/**
* Trouve toutes les adresses d'un membre
*
* @param membreId ID du membre
* @return Liste des adresses
*/
public List<Adresse> findByMembreId(UUID membreId) {
return find("membre.id", membreId).list();
}
/**
* Trouve l'adresse principale d'un membre
*
* @param membreId ID du membre
* @return Adresse principale ou Optional.empty()
*/
public Optional<Adresse> findPrincipaleByMembreId(UUID membreId) {
return find("membre.id = ?1 AND principale = true", membreId).firstResultOptional();
}
/**
* Trouve l'adresse d'un événement
*
* @param evenementId ID de l'événement
* @return Adresse ou Optional.empty()
*/
public Optional<Adresse> findByEvenementId(UUID evenementId) {
return find("evenement.id", evenementId).firstResultOptional();
}
/**
* Trouve les adresses par type
*
* @param typeAdresse Type d'adresse
* @return Liste des adresses
*/
public List<Adresse> findByType(TypeAdresse typeAdresse) {
return find("typeAdresse", typeAdresse).list();
}
/**
* Trouve les adresses par ville
*
* @param ville Nom de la ville
* @return Liste des adresses
*/
public List<Adresse> findByVille(String ville) {
return find("LOWER(ville) = LOWER(?1)", ville).list();
}
/**
* Trouve les adresses par pays
*
* @param pays Nom du pays
* @return Liste des adresses
*/
public List<Adresse> findByPays(String pays) {
return find("LOWER(pays) = LOWER(?1)", pays).list();
}
}

View File

@@ -0,0 +1,66 @@
package dev.lions.unionflow.server.repository;
import dev.lions.unionflow.server.entity.MembreRole;
import io.quarkus.hibernate.orm.panache.PanacheRepository;
import jakarta.enterprise.context.ApplicationScoped;
import java.time.LocalDate;
import java.util.List;
import java.util.UUID;
/**
* Repository pour l'entité MembreRole
*
* @author UnionFlow Team
* @version 3.0
* @since 2025-01-29
*/
@ApplicationScoped
public class MembreRoleRepository implements PanacheRepository<MembreRole> {
/**
* Trouve tous les rôles d'un membre
*
* @param membreId ID du membre
* @return Liste des attributions de rôles
*/
public List<MembreRole> findByMembreId(UUID membreId) {
return find("membre.id = ?1 AND actif = true", membreId).list();
}
/**
* Trouve tous les rôles actifs d'un membre (dans la période valide)
*
* @param membreId ID du membre
* @return Liste des attributions de rôles actives
*/
public List<MembreRole> findActifsByMembreId(UUID membreId) {
LocalDate aujourdhui = LocalDate.now();
return find(
"membre.id = ?1 AND actif = true AND (dateDebut IS NULL OR dateDebut <= ?2) AND (dateFin IS NULL OR dateFin >= ?2)",
membreId,
aujourdhui)
.list();
}
/**
* Trouve tous les membres ayant un rôle spécifique
*
* @param roleId ID du rôle
* @return Liste des attributions de rôles
*/
public List<MembreRole> findByRoleId(UUID roleId) {
return find("role.id = ?1 AND actif = true", roleId).list();
}
/**
* Trouve une attribution spécifique membre-role
*
* @param membreId ID du membre
* @param roleId ID du rôle
* @return Attribution ou null
*/
public MembreRole findByMembreAndRole(UUID membreId, UUID roleId) {
return find("membre.id = ?1 AND role.id = ?2", membreId, roleId).firstResult();
}
}

View File

@@ -0,0 +1,73 @@
package dev.lions.unionflow.server.repository;
import dev.lions.unionflow.server.entity.Permission;
import io.quarkus.hibernate.orm.panache.PanacheRepository;
import jakarta.enterprise.context.ApplicationScoped;
import java.util.List;
import java.util.Optional;
/**
* Repository pour l'entité Permission
*
* @author UnionFlow Team
* @version 3.0
* @since 2025-01-29
*/
@ApplicationScoped
public class PermissionRepository implements PanacheRepository<Permission> {
/**
* Trouve une permission par son code
*
* @param code Code de la permission
* @return Permission ou Optional.empty()
*/
public Optional<Permission> findByCode(String code) {
return find("code", code).firstResultOptional();
}
/**
* Trouve les permissions par module
*
* @param module Nom du module
* @return Liste des permissions
*/
public List<Permission> findByModule(String module) {
return find("LOWER(module) = LOWER(?1) AND actif = true", module).list();
}
/**
* Trouve les permissions par ressource
*
* @param ressource Nom de la ressource
* @return Liste des permissions
*/
public List<Permission> findByRessource(String ressource) {
return find("LOWER(ressource) = LOWER(?1) AND actif = true", ressource).list();
}
/**
* Trouve les permissions par module et ressource
*
* @param module Nom du module
* @param ressource Nom de la ressource
* @return Liste des permissions
*/
public List<Permission> findByModuleAndRessource(String module, String ressource) {
return find(
"LOWER(module) = LOWER(?1) AND LOWER(ressource) = LOWER(?2) AND actif = true",
module,
ressource)
.list();
}
/**
* Trouve toutes les permissions actives
*
* @return Liste des permissions actives
*/
public List<Permission> findAllActives() {
return find("actif = true").order("module ASC, ressource ASC, action ASC").list();
}
}

View File

@@ -0,0 +1,50 @@
package dev.lions.unionflow.server.repository;
import dev.lions.unionflow.server.entity.RolePermission;
import io.quarkus.hibernate.orm.panache.PanacheRepository;
import jakarta.enterprise.context.ApplicationScoped;
import java.util.List;
import java.util.UUID;
/**
* Repository pour l'entité RolePermission
*
* @author UnionFlow Team
* @version 3.0
* @since 2025-01-29
*/
@ApplicationScoped
public class RolePermissionRepository implements PanacheRepository<RolePermission> {
/**
* Trouve toutes les permissions d'un rôle
*
* @param roleId ID du rôle
* @return Liste des associations rôle-permission
*/
public List<RolePermission> findByRoleId(UUID roleId) {
return find("role.id = ?1 AND actif = true", roleId).list();
}
/**
* Trouve tous les rôles ayant une permission spécifique
*
* @param permissionId ID de la permission
* @return Liste des associations rôle-permission
*/
public List<RolePermission> findByPermissionId(UUID permissionId) {
return find("permission.id = ?1 AND actif = true", permissionId).list();
}
/**
* Trouve une association spécifique rôle-permission
*
* @param roleId ID du rôle
* @param permissionId ID de la permission
* @return Association ou null
*/
public RolePermission findByRoleAndPermission(UUID roleId, UUID permissionId) {
return find("role.id = ?1 AND permission.id = ?2", roleId, permissionId).firstResult();
}
}

View File

@@ -0,0 +1,75 @@
package dev.lions.unionflow.server.repository;
import dev.lions.unionflow.server.entity.Role;
import dev.lions.unionflow.server.entity.Role.TypeRole;
import io.quarkus.hibernate.orm.panache.PanacheRepository;
import jakarta.enterprise.context.ApplicationScoped;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
/**
* Repository pour l'entité Role
*
* @author UnionFlow Team
* @version 3.0
* @since 2025-01-29
*/
@ApplicationScoped
public class RoleRepository implements PanacheRepository<Role> {
/**
* Trouve un rôle par son code
*
* @param code Code du rôle
* @return Rôle ou Optional.empty()
*/
public Optional<Role> findByCode(String code) {
return find("code", code).firstResultOptional();
}
/**
* Trouve tous les rôles système
*
* @return Liste des rôles système
*/
public List<Role> findRolesSysteme() {
return find("typeRole = ?1 AND actif = true", TypeRole.SYSTEME)
.order("niveauHierarchique ASC")
.list();
}
/**
* Trouve tous les rôles d'une organisation
*
* @param organisationId ID de l'organisation
* @return Liste des rôles
*/
public List<Role> findByOrganisationId(UUID organisationId) {
return find("organisation.id = ?1 AND actif = true", organisationId)
.order("niveauHierarchique ASC")
.list();
}
/**
* Trouve tous les rôles actifs
*
* @return Liste des rôles actifs
*/
public List<Role> findAllActifs() {
return find("actif = true").order("niveauHierarchique ASC").list();
}
/**
* Trouve les rôles par type
*
* @param typeRole Type de rôle
* @return Liste des rôles
*/
public List<Role> findByType(TypeRole typeRole) {
return find("typeRole = ?1 AND actif = true", typeRole)
.order("niveauHierarchique ASC")
.list();
}
}

View File

@@ -0,0 +1,358 @@
package dev.lions.unionflow.server.service;
import dev.lions.unionflow.server.api.dto.adresse.AdresseDTO;
import dev.lions.unionflow.server.api.enums.adresse.TypeAdresse;
import dev.lions.unionflow.server.entity.Adresse;
import dev.lions.unionflow.server.entity.Evenement;
import dev.lions.unionflow.server.entity.Membre;
import dev.lions.unionflow.server.entity.Organisation;
import dev.lions.unionflow.server.repository.AdresseRepository;
import dev.lions.unionflow.server.repository.EvenementRepository;
import dev.lions.unionflow.server.repository.MembreRepository;
import dev.lions.unionflow.server.repository.OrganisationRepository;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.NotFoundException;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import org.jboss.logging.Logger;
/**
* Service métier pour la gestion des adresses
*
* @author UnionFlow Team
* @version 3.0
* @since 2025-01-29
*/
@ApplicationScoped
public class AdresseService {
private static final Logger LOG = Logger.getLogger(AdresseService.class);
@Inject AdresseRepository adresseRepository;
@Inject OrganisationRepository organisationRepository;
@Inject MembreRepository membreRepository;
@Inject EvenementRepository evenementRepository;
/**
* Crée une nouvelle adresse
*
* @param adresseDTO DTO de l'adresse à créer
* @return DTO de l'adresse créée
*/
@Transactional
public AdresseDTO creerAdresse(AdresseDTO adresseDTO) {
LOG.infof("Création d'une nouvelle adresse de type: %s", adresseDTO.getTypeAdresse());
Adresse adresse = convertToEntity(adresseDTO);
// Gestion de l'adresse principale
if (Boolean.TRUE.equals(adresseDTO.getPrincipale())) {
desactiverAutresPrincipales(adresseDTO);
}
adresseRepository.persist(adresse);
LOG.infof("Adresse créée avec succès: ID=%s", adresse.getId());
return convertToDTO(adresse);
}
/**
* Met à jour une adresse existante
*
* @param id ID de l'adresse
* @param adresseDTO DTO avec les nouvelles données
* @return DTO de l'adresse mise à jour
*/
@Transactional
public AdresseDTO mettreAJourAdresse(UUID id, AdresseDTO adresseDTO) {
LOG.infof("Mise à jour de l'adresse ID: %s", id);
Adresse adresse =
adresseRepository
.findByIdOptional(id)
.orElseThrow(() -> new NotFoundException("Adresse non trouvée avec l'ID: " + id));
// Mise à jour des champs
updateFromDTO(adresse, adresseDTO);
// Gestion de l'adresse principale
if (Boolean.TRUE.equals(adresseDTO.getPrincipale())) {
desactiverAutresPrincipales(adresseDTO);
}
adresseRepository.persist(adresse);
LOG.infof("Adresse mise à jour avec succès: ID=%s", id);
return convertToDTO(adresse);
}
/**
* Supprime une adresse
*
* @param id ID de l'adresse
*/
@Transactional
public void supprimerAdresse(UUID id) {
LOG.infof("Suppression de l'adresse ID: %s", id);
Adresse adresse =
adresseRepository
.findByIdOptional(id)
.orElseThrow(() -> new NotFoundException("Adresse non trouvée avec l'ID: " + id));
adresseRepository.delete(adresse);
LOG.infof("Adresse supprimée avec succès: ID=%s", id);
}
/**
* Trouve une adresse par son ID
*
* @param id ID de l'adresse
* @return DTO de l'adresse
*/
public AdresseDTO trouverParId(UUID id) {
return adresseRepository
.findByIdOptional(id)
.map(this::convertToDTO)
.orElseThrow(() -> new NotFoundException("Adresse non trouvée avec l'ID: " + id));
}
/**
* Trouve toutes les adresses d'une organisation
*
* @param organisationId ID de l'organisation
* @return Liste des adresses
*/
public List<AdresseDTO> trouverParOrganisation(UUID organisationId) {
return adresseRepository.findByOrganisationId(organisationId).stream()
.map(this::convertToDTO)
.collect(Collectors.toList());
}
/**
* Trouve toutes les adresses d'un membre
*
* @param membreId ID du membre
* @return Liste des adresses
*/
public List<AdresseDTO> trouverParMembre(UUID membreId) {
return adresseRepository.findByMembreId(membreId).stream()
.map(this::convertToDTO)
.collect(Collectors.toList());
}
/**
* Trouve l'adresse d'un événement
*
* @param evenementId ID de l'événement
* @return DTO de l'adresse ou null
*/
public AdresseDTO trouverParEvenement(UUID evenementId) {
return adresseRepository
.findByEvenementId(evenementId)
.map(this::convertToDTO)
.orElse(null);
}
/**
* Trouve l'adresse principale d'une organisation
*
* @param organisationId ID de l'organisation
* @return DTO de l'adresse principale ou null
*/
public AdresseDTO trouverPrincipaleParOrganisation(UUID organisationId) {
return adresseRepository
.findPrincipaleByOrganisationId(organisationId)
.map(this::convertToDTO)
.orElse(null);
}
/**
* Trouve l'adresse principale d'un membre
*
* @param membreId ID du membre
* @return DTO de l'adresse principale ou null
*/
public AdresseDTO trouverPrincipaleParMembre(UUID membreId) {
return adresseRepository
.findPrincipaleByMembreId(membreId)
.map(this::convertToDTO)
.orElse(null);
}
// ========================================
// MÉTHODES PRIVÉES
// ========================================
/** Désactive les autres adresses principales pour la même entité */
private void desactiverAutresPrincipales(AdresseDTO adresseDTO) {
List<Adresse> autresPrincipales;
if (adresseDTO.getOrganisationId() != null) {
autresPrincipales =
adresseRepository
.find("organisation.id = ?1 AND principale = true", adresseDTO.getOrganisationId())
.list();
} else if (adresseDTO.getMembreId() != null) {
autresPrincipales =
adresseRepository
.find("membre.id = ?1 AND principale = true", adresseDTO.getMembreId())
.list();
} else {
return; // Pas d'entité associée
}
autresPrincipales.forEach(adr -> adr.setPrincipale(false));
}
/** Convertit une entité en DTO */
private AdresseDTO convertToDTO(Adresse adresse) {
if (adresse == null) {
return null;
}
AdresseDTO dto = new AdresseDTO();
dto.setId(adresse.getId());
dto.setTypeAdresse(convertTypeAdresse(adresse.getTypeAdresse()));
dto.setAdresse(adresse.getAdresse());
dto.setComplementAdresse(adresse.getComplementAdresse());
dto.setCodePostal(adresse.getCodePostal());
dto.setVille(adresse.getVille());
dto.setRegion(adresse.getRegion());
dto.setPays(adresse.getPays());
dto.setLatitude(adresse.getLatitude());
dto.setLongitude(adresse.getLongitude());
dto.setPrincipale(adresse.getPrincipale());
dto.setLibelle(adresse.getLibelle());
dto.setNotes(adresse.getNotes());
if (adresse.getOrganisation() != null) {
dto.setOrganisationId(adresse.getOrganisation().getId());
}
if (adresse.getMembre() != null) {
dto.setMembreId(adresse.getMembre().getId());
}
if (adresse.getEvenement() != null) {
dto.setEvenementId(adresse.getEvenement().getId());
}
dto.setAdresseComplete(adresse.getAdresseComplete());
dto.setDateCreation(adresse.getDateCreation());
dto.setDateModification(adresse.getDateModification());
dto.setActif(adresse.getActif());
return dto;
}
/** Convertit un DTO en entité */
private Adresse convertToEntity(AdresseDTO dto) {
if (dto == null) {
return null;
}
Adresse adresse = new Adresse();
adresse.setTypeAdresse(convertTypeAdresse(dto.getTypeAdresse()));
adresse.setAdresse(dto.getAdresse());
adresse.setComplementAdresse(dto.getComplementAdresse());
adresse.setCodePostal(dto.getCodePostal());
adresse.setVille(dto.getVille());
adresse.setRegion(dto.getRegion());
adresse.setPays(dto.getPays());
adresse.setLatitude(dto.getLatitude());
adresse.setLongitude(dto.getLongitude());
adresse.setPrincipale(dto.getPrincipale() != null ? dto.getPrincipale() : false);
adresse.setLibelle(dto.getLibelle());
adresse.setNotes(dto.getNotes());
// Relations
if (dto.getOrganisationId() != null) {
Organisation org =
organisationRepository
.findByIdOptional(dto.getOrganisationId())
.orElseThrow(
() ->
new NotFoundException(
"Organisation non trouvée avec l'ID: " + dto.getOrganisationId()));
adresse.setOrganisation(org);
}
if (dto.getMembreId() != null) {
Membre membre =
membreRepository
.findByIdOptional(dto.getMembreId())
.orElseThrow(
() -> new NotFoundException("Membre non trouvé avec l'ID: " + dto.getMembreId()));
adresse.setMembre(membre);
}
if (dto.getEvenementId() != null) {
Evenement evenement =
evenementRepository
.findByIdOptional(dto.getEvenementId())
.orElseThrow(
() ->
new NotFoundException(
"Événement non trouvé avec l'ID: " + dto.getEvenementId()));
adresse.setEvenement(evenement);
}
return adresse;
}
/** Met à jour une entité à partir d'un DTO */
private void updateFromDTO(Adresse adresse, AdresseDTO dto) {
if (dto.getTypeAdresse() != null) {
adresse.setTypeAdresse(convertTypeAdresse(dto.getTypeAdresse()));
}
if (dto.getAdresse() != null) {
adresse.setAdresse(dto.getAdresse());
}
if (dto.getComplementAdresse() != null) {
adresse.setComplementAdresse(dto.getComplementAdresse());
}
if (dto.getCodePostal() != null) {
adresse.setCodePostal(dto.getCodePostal());
}
if (dto.getVille() != null) {
adresse.setVille(dto.getVille());
}
if (dto.getRegion() != null) {
adresse.setRegion(dto.getRegion());
}
if (dto.getPays() != null) {
adresse.setPays(dto.getPays());
}
if (dto.getLatitude() != null) {
adresse.setLatitude(dto.getLatitude());
}
if (dto.getLongitude() != null) {
adresse.setLongitude(dto.getLongitude());
}
if (dto.getPrincipale() != null) {
adresse.setPrincipale(dto.getPrincipale());
}
if (dto.getLibelle() != null) {
adresse.setLibelle(dto.getLibelle());
}
if (dto.getNotes() != null) {
adresse.setNotes(dto.getNotes());
}
}
/** Convertit TypeAdresse (entité) vers TypeAdresse (DTO) - même enum, pas de conversion nécessaire */
private TypeAdresse convertTypeAdresse(TypeAdresse type) {
return type; // Même enum, pas de conversion nécessaire
}
/** Convertit TypeAdresse (DTO) vers TypeAdresse (entité) - même enum, pas de conversion nécessaire */
private TypeAdresse convertTypeAdresse(TypeAdresse type) {
return type != null ? type : TypeAdresse.AUTRE; // Même enum, valeur par défaut si null
}
}

View File

@@ -0,0 +1,165 @@
package dev.lions.unionflow.server.service;
import dev.lions.unionflow.server.entity.Permission;
import dev.lions.unionflow.server.repository.PermissionRepository;
import dev.lions.unionflow.server.service.KeycloakService;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.NotFoundException;
import java.util.List;
import java.util.UUID;
import org.jboss.logging.Logger;
/**
* Service métier pour la gestion des permissions
*
* @author UnionFlow Team
* @version 3.0
* @since 2025-01-29
*/
@ApplicationScoped
public class PermissionService {
private static final Logger LOG = Logger.getLogger(PermissionService.class);
@Inject PermissionRepository permissionRepository;
@Inject KeycloakService keycloakService;
/**
* Crée une nouvelle permission
*
* @param permission Permission à créer
* @return Permission créée
*/
@Transactional
public Permission creerPermission(Permission permission) {
LOG.infof("Création d'une nouvelle permission: %s", permission.getCode());
// Vérifier l'unicité du code
if (permissionRepository.findByCode(permission.getCode()).isPresent()) {
throw new IllegalArgumentException(
"Une permission avec ce code existe déjà: " + permission.getCode());
}
// Générer le code si non fourni
if (permission.getCode() == null || permission.getCode().isEmpty()) {
permission.setCode(
Permission.genererCode(
permission.getModule(), permission.getRessource(), permission.getAction()));
}
// Métadonnées
permission.setCreePar(keycloakService.getCurrentUserEmail());
permissionRepository.persist(permission);
LOG.infof(
"Permission créée avec succès: ID=%s, Code=%s",
permission.getId(), permission.getCode());
return permission;
}
/**
* Met à jour une permission existante
*
* @param id ID de la permission
* @param permissionModifiee Permission avec les modifications
* @return Permission mise à jour
*/
@Transactional
public Permission mettreAJourPermission(UUID id, Permission permissionModifiee) {
LOG.infof("Mise à jour de la permission ID: %s", id);
Permission permission =
permissionRepository
.findByIdOptional(id)
.orElseThrow(() -> new NotFoundException("Permission non trouvée avec l'ID: " + id));
// Mise à jour
permission.setCode(permissionModifiee.getCode());
permission.setModule(permissionModifiee.getModule());
permission.setRessource(permissionModifiee.getRessource());
permission.setAction(permissionModifiee.getAction());
permission.setLibelle(permissionModifiee.getLibelle());
permission.setDescription(permissionModifiee.getDescription());
permission.setModifiePar(keycloakService.getCurrentUserEmail());
permissionRepository.persist(permission);
LOG.infof("Permission mise à jour avec succès: ID=%s", id);
return permission;
}
/**
* Trouve une permission par son ID
*
* @param id ID de la permission
* @return Permission ou null
*/
public Permission trouverParId(UUID id) {
return permissionRepository.findByIdOptional(id).orElse(null);
}
/**
* Trouve une permission par son code
*
* @param code Code de la permission
* @return Permission ou null
*/
public Permission trouverParCode(String code) {
return permissionRepository.findByCode(code).orElse(null);
}
/**
* Liste les permissions par module
*
* @param module Nom du module
* @return Liste des permissions
*/
public List<Permission> listerParModule(String module) {
return permissionRepository.findByModule(module);
}
/**
* Liste les permissions par ressource
*
* @param ressource Nom de la ressource
* @return Liste des permissions
*/
public List<Permission> listerParRessource(String ressource) {
return permissionRepository.findByRessource(ressource);
}
/**
* Liste toutes les permissions actives
*
* @return Liste des permissions actives
*/
public List<Permission> listerToutesActives() {
return permissionRepository.findAllActives();
}
/**
* Supprime (désactive) une permission
*
* @param id ID de la permission
*/
@Transactional
public void supprimerPermission(UUID id) {
LOG.infof("Suppression de la permission ID: %s", id);
Permission permission =
permissionRepository
.findByIdOptional(id)
.orElseThrow(() -> new NotFoundException("Permission non trouvée avec l'ID: " + id));
permission.setActif(false);
permission.setModifiePar(keycloakService.getCurrentUserEmail());
permissionRepository.persist(permission);
LOG.infof("Permission supprimée avec succès: ID=%s", id);
}
}

View File

@@ -0,0 +1,171 @@
package dev.lions.unionflow.server.service;
import dev.lions.unionflow.server.entity.Organisation;
import dev.lions.unionflow.server.entity.Role;
import dev.lions.unionflow.server.entity.Role.TypeRole;
import dev.lions.unionflow.server.repository.OrganisationRepository;
import dev.lions.unionflow.server.repository.RoleRepository;
import dev.lions.unionflow.server.service.KeycloakService;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.NotFoundException;
import java.util.List;
import java.util.UUID;
import org.jboss.logging.Logger;
/**
* Service métier pour la gestion des rôles
*
* @author UnionFlow Team
* @version 3.0
* @since 2025-01-29
*/
@ApplicationScoped
public class RoleService {
private static final Logger LOG = Logger.getLogger(RoleService.class);
@Inject RoleRepository roleRepository;
@Inject OrganisationRepository organisationRepository;
@Inject KeycloakService keycloakService;
/**
* Crée un nouveau rôle
*
* @param role Rôle à créer
* @return Rôle créé
*/
@Transactional
public Role creerRole(Role role) {
LOG.infof("Création d'un nouveau rôle: %s", role.getCode());
// Vérifier l'unicité du code
if (roleRepository.findByCode(role.getCode()).isPresent()) {
throw new IllegalArgumentException("Un rôle avec ce code existe déjà: " + role.getCode());
}
// Métadonnées
role.setCreePar(keycloakService.getCurrentUserEmail());
roleRepository.persist(role);
LOG.infof("Rôle créé avec succès: ID=%s, Code=%s", role.getId(), role.getCode());
return role;
}
/**
* Met à jour un rôle existant
*
* @param id ID du rôle
* @param roleModifie Rôle avec les modifications
* @return Rôle mis à jour
*/
@Transactional
public Role mettreAJourRole(UUID id, Role roleModifie) {
LOG.infof("Mise à jour du rôle ID: %s", id);
Role role =
roleRepository
.findByIdOptional(id)
.orElseThrow(() -> new NotFoundException("Rôle non trouvé avec l'ID: " + id));
// Vérifier l'unicité du code si modifié
if (!role.getCode().equals(roleModifie.getCode())) {
if (roleRepository.findByCode(roleModifie.getCode()).isPresent()) {
throw new IllegalArgumentException("Un rôle avec ce code existe déjà: " + roleModifie.getCode());
}
}
// Mise à jour
role.setCode(roleModifie.getCode());
role.setLibelle(roleModifie.getLibelle());
role.setDescription(roleModifie.getDescription());
role.setNiveauHierarchique(roleModifie.getNiveauHierarchique());
role.setTypeRole(roleModifie.getTypeRole());
role.setOrganisation(roleModifie.getOrganisation());
role.setModifiePar(keycloakService.getCurrentUserEmail());
roleRepository.persist(role);
LOG.infof("Rôle mis à jour avec succès: ID=%s", id);
return role;
}
/**
* Trouve un rôle par son ID
*
* @param id ID du rôle
* @return Rôle ou null
*/
public Role trouverParId(UUID id) {
return roleRepository.findByIdOptional(id).orElse(null);
}
/**
* Trouve un rôle par son code
*
* @param code Code du rôle
* @return Rôle ou null
*/
public Role trouverParCode(String code) {
return roleRepository.findByCode(code).orElse(null);
}
/**
* Liste tous les rôles système
*
* @return Liste des rôles système
*/
public List<Role> listerRolesSysteme() {
return roleRepository.findRolesSysteme();
}
/**
* Liste tous les rôles d'une organisation
*
* @param organisationId ID de l'organisation
* @return Liste des rôles
*/
public List<Role> listerParOrganisation(UUID organisationId) {
return roleRepository.findByOrganisationId(organisationId);
}
/**
* Liste tous les rôles actifs
*
* @return Liste des rôles actifs
*/
public List<Role> listerTousActifs() {
return roleRepository.findAllActifs();
}
/**
* Supprime (désactive) un rôle
*
* @param id ID du rôle
*/
@Transactional
public void supprimerRole(UUID id) {
LOG.infof("Suppression du rôle ID: %s", id);
Role role =
roleRepository
.findByIdOptional(id)
.orElseThrow(() -> new NotFoundException("Rôle non trouvé avec l'ID: " + id));
// Vérifier si c'est un rôle système
if (role.isRoleSysteme()) {
throw new IllegalStateException("Impossible de supprimer un rôle système");
}
role.setActif(false);
role.setModifiePar(keycloakService.getCurrentUserEmail());
roleRepository.persist(role);
LOG.infof("Rôle supprimé avec succès: ID=%s", id);
}
}

1297
unionflow.md Normal file

File diff suppressed because it is too large Load Diff