Compare commits

...

10 Commits

Author SHA1 Message Date
dahoud
d04a625811 feat(sprint-17 web 2026-04-25): page publique /pages/public/kpi.xhtml — consultation KPI signée
Page anonyme accessible aux autorités externes via URL signée (HMAC-SHA256 backend).
DRY strict — pas de DTO miroir, réutilise KpiPublicSnapshot officiel api 1.0.10.

PublicKpiRestClient (@RegisterRestClient sans AuthHeaderFactory — endpoint anonyme)
- consulter(token) → KpiPublicSnapshot

PublicKpiBean @ViewScoped
- charger() lit query param token via FacesContext / viewParam, appelle endpoint
- Gestion erreurs HTTP : 401 → "Token invalide ou expiré", 404 → "Organisation introuvable"
- Helpers couleurScore (>=80 success / >=60 warning / <60 danger), couleurStatut

Page /pages/public/kpi.xhtml
- Template public-template.xhtml (sans auth, sans menu)
- f:viewParam token + f:viewAction publicKpiBean.charger
- Card KPI : score coloré central + 8 indicateurs en grid (Compliance Officer, AG, AIRMS, CMU, KYC, formation LBC/FT, UBO, CAC)
- Badge transparency footer : "Toute consultation est tracée dans l'audit trail"
- Whitelisté via existing /pages/public/* dans application.properties

Bump api 1.0.9 → 1.0.10

Tests (8/8) PublicKpiBean
- couleurScore × 4 (success/warning/danger/null)
- couleurStatut × 6 cas
- isCharge avec/sans snapshot
2026-04-25 16:50:03 +00:00
dahoud
fcaac36a14 feat(sprint-15 web 2026-04-25): Live Activity Feed page (transparency operations)
Page consommant /api/audit-trail/recent avec auto-refresh PrimeFaces toutes les 10s.
Transparency opérationnelle UnionFlow — chaque utilisateur voit selon son scope.

REST client AuditTrailRestClient
- Nouvelle méthode recent(scope, orgId, userId, limit)

Bean LiveFeedBean (@ViewScoped)
- Polling pilote externe via p:poll interval=10
- 3 scopes : SELF (défaut, n'importe quel rôle), ORG (admin/officer), ALL (compliance/contrôleur)
- Helpers : couleurAction (8 mappings), couleurSod (3 cas), tempsRelatif (s/m/h/j/futur)
- Limit clamp [1, 500]
- Compteur de refresh visible dans l'UI (debug)

Page /pages/secure/conformite/live-feed.xhtml
- Panel scope avec selectOneMenu + p:ajax change → rafraîchir
- Conditional inputs : orgId si scope=ORG, userId si scope=SELF
- p:poll interval=10 listener=rafraichir autoStart=true
- DataTable opérations : index, "il y a Xs", acteur+rôle, action coloré, entité, description, SoD tag
- Tag refresh counter (visible feedback)

Centralisation
- ViewPaths.CONFORMITE_LIVE_FEED + getter ViewPathsBean
- menu.xhtml : entrée Live Feed sous sous-menu Conformité (icon pi-bolt)

Tests (13/13 verts)
- couleurAction × 4 (danger/success/info/autres)
- couleurSod
- tempsRelatif × 6 (null, secondes, minutes, heures, jours, futur)
- setLimit clamp × 4
- defaults
2026-04-25 16:10:41 +00:00
dahoud
e936af7d39 feat(sprint-14 web 2026-04-25): picker p:autoComplete pour Compliance Officer (UX vs UUID textuel)
DRY strict — réutilise MembreService REST client existant, aucun nouveau client.

ComplianceOfficerPickerBean (@Named, @ApplicationScoped)
- suggest(query) : recherche multi-champ (nom OU prénom) via MembreService.rechercher,
  dédoublonne via LinkedHashMap, gère erreur gracieuse → []
- label(membre) : "Prénom NOM (numéro)" avec fallback id si entité minimaliste
- resoudre(uuid) : pour affichage initial mode édition

UI organisation-form.xhtml
- Remplacement p:inputText UUID → p:autoComplete forceSelection minQueryLength=2 queryDelay=300
- Placeholder "Tapez 2+ lettres du nom ou prénom..."
- Stocke UUID, affiche label humain — DTO unchanged côté backend

Tests (8 tests, logique pure sans mock REST)
- label × 6 (null, complet, sans numéro, nom seul, prénom seul, fallback id)
- suggest × 2 (query null/blank → liste vide sans appel réseau)
- resoudre × 1 (id null)

ACTION USER : `mvn install` côté unionflow-server-api 1.0.9 puis tester web local.
2026-04-25 15:36:37 +00:00
dahoud
11a1299bc7 feat(sprint-13.A+B web 2026-04-25): formulaire org enrichi conformité + 3 pages stubs détectées Sprint 12
S13.A — Formulaire organisation enrichi
- ui/includes/organisation-form.xhtml : nouvelle fieldset 🛡️ Conformité réglementaire
- selectOneMenu referentielComptable : SYSCOHADA / SYCEBNL / PCSFD_UMOA + auto
- inputText complianceOfficerId (UUID) avec tooltip Instr. BCEAO 001-03-2025
- Insérée entre fieldset Budget & Mission

S13.B — 3 pages stubs (détectées par test ViewPathsConsistency Sprint 12)
- pages/secure/evenement/bilan-detail.xhtml : stub bilan détail événement (panel "en construction")
- pages/admin/parametres.xhtml : hub paramètres admin (3 boutons : profil, préférences, notifications)
- pages/membre/parametres.xhtml : hub paramètres membre (3 boutons identiques)
- Tous les outcomes utilisent #{paths.xxx} (DRY centralisation Sprint 12)
- ViewPathsConsistencyTest : KNOWN_MISSING_PAGES vidé (tous les paths existent)

Bump dépendance api 1.0.8 → 1.0.9 (OrganisationResponse exposant les 2 champs)
Quarkus inchangé (3.27.3)

ACTION USER : `mvn install` après que api 1.0.9 soit en m2 local.
2026-04-25 15:24:55 +00:00
dahoud
cdcdb37459 refactor(sprint-12 web 2026-04-25): centralisation navigation outcomes via ViewPaths constants + ViewPathsBean
DRY strict appliqué pour la maintenance — outcomes de navigation centralisés en un lieu unique
au lieu de paths hardcodés disséminés dans 13+ fichiers.

Architecture
- constants.ViewPaths : classe finale + ~80 constantes public static final String, organisées
  par module (Dashboard, Membres, Adhésions, Cotisations, Finance, Crédit, Évènements, Aide,
  Communication, Documents, Organisation, Conformité, Admin, Rapports, Souscription, Super-Admin)
- view.ViewPathsBean (@Named "paths" @ApplicationScoped) : expose les constantes en EL aux XHTML

Migration menu.xhtml (65 edits, 0 path orphelin)
- outcome="/pages/secure/dashboard" → outcome="#{paths.dashboard}"
- outcome="/pages/secure/conformite/dashboard" → outcome="#{paths.conformiteDashboard}"
- ... 60+ autres mappings

Migration autres XHTML (13 edits, 12 fichiers)
- index, error/viewExpired, topbar, admin × 4, evenement × 2, organisation × 2, reports

Migration beans Java (4 fichiers, 18 returns)
- NavigationBean : goToProfile, goToSettings (super-admin / admin / membre), getDashboardUrlForUserType
- DashboardMembreBean : 5 méthodes navigation (cotisations, événement, aide, profil)
- MembreDashboardBean : voirEvenement, payerCotisations
- OrganisationsBean : retour vers liste

Test cohérence (3 tests, 100%)
- chaquePathExisteCommeFichier : reflectif sur ViewPaths, vérifie xhtml en classpath
- redirectSuffixFormat : structure REDIRECT_SUFFIX
- aucuneConstanteVide : non-null, non-blank

Dette détectée (3 pages référencées mais inexistantes — flaggées KNOWN_MISSING_PAGES)
- /pages/secure/evenement/bilan-detail.xhtml
- /pages/admin/parametres.xhtml
- /pages/membre/parametres.xhtml

Bénéfices
- 1 seul lieu pour renommer/déplacer une page
- Type-safety compile-time côté Java
- Tests détectent automatiquement les paths orphelins
- IDE complétion sur les getters paths.xxx
2026-04-25 14:34:11 +00:00
dahoud
917c8c5359 feat(sprint-11 web 2026-04-25): pages PrimeFaces Sprint 10 (UBO, audit-trail viewer, délégations rôles) + bump api 1.0.6→1.0.8
DRY strict appliqué : web réutilise directement les DTOs officiels de
unionflow-server-api 1.0.8 (CreateBeneficiaireEffectifRequest, BeneficiaireEffectifResponse,
AuditTrailOperationResponse, CreateRoleDelegationRequest, RoleDelegationResponse) au lieu
de DTOs miroirs locaux. Aucune duplication.

Bump dépendance api 1.0.6 → 1.0.8

REST clients @RegisterRestClient configKey=unionflow-api
- BeneficiaireEffectifRestClient : CRUD lister/trouverParId/creer/mettreAJour/desactiver
- AuditTrailRestClient : 5 endpoints lecture (parUtilisateur, historique, parOrganisation, sodViolations, financial)
- RoleDelegationRestClient : listerParOrganisation / creer / revoquer

Beans @ViewScoped
- BeneficiaireEffectifBean : recherche (KYC|org|PEP), création formulaire, marquerPep, désactiver
- AuditTrailViewerBean : 5 modes (USER/ENTITY/ORG/SOD_VIOLATIONS/FINANCIAL), couleurAction (DELETE→danger, VALIDATE→success, etc.), couleurSod
- RoleDelegationBean : recherche/créer/révoquer, couleurStatut (ACTIVE/REVOQUEE/EXPIREE)

Pages XHTML
- /pages/secure/conformite/beneficiaires-effectifs.xhtml — recherche + tableau + nouvelle UBO (panel toggleable)
- /pages/secure/conformite/audit-trail.xhtml — filtres mode + tableau + détail JSONB (pre format)
- /pages/secure/admin/role-delegations.xhtml — table actives + nouvelle (datePicker dates)

MenuBean + menu.xhtml
- 3 nouveaux flags : isBeneficiairesEffectifsVisible, isAuditTrailViewerVisible, isRoleDelegationsVisible
- 3 menuitems ajoutés au sous-menu Conformité existant (icônes pi-users, pi-history, pi-share-alt)
- Gating par rôles : COMPLIANCE_OFFICER + CONTROLEUR_INTERNE pour audit ; ADMIN_ORGANISATION + PRESIDENT pour délégations

Tests (10/10 verts, 31/31 cumulé S8+S11)
- AuditTrailViewerBeanTest : 8 tests (couleurAction × 6 cas, couleurSod, defaults)
- RoleDelegationBeanTest : 2 tests (couleurStatut × 5, defaults)
2026-04-25 12:56:13 +00:00
dahoud
8f96fa4209 fix(sprint-9 followup web): expose nouvelles pages conformité dans le menu de navigation
Les 3 pages PrimeFaces livrées en Sprint 8 (compliance dashboard, rapports trimestriels,
PI-SPI readiness) existaient mais étaient inaccessibles via la navigation principale.

MenuBean
- isConformiteDashboardVisible : SUPER_ADMIN, ADMIN_ORGANISATION, PRESIDENT, TRESORIER, COMPLIANCE_OFFICER, CONTROLEUR_INTERNE
- isRapportsTrimestrielsVisible : SUPER_ADMIN, ADMIN_ORGANISATION, PRESIDENT, CONTROLEUR_INTERNE
- isPispiReadinessVisible : SUPER_ADMIN, COMPLIANCE_OFFICER

menu.xhtml
- Nouveau sous-menu "Conformité" (icône pi-verified) inséré après "Gestion Financière"
- 3 menuitems gated par les flags ci-dessus
2026-04-25 11:21:58 +00:00
dahoud
a7788036eb feat(sprint-8 web 2026-04-25): pages PrimeFaces conformité (compliance dashboard + rapports trimestriels + PI-SPI readiness)
Front-end web JSF/PrimeFaces pour exposer les features backend Sprints 3, 5, 7 aux compliance officers et controleurs internes.

DTOs locaux (miroirs JSON, @JsonIgnoreProperties)
- ComplianceSnapshotDto + ConformiteIndicateurDto (P1-NEW-7)
- RapportTrimestrielDto + helpers estDraft/estSigne/estArchive (P2-NEW-3)
- PispiReadinessDto + CheckResultDto + helpers estReady/estBlocked (P1-NEW-15)

REST clients @RegisterRestClient configKey=unionflow-api
- ComplianceDashboardRestClient : getSnapshotCurrent + getSnapshotOf(orgId)
- RapportTrimestrielRestClient : lister, generer, signer, archiver, telechargerPdf
- PispiReadinessRestClient : getReadiness
- AuthHeaderFactory propage le token OIDC

Beans @ViewScoped
- ConformiteDashboardBean : init + rafraichir + couleurScore + hasAlertes
- RapportsTrimestrielsBean : lister, genererRapport, signerSelection, archiverSelection, telechargerPdf via ExternalContext
- PispiReadinessBean : rafraichir + gestion HTTP 503 BLOCKED + couleurStatus + couleurCheck

Pages XHTML PrimeFaces (template main-template.xhtml, classes Freya)
- /pages/secure/conformite/dashboard.xhtml — score global + 9 indicateurs en grille + alertes
- /pages/secure/conformite/rapports-trimestriels.xhtml — table DRAFT/SIGNE/ARCHIVE + bouton générer/signer/archiver/PDF
- /pages/secure/admin/pispi-readiness.xhtml — 8 checks + blocages/warnings dédiés + statut global

Tests (21/21 verts, JUnit5 natif puisque AssertJ non transitif)
- ConformiteDashboardBeanTest : 9 tests (couleur score success/warning/danger/secondary, hasAlertes 5 cas)
- PispiReadinessBeanTest : 8 tests (couleurStatus READY/DEGRADED/BLOCKED/null, couleurCheck PASS/FAIL × severity, DTO helpers)
- RapportTrimestrielDtoTest : 4 tests (estDraft/estSigne/estArchive/inconnu)
2026-04-25 11:02:48 +00:00
0c5a027ec3 docs: Quarkus 3.15.1→3.27.3 LTS, Java 17→21, CHANGELOG v3.1.0 2026-04-24, section Déploiement lionsctl pipeline 2026-04-24 18:05:56 +00:00
0f360dab83 chore(docker): add root Dockerfile pinning ubi8/openjdk-21:1.21 + UID 1001 for lionsctl pipeline 2026-04-24 16:35:12 +00:00
64 changed files with 3917 additions and 110 deletions

View File

@@ -7,6 +7,23 @@ et ce projet adhère au [Semantic Versioning](https://semver.org/lang/fr/).
---
## [3.1.0] - 2026-04-24 🚀 **Quarkus 3.27.3 LTS**
### Infrastructure
- ⬆️ **Quarkus** : 3.15.1 → **3.27.3 LTS** (Java 17 → **Java 21**)
- ⬆️ **unionflow-server-api** : consommé en **1.0.7** (alignement DTOs + enrichissements `ville`/`pays`)
- 🔄 **Pipeline lionsctl** : migration déploiement Helm → `lionsctl pipeline` non-Helm
- Nouveau `Dockerfile` racine (`ubi8/openjdk-21:1.21`, UID 1001, `java -jar` direct)
- `.dockerignore` adapté fast-jar (`target/*` + `!target/quarkus-app/**`)
- 🔧 **Config Quarkus** : renommage clés dépréciées (5 props)
### Déploiement
- URL prod `https://unionflow.lions.dev` redéployée avec succès (selector Helm supprimé, svc repatch flat `app:`)
---
## [3.0.0] - 2026-01-04 🚀 **PRODUCTION-READY**
### 🎯 Migration Complète vers Architecture Production-Ready

22
Dockerfile Normal file
View File

@@ -0,0 +1,22 @@
# Dockerfile for unionflow-client-quarkus-primefaces-freya
# Used by lionsctl pipeline. Expects `mvn clean package -Pprod` to have produced target/quarkus-app/ (fast-jar).
FROM registry.access.redhat.com/ubi8/openjdk-21:1.21
ENV LANGUAGE='en_US:en'
COPY --chown=1001:1001 target/quarkus-app/lib/ /deployments/lib/
COPY --chown=1001:1001 target/quarkus-app/*.jar /deployments/
COPY --chown=1001:1001 target/quarkus-app/app/ /deployments/app/
COPY --chown=1001:1001 target/quarkus-app/quarkus/ /deployments/quarkus/
USER 1001
EXPOSE 8080
ENV JAVA_OPTS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
ENV JAVA_APP_JAR="/deployments/quarkus-run.jar"
HEALTHCHECK --interval=30s --timeout=3s --start-period=30s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
ENTRYPOINT [ "java", "-jar", "/deployments/quarkus-run.jar" ]

View File

@@ -1,7 +1,7 @@
# UnionFlow Client - Application Web de Gestion
![Java](https://img.shields.io/badge/Java-17-blue)
![Quarkus](https://img.shields.io/badge/Quarkus-3.15.1-red)
![Java](https://img.shields.io/badge/Java-21-blue)
![Quarkus](https://img.shields.io/badge/Quarkus-3.27.3_LTS-red)
![PrimeFaces](https://img.shields.io/badge/PrimeFaces-14.0.5-green)
![Jakarta EE](https://img.shields.io/badge/Jakarta%20EE-10-orange)
![License](https://img.shields.io/badge/License-Proprietary-red)
@@ -35,7 +35,7 @@ Application web moderne de gestion pour organisations Lions Club, basée sur Qua
### Technologies Clés
- **Backend Framework**: Quarkus 3.15.1 (JVM optimisé, démarrage rapide)
- **Backend Framework**: Quarkus 3.27.3 LTS (Java 21, JVM optimisé, démarrage rapide)
- **UI Framework**: JSF 4.0 (MyFaces) + PrimeFaces 14.0.5
- **UI Theme**: Freya 5.0.0 (design moderne et responsive)
- **Sécurité**: Keycloak OIDC + JWT
@@ -95,7 +95,7 @@ Application web moderne de gestion pour organisations Lions Club, basée sur Qua
| Composant | Version | Rôle |
|-----------|---------|------|
| Java | 17 (LTS) | Langage |
| Quarkus | 3.15.1 | Framework application |
| Quarkus | 3.27.3 LTS | Framework application |
| Jakarta EE | 10 | Standard entreprise |
| JSF (MyFaces) | 4.0 | MVC web framework |
| PrimeFaces | 14.0.5 | Composants riches |
@@ -286,8 +286,8 @@ java -Dquarkus.profile=prod -jar target/quarkus-app/quarkus-run.jar
### Docker (Optionnel)
```bash
# Construction de l'image
docker build -f src/main/docker/Dockerfile.jvm -t unionflow-client:latest .
# Construction de l'image (Dockerfile à la racine — fast-jar, ubi8/openjdk-21:1.21, UID 1001)
docker build -t unionflow-client:latest .
# Lancement du conteneur
docker run -p 8080:8080 \
@@ -296,6 +296,21 @@ docker run -p 8080:8080 \
unionflow-client:latest
```
### Déploiement prod — lionsctl pipeline
```bash
lionsctl pipeline \
-u https://git.lions.dev/lionsdev/unionflow-client-quarkus-primefaces-freya \
-b main -j 21 -e production -c k1 -p prod
```
**URL prod** : `https://unionflow.lions.dev`
**Pré-requis infrastructure** (migration Helm → lionsctl) :
- Deployment Helm existant supprimé au préalable (selector immutable)
- Service selector à repatcher après pipeline (retirer labels `app.kubernetes.io/*`)
- OIDC : le pipeline hérite des secrets ESO `unionflow-client-oidc-eso` + `brevo-smtp-eso` (déjà référencés via `envFrom`)
## 🔒 Sécurité
### Authentification et Autorisation

View File

@@ -142,7 +142,7 @@
<dependency>
<groupId>dev.lions.unionflow</groupId>
<artifactId>unionflow-server-api</artifactId>
<version>1.0.6</version>
<version>1.0.10</version>
</dependency>
<!-- Lions User Manager Client - Module réutilisable de gestion d'utilisateurs Keycloak -->

View File

@@ -0,0 +1,29 @@
package dev.lions.unionflow.client.api.dto;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import java.math.BigDecimal;
import java.util.UUID;
/**
* Miroir local du record {@code ComplianceSnapshot} backend (P1-NEW-7).
* @since 2026-04-25 (Sprint 8)
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public record ComplianceSnapshotDto(
UUID organisationId,
String organisationNom,
String referentielComptable,
boolean complianceOfficerDesigne,
ConformiteIndicateurDto agAnnuelle,
ConformiteIndicateurDto rapportAirms,
int dirigeantsAvecCmu,
BigDecimal tauxKycAJourPct,
BigDecimal tauxFormationLbcFtPct,
ConformiteIndicateurDto commissaireAuxComptes,
ConformiteIndicateurDto fomusCi,
BigDecimal couvertureUboPct,
int scoreGlobal
) {
@JsonIgnoreProperties(ignoreUnknown = true)
public record ConformiteIndicateurDto(String statut, String message) {}
}

View File

@@ -0,0 +1,29 @@
package dev.lions.unionflow.client.api.dto;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import java.util.List;
/**
* Miroir local du DTO {@code ReadinessReport} backend (P1-NEW-15).
*
* @since 2026-04-25 (Sprint 8)
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public record PispiReadinessDto(
String globalStatus, // READY, DEGRADED, BLOCKED
String baseUrl,
List<CheckResultDto> checks,
List<String> blockingIssues,
List<String> warnings
) {
@JsonIgnoreProperties(ignoreUnknown = true)
public record CheckResultDto(
String name,
String status, // PASS, FAIL
String severity, // BLOCKING, WARNING
String message
) {}
public boolean estReady() { return "READY".equals(globalStatus); }
public boolean estBlocked() { return "BLOCKED".equals(globalStatus); }
}

View File

@@ -0,0 +1,30 @@
package dev.lions.unionflow.client.api.dto;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import java.time.LocalDateTime;
import java.util.UUID;
/**
* Miroir local de l'entité {@code RapportTrimestrielControleurInterne} backend (P2-NEW-3).
*
* <p>Champs JSONB et byte[] omis — récupérés via endpoint PDF dédié.
*
* @since 2026-04-25 (Sprint 8)
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public record RapportTrimestrielDto(
UUID id,
UUID organisationId,
Integer annee,
Integer trimestre,
LocalDateTime dateGeneration,
String statut,
Integer scoreConformite,
UUID signataireId,
LocalDateTime dateSignature,
String hashSha256
) {
public boolean estDraft() { return "DRAFT".equals(statut); }
public boolean estSigne() { return "SIGNE".equals(statut); }
public boolean estArchive() { return "ARCHIVE".equals(statut); }
}

View File

@@ -616,6 +616,61 @@ public class MenuBean implements Serializable {
hasAnyRole("SUPER_ADMIN", "ADMIN_ORGANISATION");
}
// ========================================================================
// Pages conformité (Sprint 8 — backend P1-NEW-7 / P2-NEW-3 / P1-NEW-15)
// ========================================================================
/**
* Tableau de bord conformité — Compliance Officer / Contrôleur Interne / direction.
* @since 2026-04-25
*/
public boolean isConformiteDashboardVisible() {
return hasAnyRole("SUPER_ADMIN", "ADMIN_ORGANISATION", "PRESIDENT",
"TRESORIER", "COMPLIANCE_OFFICER", "CONTROLEUR_INTERNE");
}
/**
* Rapports trimestriels Contrôleur Interne — restreint contrôleur + président + admin.
* @since 2026-04-25
*/
public boolean isRapportsTrimestrielsVisible() {
return hasAnyRole("SUPER_ADMIN", "ADMIN_ORGANISATION", "PRESIDENT",
"CONTROLEUR_INTERNE");
}
/**
* PI-SPI Readiness — admin technique uniquement.
* @since 2026-04-25
*/
public boolean isPispiReadinessVisible() {
return hasAnyRole("SUPER_ADMIN", "COMPLIANCE_OFFICER");
}
/**
* Bénéficiaires Effectifs (UBO) — Instr. BCEAO 003-03-2025.
* @since 2026-04-25 (Sprint 11)
*/
public boolean isBeneficiairesEffectifsVisible() {
return hasAnyRole("SUPER_ADMIN", "ADMIN_ORGANISATION", "COMPLIANCE_OFFICER",
"CONTROLEUR_INTERNE");
}
/**
* Audit Trail viewer — compliance / contrôle interne.
* @since 2026-04-25 (Sprint 11)
*/
public boolean isAuditTrailViewerVisible() {
return hasAnyRole("SUPER_ADMIN", "COMPLIANCE_OFFICER", "CONTROLEUR_INTERNE");
}
/**
* Délégations de rôles — admin org / président.
* @since 2026-04-25 (Sprint 11)
*/
public boolean isRoleDelegationsVisible() {
return hasAnyRole("SUPER_ADMIN", "ADMIN_ORGANISATION", "PRESIDENT");
}
/**
* Retourne true si l'organisation active dispose d'au moins un module métier spécifique
* (au-delà des modules communs toujours disponibles).

View File

@@ -0,0 +1,144 @@
package dev.lions.unionflow.client.constants;
/**
* Centralisation des chemins de vues JSF.
*
* <p>Lieu unique pour tous les outcomes de navigation. À utiliser depuis :
* <ul>
* <li>XHTML : {@code outcome="#{paths.uboList}"} via {@link
* dev.lions.unionflow.client.view.ViewPathsBean}</li>
* <li>Java beans : {@code return ViewPaths.UBO_LIST + ViewPaths.REDIRECT_SUFFIX;}</li>
* </ul>
*
* <p>Convention de nommage : {@code MODULE_PAGE} en MAJ avec underscore
* (ex: {@code COTISATION_PAIEMENT}, {@code SUPER_ADMIN_DASHBOARD}).
*
* @since 2026-04-25 (Sprint 12 — centralisation navigation DRY)
*/
public final class ViewPaths {
private ViewPaths() {
// Classe utilitaire non instantiable
}
// ─── Suffixes ────────────────────────────────────────────────────────────
public static final String REDIRECT_SUFFIX = "?faces-redirect=true";
// ─── Index / racine / erreur ─────────────────────────────────────────────
public static final String ROOT = "/";
public static final String INDEX = "/index";
// ─── Dashboard ───────────────────────────────────────────────────────────
public static final String DASHBOARD = "/pages/secure/dashboard";
public static final String DASHBOARD_MEMBRE = "/pages/secure/dashboard-membre";
// ─── Profil personnel ────────────────────────────────────────────────────
public static final String PROFILE = "/pages/secure/profile";
public static final String PERSONNEL_PROFIL = "/pages/secure/personnel/profil";
public static final String PERSONNEL_PARAMETRES = "/pages/secure/personnel/parametres";
public static final String PERSONNEL_PREFERENCES = "/pages/secure/personnel/preferences";
public static final String PERSONNEL_NOTIFICATIONS = "/pages/secure/personnel/notifications";
// ─── Membres ─────────────────────────────────────────────────────────────
public static final String MEMBRE_LISTE = "/pages/secure/membre/liste";
public static final String MEMBRE_INSCRIPTION = "/pages/secure/membre/inscription";
public static final String MEMBRE_VALIDATION = "/pages/secure/membre/validation";
public static final String MEMBRE_IMPORT = "/pages/secure/membre/import";
public static final String MEMBRE_EXPORT = "/pages/secure/membre/export";
public static final String MEMBRE_RECHERCHE = "/pages/secure/membre/recherche";
public static final String MEMBRE_COTISATIONS = "/pages/secure/membre/cotisations";
public static final String MEMBRE_PROFIL = "/pages/secure/membre/profil";
public static final String MEMBRE_PAIEMENT_COTISATIONS = "/pages/secure/membre/paiement-mes-cotisations";
// ─── Adhésions ───────────────────────────────────────────────────────────
public static final String ADHESION_LISTE = "/pages/secure/adhesion/liste";
public static final String ADHESION_DEMANDE = "/pages/secure/adhesion/demande";
public static final String ADHESION_VALIDATION = "/pages/secure/adhesion/validation";
public static final String ADHESION_HISTORIQUE = "/pages/secure/adhesion/historique";
public static final String ADHESION_RENOUVELLEMENT = "/pages/secure/adhesion/renouvellement";
// ─── Cotisations ─────────────────────────────────────────────────────────
public static final String COTISATIONS_GESTION_ADMIN = "/pages/admin/cotisations/gestion";
public static final String COTISATION_PAIEMENT = "/pages/secure/cotisation/paiement";
public static final String COTISATION_HISTORIQUE = "/pages/secure/cotisation/historique";
public static final String COTISATION_RELANCES = "/pages/secure/cotisation/relances";
// ─── Finance ─────────────────────────────────────────────────────────────
public static final String FINANCE_TRESORERIE = "/pages/secure/finance/tresorerie";
public static final String FINANCE_BUDGETS = "/pages/secure/finance/budgets";
public static final String FINANCE_BILANS = "/pages/secure/finance/bilans";
public static final String FINANCE_APPROBATIONS = "/pages/secure/finance/approbations";
public static final String COMPTABILITE_GESTION = "/pages/secure/comptabilite/gestion";
// ─── Épargne / Crédit ────────────────────────────────────────────────────
public static final String EPARGNE_COMPTES = "/pages/secure/epargne/comptes";
public static final String CREDIT_DEMANDES = "/pages/secure/credit/demandes";
public static final String CREDIT_EVALUATION = "/pages/secure/credit/evaluation";
public static final String CREDIT_SUIVI = "/pages/secure/credit/suivi";
public static final String CREDIT_REMBOURSEMENTS = "/pages/secure/credit/remboursements";
public static final String CREDIT_STATISTIQUES = "/pages/secure/credit/statistiques";
// ─── Événements ──────────────────────────────────────────────────────────
public static final String EVENEMENT_GESTION = "/pages/secure/evenement/gestion";
public static final String EVENEMENT_CREATION = "/pages/secure/evenement/creation";
public static final String EVENEMENT_CALENDRIER = "/pages/secure/evenement/calendrier";
public static final String EVENEMENT_PARTICIPANTS = "/pages/secure/evenement/participants";
public static final String EVENEMENT_PLANIFICATION = "/pages/secure/evenement/planification";
public static final String EVENEMENT_LOGISTIQUE = "/pages/secure/evenement/logistique";
public static final String EVENEMENT_BILAN = "/pages/secure/evenement/bilan";
public static final String EVENEMENT_BILAN_DETAIL = "/pages/secure/evenement/bilan-detail";
// ─── Aide / support ──────────────────────────────────────────────────────
public static final String AIDE_DEMANDE = "/pages/secure/aide/demande";
public static final String AIDE_REQUESTS = "/pages/secure/aide/requests";
public static final String AIDE_APPROVED = "/pages/secure/aide/approved";
public static final String AIDE_TRAITEMENT = "/pages/secure/aide/traitement";
public static final String AIDE_HISTORIQUE = "/pages/secure/aide/historique";
public static final String AIDE_STATISTIQUES = "/pages/secure/aide/statistiques";
public static final String AIDE_FAQ = "/pages/secure/aide/faq";
public static final String AIDE_SUPPORT = "/pages/secure/aide/support";
public static final String AIDE_APROPOS = "/pages/secure/aide/apropos";
// ─── Communication / Documents ───────────────────────────────────────────
public static final String COMMUNICATION_CONVERSATIONS = "/pages/secure/communication/conversations";
public static final String COMMUNICATION_NOTIFICATIONS = "/pages/secure/communication/notifications";
public static final String DOCUMENTS_MES_DOCUMENTS = "/pages/secure/documents/mes-documents";
// ─── Organisation ────────────────────────────────────────────────────────
public static final String ORGANISATION_LISTE = "/pages/secure/organisation/liste";
public static final String ORGANISATION_DETAIL = "/pages/secure/organisation/detail";
public static final String ORGANISATION_NOUVELLE = "/pages/secure/organisation/nouvelle";
public static final String ORGANISATION_STATISTIQUES = "/pages/secure/organisation/statistiques";
// ─── Conformité (Sprints 8, 11) ──────────────────────────────────────────
public static final String CONFORMITE_DASHBOARD = "/pages/secure/conformite/dashboard";
public static final String CONFORMITE_RAPPORTS_TRIMESTRIELS = "/pages/secure/conformite/rapports-trimestriels";
public static final String CONFORMITE_BENEFICIAIRES_EFFECTIFS = "/pages/secure/conformite/beneficiaires-effectifs";
public static final String CONFORMITE_AUDIT_TRAIL = "/pages/secure/conformite/audit-trail";
public static final String CONFORMITE_LIVE_FEED = "/pages/secure/conformite/live-feed";
// ─── Admin technique ─────────────────────────────────────────────────────
public static final String ADMIN_PISPI_READINESS = "/pages/secure/admin/pispi-readiness";
public static final String ADMIN_ROLE_DELEGATIONS = "/pages/secure/admin/role-delegations";
public static final String ADMIN_SAUVEGARDE = "/pages/secure/admin/sauvegarde";
public static final String ADMIN_AUDIT_JOURNAL = "/pages/admin/audit/journal";
public static final String ADMIN_LOGS_SYSTEME = "/pages/admin/logs/systeme";
public static final String ADMIN_PARAMETRES = "/pages/admin/parametres";
public static final String MEMBRE_PARAMETRES = "/pages/membre/parametres";
// ─── Rapports ────────────────────────────────────────────────────────────
public static final String RAPPORT_FINANCES = "/pages/secure/rapport/finances";
public static final String RAPPORT_MEMBRES = "/pages/secure/rapport/membres";
public static final String RAPPORT_ACTIVITES = "/pages/secure/rapport/activites";
public static final String RAPPORT_TABLEAUX_BORD = "/pages/secure/rapport/tableaux-bord";
public static final String RAPPORT_EXPORT = "/pages/secure/rapport/export";
// ─── Souscription ────────────────────────────────────────────────────────
public static final String SOUSCRIPTION_DASHBOARD = "/pages/secure/souscription/dashboard";
// ─── Super-Admin ─────────────────────────────────────────────────────────
public static final String SUPER_ADMIN_DASHBOARD = "/pages/super-admin/dashboard";
public static final String SUPER_ADMIN_ROLES_GESTION = "/pages/super-admin/roles/gestion";
public static final String SUPER_ADMIN_TYPES_ORGANISATIONS = "/pages/super-admin/types/organisations";
public static final String SUPER_ADMIN_CONFIGURATION_SYSTEME = "/pages/super-admin/configuration/systeme";
}

View File

@@ -0,0 +1,62 @@
package dev.lions.unionflow.client.service;
import dev.lions.unionflow.client.security.AuthHeaderFactory;
import dev.lions.unionflow.server.api.dto.audit.response.AuditTrailOperationResponse;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
import java.util.UUID;
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
/**
* Client REST de lecture audit trail (Sprint 11 ⇄ backend Sprint 10 CQRS read).
*/
@RegisterRestClient(configKey = "unionflow-api")
@RegisterClientHeaders(AuthHeaderFactory.class)
@Path("/api/audit-trail")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public interface AuditTrailRestClient {
@GET
@Path("/by-user/{userId}")
List<AuditTrailOperationResponse> parUtilisateur(
@PathParam("userId") UUID userId,
@QueryParam("from") String from,
@QueryParam("to") String to);
@GET
@Path("/by-entity/{type}/{id}")
List<AuditTrailOperationResponse> historique(
@PathParam("type") String entityType, @PathParam("id") UUID entityId);
@GET
@Path("/by-organisation/{orgId}")
List<AuditTrailOperationResponse> parOrganisation(@PathParam("orgId") UUID orgId);
@GET
@Path("/sod-violations")
List<AuditTrailOperationResponse> violationsSod();
@GET
@Path("/financial/{orgId}")
List<AuditTrailOperationResponse> operationsFinancieres(
@PathParam("orgId") UUID orgId,
@QueryParam("from") String from,
@QueryParam("to") String to);
/** Live Activity Feed (Sprint 15) — N opérations les plus récentes selon scope. */
@GET
@Path("/recent")
List<AuditTrailOperationResponse> recent(
@QueryParam("scope") String scope,
@QueryParam("orgId") UUID orgId,
@QueryParam("userId") UUID userId,
@QueryParam("limit") int limit);
}

View File

@@ -0,0 +1,54 @@
package dev.lions.unionflow.client.service;
import dev.lions.unionflow.client.security.AuthHeaderFactory;
import dev.lions.unionflow.server.api.dto.kyc.request.CreateBeneficiaireEffectifRequest;
import dev.lions.unionflow.server.api.dto.kyc.request.UpdateBeneficiaireEffectifRequest;
import dev.lions.unionflow.server.api.dto.kyc.response.BeneficiaireEffectifResponse;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
import java.util.UUID;
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
/**
* Client REST des Bénéficiaires Effectifs (Sprint 11 ⇄ backend Sprint 10).
* Réutilise les DTOs officiels de unionflow-server-api 1.0.8 — zéro duplication.
*/
@RegisterRestClient(configKey = "unionflow-api")
@RegisterClientHeaders(AuthHeaderFactory.class)
@Path("/api/kyc/beneficiaires-effectifs")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public interface BeneficiaireEffectifRestClient {
@GET
List<BeneficiaireEffectifResponse> lister(
@QueryParam("kycDossierId") UUID kycDossierId,
@QueryParam("organisationCibleId") UUID organisationCibleId,
@QueryParam("pep") Boolean pep);
@GET
@Path("/{id}")
BeneficiaireEffectifResponse trouverParId(@PathParam("id") UUID id);
@POST
BeneficiaireEffectifResponse creer(CreateBeneficiaireEffectifRequest request);
@PUT
@Path("/{id}")
BeneficiaireEffectifResponse mettreAJour(
@PathParam("id") UUID id, UpdateBeneficiaireEffectifRequest request);
@DELETE
@Path("/{id}")
void desactiver(@PathParam("id") UUID id);
}

View File

@@ -0,0 +1,30 @@
package dev.lions.unionflow.client.service;
import dev.lions.unionflow.client.api.dto.ComplianceSnapshotDto;
import dev.lions.unionflow.client.security.AuthHeaderFactory;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import java.util.UUID;
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
@RegisterRestClient(configKey = "unionflow-api")
@RegisterClientHeaders(AuthHeaderFactory.class)
@Path("/api/compliance/dashboard")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public interface ComplianceDashboardRestClient {
/** Snapshot pour l'organisation active (résolue par le backend via header X-Active-Organisation-Id). */
@GET
ComplianceSnapshotDto getSnapshotCurrent();
/** Snapshot d'une organisation arbitraire — restreint SUPER_ADMIN backend. */
@GET
@Path("/{organisationId}")
ComplianceSnapshotDto getSnapshotOf(@PathParam("organisationId") UUID organisationId);
}

View File

@@ -0,0 +1,22 @@
package dev.lions.unionflow.client.service;
import dev.lions.unionflow.client.api.dto.PispiReadinessDto;
import dev.lions.unionflow.client.security.AuthHeaderFactory;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
@RegisterRestClient(configKey = "unionflow-api")
@RegisterClientHeaders(AuthHeaderFactory.class)
@Path("/api/admin/pispi/readiness")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public interface PispiReadinessRestClient {
@GET
PispiReadinessDto getReadiness();
}

View File

@@ -0,0 +1,25 @@
package dev.lions.unionflow.client.service;
import dev.lions.unionflow.server.api.dto.compliance.response.KpiPublicSnapshot;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
/**
* Client REST public KPI (Sprint 17 — pas de header auth, token signé en query).
*
* <p>Note : pas de {@code @RegisterClientHeaders(AuthHeaderFactory.class)} car endpoint anonyme.
*/
@RegisterRestClient(configKey = "unionflow-api")
@Path("/api/public/kpi")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public interface PublicKpiRestClient {
@GET
KpiPublicSnapshot consulter(@QueryParam("token") String token);
}

View File

@@ -0,0 +1,51 @@
package dev.lions.unionflow.client.service;
import dev.lions.unionflow.client.api.dto.RapportTrimestrielDto;
import dev.lions.unionflow.client.security.AuthHeaderFactory;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
import java.util.UUID;
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
@RegisterRestClient(configKey = "unionflow-api")
@RegisterClientHeaders(AuthHeaderFactory.class)
@Path("/api/rapports/trimestriel")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public interface RapportTrimestrielRestClient {
@GET
List<RapportTrimestrielDto> lister(
@QueryParam("orgId") UUID orgId,
@QueryParam("annee") Integer annee);
@POST
@Path("/generer")
RapportTrimestrielDto generer(
@QueryParam("orgId") UUID orgId,
@QueryParam("annee") int annee,
@QueryParam("trimestre") int trimestre);
@POST
@Path("/{id}/signer")
RapportTrimestrielDto signer(
@PathParam("id") UUID id,
@QueryParam("signataireId") UUID signataireId);
@POST
@Path("/{id}/archiver")
RapportTrimestrielDto archiver(@PathParam("id") UUID id);
@GET
@Path("/{id}/pdf")
@Produces("application/pdf")
byte[] telechargerPdf(@PathParam("id") UUID id);
}

View File

@@ -0,0 +1,42 @@
package dev.lions.unionflow.client.service;
import dev.lions.unionflow.client.security.AuthHeaderFactory;
import dev.lions.unionflow.server.api.dto.delegation.request.CreateRoleDelegationRequest;
import dev.lions.unionflow.server.api.dto.delegation.response.RoleDelegationResponse;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
import java.util.UUID;
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
/**
* Client REST des délégations de rôle (Sprint 11 ⇄ backend Sprint 10).
*/
@RegisterRestClient(configKey = "unionflow-api")
@RegisterClientHeaders(AuthHeaderFactory.class)
@Path("/api/role-delegations")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public interface RoleDelegationRestClient {
@GET
@Path("/organisation/{orgId}")
List<RoleDelegationResponse> listerParOrganisation(@PathParam("orgId") UUID orgId);
@POST
RoleDelegationResponse creer(
CreateRoleDelegationRequest request,
@QueryParam("rolesDelegataire") String rolesDelegataireCsv);
@DELETE
@Path("/{id}")
RoleDelegationResponse revoquer(@PathParam("id") UUID id);
}

View File

@@ -0,0 +1,113 @@
package dev.lions.unionflow.client.view;
import dev.lions.unionflow.client.service.AuditTrailRestClient;
import dev.lions.unionflow.server.api.dto.audit.response.AuditTrailOperationResponse;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.jboss.logging.Logger;
/**
* Bean viewer audit trail (Sprint 11 ⇄ backend Sprint 10 CQRS read).
* Pas d'écriture — la production des événements est dans le backend lifecycle.
*/
@Named
@ViewScoped
public class AuditTrailViewerBean implements Serializable {
private static final long serialVersionUID = 1L;
private static final Logger LOG = Logger.getLogger(AuditTrailViewerBean.class);
@Inject @RestClient AuditTrailRestClient client;
/** Mode : USER, ENTITY, ORG, SOD_VIOLATIONS, FINANCIAL */
private String mode = "ORG";
private UUID userId;
private UUID orgId;
private String entityType;
private UUID entityId;
private LocalDateTime from = LocalDateTime.now().minusDays(30);
private LocalDateTime to = LocalDateTime.now();
private List<AuditTrailOperationResponse> operations = Collections.emptyList();
private AuditTrailOperationResponse selection;
private String erreur;
public void rechercher() {
erreur = null;
try {
operations = switch (mode) {
case "USER" -> userId != null
? client.parUtilisateur(userId, from.toString(), to.toString())
: Collections.emptyList();
case "ENTITY" -> entityType != null && entityId != null
? client.historique(entityType, entityId)
: Collections.emptyList();
case "ORG" -> orgId != null
? client.parOrganisation(orgId)
: Collections.emptyList();
case "SOD_VIOLATIONS" -> client.violationsSod();
case "FINANCIAL" -> orgId != null
? client.operationsFinancieres(orgId, from.toString(), to.toString())
: Collections.emptyList();
default -> Collections.emptyList();
};
LOG.infof("Audit trail (%s) chargé : %d entrées", mode, operations.size());
} catch (Exception e) {
handleError("Recherche audit trail échouée", e);
operations = Collections.emptyList();
}
}
public String getCouleurAction(String actionType) {
if (actionType == null) return "secondary";
return switch (actionType) {
case "DELETE", "PAYMENT_FAILED" -> "danger";
case "VALIDATE", "PAYMENT_CONFIRMED", "AID_REQUEST_APPROVED" -> "success";
case "UPDATE", "PAYMENT_INITIATED", "BUDGET_APPROVED" -> "info";
case "CREATE" -> "primary";
case "EXPORT" -> "warning";
default -> "secondary";
};
}
public String getCouleurSod(Boolean sodCheckPassed) {
if (sodCheckPassed == null) return "secondary";
return sodCheckPassed ? "success" : "danger";
}
private void handleError(String summary, Exception e) {
LOG.warnf("%s : %s", summary, e.getMessage());
erreur = summary + "" + e.getMessage();
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur", erreur));
}
public String getMode() { return mode; }
public void setMode(String mode) { this.mode = mode; }
public UUID getUserId() { return userId; }
public void setUserId(UUID userId) { this.userId = userId; }
public UUID getOrgId() { return orgId; }
public void setOrgId(UUID orgId) { this.orgId = orgId; }
public String getEntityType() { return entityType; }
public void setEntityType(String entityType) { this.entityType = entityType; }
public UUID getEntityId() { return entityId; }
public void setEntityId(UUID entityId) { this.entityId = entityId; }
public LocalDateTime getFrom() { return from; }
public void setFrom(LocalDateTime from) { this.from = from; }
public LocalDateTime getTo() { return to; }
public void setTo(LocalDateTime to) { this.to = to; }
public List<AuditTrailOperationResponse> getOperations() { return operations; }
public AuditTrailOperationResponse getSelection() { return selection; }
public void setSelection(AuditTrailOperationResponse selection) { this.selection = selection; }
public String getErreur() { return erreur; }
}

View File

@@ -0,0 +1,165 @@
package dev.lions.unionflow.client.view;
import dev.lions.unionflow.client.service.BeneficiaireEffectifRestClient;
import dev.lions.unionflow.server.api.dto.kyc.request.CreateBeneficiaireEffectifRequest;
import dev.lions.unionflow.server.api.dto.kyc.request.UpdateBeneficiaireEffectifRequest;
import dev.lions.unionflow.server.api.dto.kyc.response.BeneficiaireEffectifResponse;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.io.Serializable;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.jboss.logging.Logger;
/**
* Bean CRUD des Bénéficiaires Effectifs (Sprint 11 ⇄ backend Sprint 10).
*/
@Named
@ViewScoped
public class BeneficiaireEffectifBean implements Serializable {
private static final long serialVersionUID = 1L;
private static final Logger LOG = Logger.getLogger(BeneficiaireEffectifBean.class);
@Inject @RestClient BeneficiaireEffectifRestClient client;
// Filtres
private UUID kycDossierId;
private UUID organisationCibleId;
private Boolean filtrePep;
private List<BeneficiaireEffectifResponse> ubos = Collections.emptyList();
private BeneficiaireEffectifResponse selection;
private String erreur;
// Formulaire de création
private String nom;
private String prenoms;
private String nationalite;
private String paysResidence;
private String numeroPieceIdentite;
private String pourcentageCapital;
private String natureControle = "DETENTION_CAPITAL";
private boolean estPep;
public void rechercher() {
erreur = null;
try {
ubos = client.lister(kycDossierId, organisationCibleId, filtrePep);
LOG.infof("UBOs chargés : %d", ubos.size());
} catch (Exception e) {
handleError("Recherche UBO échouée", e);
ubos = Collections.emptyList();
}
}
public void creer() {
if (kycDossierId == null && organisationCibleId == null) {
addMessage(FacesMessage.SEVERITY_WARN, "Cible requise",
"Sélectionnez d'abord un KycDossier ou une organisation cible");
return;
}
try {
CreateBeneficiaireEffectifRequest req = CreateBeneficiaireEffectifRequest.builder()
.kycDossierId(kycDossierId)
.organisationCibleId(organisationCibleId)
.nom(nom)
.prenoms(prenoms)
.nationalite(nationalite != null ? nationalite.toUpperCase() : null)
.paysResidence(paysResidence != null ? paysResidence.toUpperCase() : null)
.numeroPieceIdentite(numeroPieceIdentite)
.pourcentageCapital(pourcentageCapital != null && !pourcentageCapital.isBlank()
? new java.math.BigDecimal(pourcentageCapital) : null)
.natureControle(natureControle)
.estPep(estPep)
.build();
BeneficiaireEffectifResponse created = client.creer(req);
addMessage(FacesMessage.SEVERITY_INFO, "UBO créé",
created.prenoms() + " " + created.nom());
resetForm();
rechercher();
} catch (Exception e) {
handleError("Création UBO échouée", e);
}
}
public void desactiverSelection() {
if (selection == null) return;
try {
client.desactiver(selection.id());
addMessage(FacesMessage.SEVERITY_INFO, "UBO désactivé",
selection.prenoms() + " " + selection.nom());
rechercher();
} catch (Exception e) {
handleError("Désactivation échouée", e);
}
}
public void marquerPepSelection() {
if (selection == null) return;
try {
UpdateBeneficiaireEffectifRequest req = UpdateBeneficiaireEffectifRequest.builder()
.estPep(true).build();
client.mettreAJour(selection.id(), req);
addMessage(FacesMessage.SEVERITY_INFO, "UBO marqué PEP", "");
rechercher();
} catch (Exception e) {
handleError("Mise à jour PEP échouée", e);
}
}
private void resetForm() {
nom = null;
prenoms = null;
nationalite = null;
paysResidence = null;
numeroPieceIdentite = null;
pourcentageCapital = null;
natureControle = "DETENTION_CAPITAL";
estPep = false;
}
private void handleError(String summary, Exception e) {
LOG.warnf("%s : %s", summary, e.getMessage());
erreur = summary + "" + e.getMessage();
addMessage(FacesMessage.SEVERITY_ERROR, "Erreur", erreur);
}
private void addMessage(FacesMessage.Severity sev, String summary, String detail) {
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(sev, summary, detail));
}
// Getters / setters
public UUID getKycDossierId() { return kycDossierId; }
public void setKycDossierId(UUID kycDossierId) { this.kycDossierId = kycDossierId; }
public UUID getOrganisationCibleId() { return organisationCibleId; }
public void setOrganisationCibleId(UUID id) { this.organisationCibleId = id; }
public Boolean getFiltrePep() { return filtrePep; }
public void setFiltrePep(Boolean filtrePep) { this.filtrePep = filtrePep; }
public List<BeneficiaireEffectifResponse> getUbos() { return ubos; }
public BeneficiaireEffectifResponse getSelection() { return selection; }
public void setSelection(BeneficiaireEffectifResponse selection) { this.selection = selection; }
public String getErreur() { return erreur; }
public String getNom() { return nom; }
public void setNom(String nom) { this.nom = nom; }
public String getPrenoms() { return prenoms; }
public void setPrenoms(String prenoms) { this.prenoms = prenoms; }
public String getNationalite() { return nationalite; }
public void setNationalite(String nationalite) { this.nationalite = nationalite; }
public String getPaysResidence() { return paysResidence; }
public void setPaysResidence(String paysResidence) { this.paysResidence = paysResidence; }
public String getNumeroPieceIdentite() { return numeroPieceIdentite; }
public void setNumeroPieceIdentite(String n) { this.numeroPieceIdentite = n; }
public String getPourcentageCapital() { return pourcentageCapital; }
public void setPourcentageCapital(String pct) { this.pourcentageCapital = pct; }
public String getNatureControle() { return natureControle; }
public void setNatureControle(String nc) { this.natureControle = nc; }
public boolean isEstPep() { return estPep; }
public void setEstPep(boolean estPep) { this.estPep = estPep; }
}

View File

@@ -0,0 +1,104 @@
package dev.lions.unionflow.client.view;
import dev.lions.unionflow.client.service.MembreService;
import dev.lions.unionflow.server.api.dto.membre.response.MembreResponse;
import dev.lions.unionflow.server.api.dto.membre.response.MembreSummaryResponse;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.io.Serializable;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.jboss.logging.Logger;
/**
* Bean picker pour la sélection du Compliance Officer (Sprint 14 — Instr. BCEAO 001-03-2025).
*
* <p>Réutilise {@link MembreService} existant — DRY strict, aucun nouveau REST client.
*
* <p>Usage XHTML :
* <pre>
* &lt;p:autoComplete value="#{model.complianceOfficerId}"
* completeMethod="#{complianceOfficerPickerBean.suggest}"
* var="m" itemLabel="#{complianceOfficerPickerBean.label(m)}"
* itemValue="#{m.id}" forceSelection="true" /&gt;
* </pre>
*
* @since 2026-04-25 (Sprint 14)
*/
@Named("complianceOfficerPickerBean")
@ApplicationScoped
public class ComplianceOfficerPickerBean implements Serializable {
private static final long serialVersionUID = 1L;
private static final Logger LOG = Logger.getLogger(ComplianceOfficerPickerBean.class);
@Inject @RestClient MembreService membreService;
/**
* Suggère des membres correspondant à la requête (utilisé par {@code p:autoComplete.completeMethod}).
* Recherche via nom OU prénom.
*/
public List<MembreSummaryResponse> suggest(String query) {
if (query == null || query.isBlank()) return Collections.emptyList();
try {
// Recherche larges : nom OU prénom contenant la query
List<MembreResponse> byNom = membreService.rechercher(
query, null, null, null, null, null, 0, 10);
List<MembreResponse> byPrenom = membreService.rechercher(
null, query, null, null, null, null, 0, 10);
java.util.LinkedHashMap<UUID, MembreSummaryResponse> uniques = new java.util.LinkedHashMap<>();
for (MembreResponse m : byNom) uniques.putIfAbsent(m.getId(), toSummary(m));
for (MembreResponse m : byPrenom) uniques.putIfAbsent(m.getId(), toSummary(m));
return new java.util.ArrayList<>(uniques.values());
} catch (Exception e) {
LOG.warnf("Suggest membres failed for query='%s' : %s", query, e.getMessage());
return Collections.emptyList();
}
}
/** Label de présentation : "Prénom NOM (numéro)". */
public String label(MembreSummaryResponse m) {
if (m == null) return "";
StringBuilder sb = new StringBuilder();
if (m.getPrenom() != null) sb.append(m.getPrenom());
if (m.getNom() != null) {
if (sb.length() > 0) sb.append(' ');
sb.append(m.getNom().toUpperCase());
}
if (m.getNumeroMembre() != null && !m.getNumeroMembre().isBlank()) {
sb.append(" (").append(m.getNumeroMembre()).append(')');
}
return sb.length() == 0 ? "(membre " + m.getId() + ")" : sb.toString();
}
/** Résolution UUID → membre pour affichage initial du form en mode édition. */
public MembreSummaryResponse resoudre(UUID id) {
if (id == null) return null;
try {
MembreResponse m = membreService.obtenirParId(id);
return toSummary(m);
} catch (Exception e) {
LOG.warnf("Résolution membre id=%s échouée : %s", id, e.getMessage());
return null;
}
}
// ── Mapping interne ────────────────────────────────────────────────────
private MembreSummaryResponse toSummary(MembreResponse m) {
if (m == null) return null;
MembreSummaryResponse s = new MembreSummaryResponse();
s.setId(m.getId());
s.setNom(m.getNom());
s.setPrenom(m.getPrenom());
s.setEmail(m.getEmail());
s.setTelephone(m.getTelephone());
s.setNumeroMembre(m.getNumeroMembre());
s.setStatutCompte(m.getStatutCompte());
return s;
}
}

View File

@@ -0,0 +1,76 @@
package dev.lions.unionflow.client.view;
import dev.lions.unionflow.client.api.dto.ComplianceSnapshotDto;
import dev.lions.unionflow.client.service.ComplianceDashboardRestClient;
import jakarta.annotation.PostConstruct;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.io.Serializable;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.jboss.logging.Logger;
/**
* Bean du tableau de bord de conformité (Sprint 8 ⇄ backend P1-NEW-7).
*
* <p>Affiche le snapshot de l'organisation active : score global, indicateurs AG/AIRMS/CMU/KYC/UBO,
* alertes critiques. Refresh manuel par bouton.
*
* @since 2026-04-25 (Sprint 8)
*/
@Named
@ViewScoped
public class ConformiteDashboardBean implements Serializable {
private static final long serialVersionUID = 1L;
private static final Logger LOG = Logger.getLogger(ConformiteDashboardBean.class);
@Inject @RestClient
ComplianceDashboardRestClient client;
private ComplianceSnapshotDto snapshot;
private String erreur;
@PostConstruct
public void init() {
rafraichir();
}
public void rafraichir() {
erreur = null;
try {
snapshot = client.getSnapshotCurrent();
LOG.infof("Snapshot conformité chargé : score=%d", snapshot.scoreGlobal());
} catch (Exception e) {
LOG.warnf("Échec chargement compliance dashboard : %s", e.getMessage());
erreur = "Impossible de charger le tableau de bord : " + e.getMessage();
addMessage(FacesMessage.SEVERITY_ERROR, "Erreur", erreur);
snapshot = null;
}
}
public String getCouleurScore() {
if (snapshot == null) return "secondary";
int s = snapshot.scoreGlobal();
if (s >= 80) return "success";
if (s >= 60) return "warning";
return "danger";
}
public boolean hasAlertes() {
if (snapshot == null) return false;
return !snapshot.complianceOfficerDesigne()
|| "RETARD".equals(snapshot.agAnnuelle().statut())
|| snapshot.scoreGlobal() < 60;
}
private void addMessage(FacesMessage.Severity sev, String summary, String detail) {
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(sev, summary, detail));
}
public ComplianceSnapshotDto getSnapshot() { return snapshot; }
public String getErreur() { return erreur; }
}

View File

@@ -1,5 +1,6 @@
package dev.lions.unionflow.client.view;
import dev.lions.unionflow.client.constants.ViewPaths;
import dev.lions.unionflow.server.api.dto.cotisation.response.CotisationResponse;
import dev.lions.unionflow.client.api.dto.MembreDashboardResponse;
import dev.lions.unionflow.client.service.CotisationService;
@@ -175,23 +176,23 @@ public class DashboardMembreBean implements Serializable {
// ═══════════════════════════════════════════════════════════════════════
public String allerAuxCotisations() {
return "/pages/secure/membre/paiement-mes-cotisations.xhtml?faces-redirect=true";
return ViewPaths.MEMBRE_PAIEMENT_COTISATIONS + ".xhtml" + ViewPaths.REDIRECT_SUFFIX;
}
public String inscrireEvenement() {
return "/pages/secure/evenement/calendrier.xhtml?faces-redirect=true";
return ViewPaths.EVENEMENT_CALENDRIER + ".xhtml" + ViewPaths.REDIRECT_SUFFIX;
}
public String demanderAide() {
return "/pages/secure/aide/demande.xhtml?faces-redirect=true";
return ViewPaths.AIDE_DEMANDE + ".xhtml" + ViewPaths.REDIRECT_SUFFIX;
}
public String allerAMonProfil() {
return "/pages/secure/membre/profil.xhtml?faces-redirect=true";
return ViewPaths.MEMBRE_PROFIL + ".xhtml" + ViewPaths.REDIRECT_SUFFIX;
}
public String allerAuxEvenements() {
return "/pages/secure/evenement/calendrier.xhtml?faces-redirect=true";
return ViewPaths.EVENEMENT_CALENDRIER + ".xhtml" + ViewPaths.REDIRECT_SUFFIX;
}
// ═══════════════════════════════════════════════════════════════════════

View File

@@ -0,0 +1,104 @@
package dev.lions.unionflow.client.view;
import dev.lions.unionflow.client.service.AuditTrailRestClient;
import dev.lions.unionflow.server.api.dto.audit.response.AuditTrailOperationResponse;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.io.Serializable;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.jboss.logging.Logger;
/**
* Bean Live Activity Feed (Sprint 15 — transparency opérationnelle).
*
* <p>Polling 10s sur {@code /api/audit-trail/recent} avec choix du scope :
* <ul>
* <li>SELF — opérations de l'utilisateur courant (défaut)</li>
* <li>ORG — opérations de l'organisation active</li>
* <li>ALL — toutes opérations (compliance/contrôleur uniquement)</li>
* </ul>
*
* <p>L'auto-refresh est piloté côté UI via {@code &lt;p:poll&gt;} qui appelle {@link #rafraichir()}.
*/
@Named
@ViewScoped
public class LiveFeedBean implements Serializable {
private static final long serialVersionUID = 1L;
private static final Logger LOG = Logger.getLogger(LiveFeedBean.class);
@Inject @RestClient AuditTrailRestClient client;
/** SELF (défaut, n'importe quel rôle), ORG (admin/officer), ALL (compliance/contrôleur). */
private String scope = "SELF";
private UUID orgId;
private UUID userId;
private int limit = 50;
private List<AuditTrailOperationResponse> operations = Collections.emptyList();
private long compteur;
private String erreur;
public void rafraichir() {
erreur = null;
try {
operations = client.recent(scope, orgId, userId, limit);
compteur++;
LOG.debugf("LiveFeed (%s) refresh #%d : %d ops", scope, compteur,
operations == null ? 0 : operations.size());
} catch (Exception e) {
LOG.warnf("LiveFeed refresh échoué : %s", e.getMessage());
erreur = "Échec rafraîchissement : " + e.getMessage();
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_WARN, "Erreur LiveFeed", erreur));
operations = Collections.emptyList();
}
}
public String getCouleurAction(String actionType) {
if (actionType == null) return "secondary";
return switch (actionType) {
case "DELETE", "PAYMENT_FAILED" -> "danger";
case "VALIDATE", "PAYMENT_CONFIRMED", "AID_REQUEST_APPROVED" -> "success";
case "UPDATE", "PAYMENT_INITIATED", "BUDGET_APPROVED" -> "info";
case "CREATE" -> "primary";
case "EXPORT" -> "warning";
default -> "secondary";
};
}
public String getCouleurSod(Boolean sodCheckPassed) {
if (sodCheckPassed == null) return "secondary";
return sodCheckPassed ? "success" : "danger";
}
/** Pour l'affichage relatif "il y a Xs". */
public String tempsRelatif(java.time.LocalDateTime dt) {
if (dt == null) return "";
long secondes = java.time.Duration.between(dt, java.time.LocalDateTime.now()).getSeconds();
if (secondes < 0) return "à l'instant";
if (secondes < 60) return "il y a " + secondes + "s";
if (secondes < 3600) return "il y a " + (secondes / 60) + "m";
if (secondes < 86400) return "il y a " + (secondes / 3600) + "h";
return "il y a " + (secondes / 86400) + "j";
}
// Getters / setters
public String getScope() { return scope; }
public void setScope(String scope) { this.scope = scope; }
public UUID getOrgId() { return orgId; }
public void setOrgId(UUID orgId) { this.orgId = orgId; }
public UUID getUserId() { return userId; }
public void setUserId(UUID userId) { this.userId = userId; }
public int getLimit() { return limit; }
public void setLimit(int limit) { this.limit = Math.max(1, Math.min(limit, 500)); }
public List<AuditTrailOperationResponse> getOperations() { return operations; }
public long getCompteur() { return compteur; }
public String getErreur() { return erreur; }
}

View File

@@ -1,5 +1,6 @@
package dev.lions.unionflow.client.view;
import dev.lions.unionflow.client.constants.ViewPaths;
import dev.lions.unionflow.server.api.dto.membre.response.MembreResponse;
import dev.lions.unionflow.server.api.dto.cotisation.response.CotisationResponse;
import dev.lions.unionflow.server.api.dto.evenement.response.EvenementResponse;
@@ -363,7 +364,7 @@ public class MembreDashboardBean implements Serializable {
}
public String voirEvenement(Evenement evenement) {
return "/pages/secure/evenement/gestion.xhtml?faces-redirect=true";
return ViewPaths.EVENEMENT_GESTION + ".xhtml" + ViewPaths.REDIRECT_SUFFIX;
}
public void annulerInscription(Evenement evenement) {
@@ -373,7 +374,7 @@ public class MembreDashboardBean implements Serializable {
}
public String payerCotisations() {
return "/pages/secure/cotisation/paiement.xhtml?faces-redirect=true";
return ViewPaths.COTISATION_PAIEMENT + ".xhtml" + ViewPaths.REDIRECT_SUFFIX;
}
public void actualiser() {

View File

@@ -1,5 +1,6 @@
package dev.lions.unionflow.client.view;
import dev.lions.unionflow.client.constants.ViewPaths;
import jakarta.enterprise.context.RequestScoped;
import jakarta.faces.context.FacesContext;
import jakarta.inject.Inject;
@@ -62,7 +63,7 @@ public class NavigationBean implements Serializable {
return redirectToLogin();
}
return "/pages/secure/profile?faces-redirect=true";
return ViewPaths.PROFILE + ViewPaths.REDIRECT_SUFFIX;
}
public String goToSettings() {
@@ -71,11 +72,11 @@ public class NavigationBean implements Serializable {
}
if (userSession.isSuperAdmin()) {
return "/pages/super-admin/configuration/systeme?faces-redirect=true";
return ViewPaths.SUPER_ADMIN_CONFIGURATION_SYSTEME + ViewPaths.REDIRECT_SUFFIX;
} else if (userSession.isAdmin()) {
return "/pages/admin/parametres?faces-redirect=true";
return ViewPaths.ADMIN_PARAMETRES + ViewPaths.REDIRECT_SUFFIX;
} else {
return "/pages/membre/parametres?faces-redirect=true";
return ViewPaths.MEMBRE_PARAMETRES + ViewPaths.REDIRECT_SUFFIX;
}
}
@@ -87,23 +88,21 @@ public class NavigationBean implements Serializable {
private String getDashboardUrlForUserType() {
if (userSession == null || userSession.getTypeCompte() == null) {
return "/pages/secure/dashboard.xhtml";
return ViewPaths.DASHBOARD + ".xhtml";
}
switch (userSession.getTypeCompte()) {
case "SUPER_ADMIN":
return "/pages/super-admin/dashboard.xhtml";
return ViewPaths.SUPER_ADMIN_DASHBOARD + ".xhtml";
case "ADMIN_ORGANISATION":
return "/pages/secure/dashboard.xhtml";
case "MODERATEUR":
return "/pages/secure/dashboard.xhtml";
return ViewPaths.DASHBOARD + ".xhtml";
case "MEMBRE_ACTIF":
return "/pages/secure/dashboard-membre.xhtml";
case "MEMBRE":
return "/pages/secure/dashboard-membre.xhtml";
return ViewPaths.DASHBOARD_MEMBRE + ".xhtml";
default:
LOGGER.warning("Type de compte non reconnu: " + userSession.getTypeCompte());
return "/pages/secure/dashboard.xhtml";
return ViewPaths.DASHBOARD + ".xhtml";
}
}

View File

@@ -1,6 +1,7 @@
package dev.lions.unionflow.client.view;
import dev.lions.unionflow.client.constants.StatutOrganisationConstants;
import dev.lions.unionflow.client.constants.ViewPaths;
import dev.lions.unionflow.server.api.dto.organisation.response.OrganisationResponse;
import dev.lions.unionflow.server.api.dto.organisation.response.OrganisationSummaryResponse;
import dev.lions.unionflow.server.api.dto.common.PagedResponse;
@@ -245,7 +246,7 @@ public class OrganisationsBean implements Serializable {
chargerOrganisations();
chargerStatistiques();
return "/pages/secure/organisation/liste?faces-redirect=true";
return ViewPaths.ORGANISATION_LISTE + ViewPaths.REDIRECT_SUFFIX;
} catch (RestClientExceptionMapper.ConflictException e) {
errorHandler.handleException(e, "lors de la création d'une organisation",

View File

@@ -0,0 +1,85 @@
package dev.lions.unionflow.client.view;
import dev.lions.unionflow.client.api.dto.PispiReadinessDto;
import dev.lions.unionflow.client.service.PispiReadinessRestClient;
import jakarta.annotation.PostConstruct;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.io.Serializable;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.jboss.logging.Logger;
/**
* Bean d'inspection PI-SPI Readiness (Sprint 8 ⇄ P1-NEW-15).
*
* @since 2026-04-25 (Sprint 8)
*/
@Named
@ViewScoped
public class PispiReadinessBean implements Serializable {
private static final long serialVersionUID = 1L;
private static final Logger LOG = Logger.getLogger(PispiReadinessBean.class);
@Inject @RestClient
PispiReadinessRestClient client;
private PispiReadinessDto readiness;
private String erreur;
@PostConstruct
public void init() {
rafraichir();
}
public void rafraichir() {
erreur = null;
try {
readiness = client.getReadiness();
LOG.infof("PI-SPI readiness chargé : %s (%d blocking, %d warnings)",
readiness.globalStatus(),
readiness.blockingIssues() == null ? 0 : readiness.blockingIssues().size(),
readiness.warnings() == null ? 0 : readiness.warnings().size());
} catch (jakarta.ws.rs.WebApplicationException wae) {
// Backend renvoie 503 quand BLOCKED — on récupère quand même le body
try {
readiness = wae.getResponse().readEntity(PispiReadinessDto.class);
LOG.infof("PI-SPI readiness BLOCKED — body décodé");
} catch (Exception parseEx) {
handleError("Décodage réponse readiness échoué", parseEx);
readiness = null;
}
} catch (Exception e) {
handleError("Chargement PI-SPI readiness échoué", e);
readiness = null;
}
}
public String getCouleurStatus() {
if (readiness == null) return "secondary";
return switch (readiness.globalStatus()) {
case "READY" -> "success";
case "DEGRADED" -> "warning";
case "BLOCKED" -> "danger";
default -> "secondary";
};
}
public String getCouleurCheck(String severity, String status) {
if ("PASS".equals(status)) return "success";
return "BLOCKING".equals(severity) ? "danger" : "warning";
}
private void handleError(String summary, Exception e) {
LOG.warnf("%s : %s", summary, e.getMessage());
erreur = summary + "" + e.getMessage();
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(FacesMessage.SEVERITY_ERROR, "Erreur", erreur));
}
public PispiReadinessDto getReadiness() { return readiness; }
public String getErreur() { return erreur; }
}

View File

@@ -0,0 +1,97 @@
package dev.lions.unionflow.client.view;
import dev.lions.unionflow.client.service.PublicKpiRestClient;
import dev.lions.unionflow.server.api.dto.compliance.response.KpiPublicSnapshot;
import jakarta.faces.context.FacesContext;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.io.Serializable;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.jboss.logging.Logger;
/**
* Bean public consultation KPI (Sprint 17) — pas d'authentification requise.
*
* <p>Lit le query param {@code token} dès l'init via {@code @ViewScoped} + viewAction,
* appelle l'endpoint public, expose le snapshot à la page.
*
* @since 2026-04-25 (Sprint 17)
*/
@Named
@ViewScoped
public class PublicKpiBean implements Serializable {
private static final long serialVersionUID = 1L;
private static final Logger LOG = Logger.getLogger(PublicKpiBean.class);
@Inject @RestClient PublicKpiRestClient client;
private String token;
private KpiPublicSnapshot snapshot;
private String erreur;
/** Appelé via {@code <f:viewAction action="#{publicKpiBean.charger}" />}. */
public void charger() {
erreur = null;
snapshot = null;
if (token == null || token.isBlank()) {
// Token vient du query param via <f:viewParam>
token = FacesContext.getCurrentInstance()
.getExternalContext()
.getRequestParameterMap()
.get("token");
}
if (token == null || token.isBlank()) {
erreur = "Aucun token fourni dans l'URL.";
return;
}
try {
snapshot = client.consulter(token);
LOG.infof("PublicKpi chargé — score=%d", snapshot == null ? -1 : snapshot.scoreGlobal());
} catch (jakarta.ws.rs.WebApplicationException wae) {
int status = wae.getResponse().getStatus();
if (status == 401) {
erreur = "Token invalide ou expiré.";
} else if (status == 404) {
erreur = "Organisation introuvable.";
} else {
erreur = "Erreur HTTP " + status;
}
LOG.warnf("PublicKpi HTTP %d : %s", status, wae.getMessage());
} catch (Exception e) {
erreur = "Erreur de chargement : " + e.getMessage();
LOG.errorf(e, "PublicKpi exception");
}
}
/** Couleur sémantique du score (success / warning / danger). */
public String getCouleurScore() {
if (snapshot == null) return "secondary";
int s = snapshot.scoreGlobal();
if (s >= 80) return "success";
if (s >= 60) return "warning";
return "danger";
}
public String getCouleurStatut(String statut) {
if (statut == null) return "secondary";
return switch (statut) {
case "OK" -> "success";
case "RETARD" -> "danger";
case "EN_ATTENTE" -> "warning";
case "OBLIGATOIRE" -> "info";
default -> "secondary";
};
}
// Getters / setters
public String getToken() { return token; }
public void setToken(String token) { this.token = token; }
public KpiPublicSnapshot getSnapshot() { return snapshot; }
public String getErreur() { return erreur; }
public boolean isCharge() { return snapshot != null; }
}

View File

@@ -0,0 +1,147 @@
package dev.lions.unionflow.client.view;
import dev.lions.unionflow.client.api.dto.RapportTrimestrielDto;
import dev.lions.unionflow.client.service.RapportTrimestrielRestClient;
import jakarta.annotation.PostConstruct;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.ExternalContext;
import jakarta.faces.context.FacesContext;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Serializable;
import java.time.Year;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.jboss.logging.Logger;
/**
* Bean liste/actions sur les rapports trimestriels Contrôleur Interne (Sprint 8 ⇄ P2-NEW-3).
*
* @since 2026-04-25 (Sprint 8)
*/
@Named
@ViewScoped
public class RapportsTrimestrielsBean implements Serializable {
private static final long serialVersionUID = 1L;
private static final Logger LOG = Logger.getLogger(RapportsTrimestrielsBean.class);
@Inject @RestClient
RapportTrimestrielRestClient client;
private List<RapportTrimestrielDto> rapports = Collections.emptyList();
private int annee = Year.now().getValue();
private int trimestreCible = 1;
private RapportTrimestrielDto selection;
private String erreur;
@PostConstruct
public void init() {
rafraichir();
}
public void rafraichir() {
erreur = null;
try {
rapports = client.lister(null, annee);
LOG.infof("Liste rapports trimestriels chargée pour année %d : %d entrée(s)",
annee, rapports.size());
} catch (Exception e) {
handleError("Chargement des rapports échoué", e);
rapports = Collections.emptyList();
}
}
public void genererRapport() {
erreur = null;
try {
var nouveau = client.generer(null, annee, trimestreCible);
addMessage(FacesMessage.SEVERITY_INFO, "Rapport généré",
"Trimestre " + nouveau.trimestre() + "/" + nouveau.annee()
+ " — score " + nouveau.scoreConformite());
rafraichir();
} catch (Exception e) {
handleError("Génération du rapport échouée", e);
}
}
public void signerSelection(UUID signataireId) {
if (selection == null) {
addMessage(FacesMessage.SEVERITY_WARN, "Aucune sélection",
"Sélectionnez un rapport DRAFT à signer");
return;
}
try {
client.signer(selection.id(), signataireId);
addMessage(FacesMessage.SEVERITY_INFO, "Signé",
"Rapport " + selection.annee() + "/T" + selection.trimestre() + " signé");
rafraichir();
} catch (Exception e) {
handleError("Signature échouée", e);
}
}
public void archiverSelection() {
if (selection == null) {
addMessage(FacesMessage.SEVERITY_WARN, "Aucune sélection",
"Sélectionnez un rapport SIGNE à archiver");
return;
}
try {
client.archiver(selection.id());
addMessage(FacesMessage.SEVERITY_INFO, "Archivé",
"Rapport " + selection.annee() + "/T" + selection.trimestre() + " archivé");
rafraichir();
} catch (Exception e) {
handleError("Archivage échoué", e);
}
}
public void telechargerPdf(RapportTrimestrielDto r) {
if (r == null) return;
try {
byte[] pdf = client.telechargerPdf(r.id());
String filename = String.format("rapport-trim-%d-T%d.pdf", r.annee(), r.trimestre());
FacesContext fc = FacesContext.getCurrentInstance();
ExternalContext ec = fc.getExternalContext();
ec.responseReset();
ec.setResponseContentType("application/pdf");
ec.setResponseHeader("Content-Disposition", "attachment; filename=\"" + filename + "\"");
ec.setResponseContentLength(pdf.length);
try (OutputStream os = ec.getResponseOutputStream()) {
os.write(pdf);
}
fc.responseComplete();
} catch (IOException e) {
handleError("Téléchargement PDF échoué", e);
} catch (Exception e) {
handleError("Téléchargement PDF échoué", e);
}
}
private void handleError(String summary, Exception e) {
LOG.warnf("%s : %s", summary, e.getMessage());
erreur = summary + "" + e.getMessage();
addMessage(FacesMessage.SEVERITY_ERROR, "Erreur", erreur);
}
private void addMessage(FacesMessage.Severity sev, String summary, String detail) {
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(sev, summary, detail));
}
public List<RapportTrimestrielDto> getRapports() { return rapports; }
public int getAnnee() { return annee; }
public void setAnnee(int annee) { this.annee = annee; }
public int getTrimestreCible() { return trimestreCible; }
public void setTrimestreCible(int trimestreCible) { this.trimestreCible = trimestreCible; }
public RapportTrimestrielDto getSelection() { return selection; }
public void setSelection(RapportTrimestrielDto selection) { this.selection = selection; }
public String getErreur() { return erreur; }
}

View File

@@ -0,0 +1,147 @@
package dev.lions.unionflow.client.view;
import dev.lions.unionflow.client.service.RoleDelegationRestClient;
import dev.lions.unionflow.server.api.dto.delegation.request.CreateRoleDelegationRequest;
import dev.lions.unionflow.server.api.dto.delegation.response.RoleDelegationResponse;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.jboss.logging.Logger;
/**
* Bean liste/création/révocation des délégations de rôle (Sprint 11 ⇄ backend Sprint 10).
*/
@Named
@ViewScoped
public class RoleDelegationBean implements Serializable {
private static final long serialVersionUID = 1L;
private static final Logger LOG = Logger.getLogger(RoleDelegationBean.class);
@Inject @RestClient RoleDelegationRestClient client;
private UUID organisationId;
private List<RoleDelegationResponse> delegations = Collections.emptyList();
private RoleDelegationResponse selection;
private String erreur;
// Formulaire création
private UUID delegantUserId;
private UUID delegataireUserId;
private String roleDelegue = "TRESORIER";
private LocalDateTime dateDebut = LocalDateTime.now().plusHours(1);
private LocalDateTime dateFin = LocalDateTime.now().plusDays(14);
private String motif;
private String rolesDelegataireCsv;
public void rechercher() {
erreur = null;
if (organisationId == null) return;
try {
delegations = client.listerParOrganisation(organisationId);
LOG.infof("Délégations chargées org=%s : %d", organisationId, delegations.size());
} catch (Exception e) {
handleError("Chargement délégations échoué", e);
delegations = Collections.emptyList();
}
}
public void creer() {
if (organisationId == null || delegantUserId == null || delegataireUserId == null) {
addMessage(FacesMessage.SEVERITY_WARN, "Champs requis",
"Organisation, déléguant et délégataire sont obligatoires");
return;
}
try {
CreateRoleDelegationRequest req = CreateRoleDelegationRequest.builder()
.organisationId(organisationId)
.delegantUserId(delegantUserId)
.delegataireUserId(delegataireUserId)
.roleDelegue(roleDelegue)
.dateDebut(dateDebut)
.dateFin(dateFin)
.motif(motif)
.build();
RoleDelegationResponse created = client.creer(req, rolesDelegataireCsv);
addMessage(FacesMessage.SEVERITY_INFO, "Délégation créée",
"Rôle " + created.roleDelegue() + " jusqu'au " + created.dateFin());
resetForm();
rechercher();
} catch (Exception e) {
handleError("Création délégation échouée", e);
}
}
public void revoquerSelection() {
if (selection == null) return;
try {
client.revoquer(selection.id());
addMessage(FacesMessage.SEVERITY_INFO, "Délégation révoquée",
"Rôle " + selection.roleDelegue());
rechercher();
} catch (Exception e) {
handleError("Révocation échouée", e);
}
}
public String getCouleurStatut(String statut) {
if (statut == null) return "secondary";
return switch (statut) {
case "ACTIVE" -> "success";
case "REVOQUEE" -> "danger";
case "EXPIREE" -> "warning";
default -> "secondary";
};
}
private void resetForm() {
delegantUserId = null;
delegataireUserId = null;
roleDelegue = "TRESORIER";
dateDebut = LocalDateTime.now().plusHours(1);
dateFin = LocalDateTime.now().plusDays(14);
motif = null;
rolesDelegataireCsv = null;
}
private void handleError(String summary, Exception e) {
LOG.warnf("%s : %s", summary, e.getMessage());
erreur = summary + "" + e.getMessage();
addMessage(FacesMessage.SEVERITY_ERROR, "Erreur", erreur);
}
private void addMessage(FacesMessage.Severity sev, String summary, String detail) {
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage(sev, summary, detail));
}
// Getters / setters
public UUID getOrganisationId() { return organisationId; }
public void setOrganisationId(UUID organisationId) { this.organisationId = organisationId; }
public List<RoleDelegationResponse> getDelegations() { return delegations; }
public RoleDelegationResponse getSelection() { return selection; }
public void setSelection(RoleDelegationResponse selection) { this.selection = selection; }
public String getErreur() { return erreur; }
public UUID getDelegantUserId() { return delegantUserId; }
public void setDelegantUserId(UUID id) { this.delegantUserId = id; }
public UUID getDelegataireUserId() { return delegataireUserId; }
public void setDelegataireUserId(UUID id) { this.delegataireUserId = id; }
public String getRoleDelegue() { return roleDelegue; }
public void setRoleDelegue(String roleDelegue) { this.roleDelegue = roleDelegue; }
public LocalDateTime getDateDebut() { return dateDebut; }
public void setDateDebut(LocalDateTime dateDebut) { this.dateDebut = dateDebut; }
public LocalDateTime getDateFin() { return dateFin; }
public void setDateFin(LocalDateTime dateFin) { this.dateFin = dateFin; }
public String getMotif() { return motif; }
public void setMotif(String motif) { this.motif = motif; }
public String getRolesDelegataireCsv() { return rolesDelegataireCsv; }
public void setRolesDelegataireCsv(String csv) { this.rolesDelegataireCsv = csv; }
}

View File

@@ -0,0 +1,139 @@
package dev.lions.unionflow.client.view;
import dev.lions.unionflow.client.constants.ViewPaths;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Named;
import java.io.Serializable;
/**
* Bean exposant {@link ViewPaths} aux pages JSF via expressions EL.
*
* <p>Usage XHTML : {@code outcome="#{paths.uboList}"}.
*
* <p>Le bean est {@code @ApplicationScoped} car les chemins sont immuables et identiques pour
* tous les utilisateurs — un seul instance suffit.
*
* @since 2026-04-25 (Sprint 12)
*/
@Named("paths")
@ApplicationScoped
public class ViewPathsBean implements Serializable {
private static final long serialVersionUID = 1L;
// ─── Index / Dashboard ───────────────────────────────────────────────────
public String getRoot() { return ViewPaths.ROOT; }
public String getIndex() { return ViewPaths.INDEX; }
public String getDashboard() { return ViewPaths.DASHBOARD; }
public String getDashboardMembre() { return ViewPaths.DASHBOARD_MEMBRE; }
// ─── Profil personnel ────────────────────────────────────────────────────
public String getProfile() { return ViewPaths.PROFILE; }
public String getPersonnelProfil() { return ViewPaths.PERSONNEL_PROFIL; }
public String getPersonnelParametres() { return ViewPaths.PERSONNEL_PARAMETRES; }
public String getPersonnelPreferences() { return ViewPaths.PERSONNEL_PREFERENCES; }
public String getPersonnelNotifications() { return ViewPaths.PERSONNEL_NOTIFICATIONS; }
// ─── Membres ─────────────────────────────────────────────────────────────
public String getMembreListe() { return ViewPaths.MEMBRE_LISTE; }
public String getMembreInscription() { return ViewPaths.MEMBRE_INSCRIPTION; }
public String getMembreValidation() { return ViewPaths.MEMBRE_VALIDATION; }
public String getMembreImport() { return ViewPaths.MEMBRE_IMPORT; }
public String getMembreExport() { return ViewPaths.MEMBRE_EXPORT; }
public String getMembreRecherche() { return ViewPaths.MEMBRE_RECHERCHE; }
public String getMembreCotisations() { return ViewPaths.MEMBRE_COTISATIONS; }
public String getMembreProfil() { return ViewPaths.MEMBRE_PROFIL; }
public String getMembrePaiementCotisations() { return ViewPaths.MEMBRE_PAIEMENT_COTISATIONS; }
// ─── Adhésions ───────────────────────────────────────────────────────────
public String getAdhesionListe() { return ViewPaths.ADHESION_LISTE; }
public String getAdhesionDemande() { return ViewPaths.ADHESION_DEMANDE; }
public String getAdhesionValidation() { return ViewPaths.ADHESION_VALIDATION; }
public String getAdhesionHistorique() { return ViewPaths.ADHESION_HISTORIQUE; }
public String getAdhesionRenouvellement() { return ViewPaths.ADHESION_RENOUVELLEMENT; }
// ─── Cotisations ─────────────────────────────────────────────────────────
public String getCotisationsGestionAdmin() { return ViewPaths.COTISATIONS_GESTION_ADMIN; }
public String getCotisationPaiement() { return ViewPaths.COTISATION_PAIEMENT; }
public String getCotisationHistorique() { return ViewPaths.COTISATION_HISTORIQUE; }
public String getCotisationRelances() { return ViewPaths.COTISATION_RELANCES; }
// ─── Finance ─────────────────────────────────────────────────────────────
public String getFinanceTresorerie() { return ViewPaths.FINANCE_TRESORERIE; }
public String getFinanceBudgets() { return ViewPaths.FINANCE_BUDGETS; }
public String getFinanceBilans() { return ViewPaths.FINANCE_BILANS; }
public String getFinanceApprobations() { return ViewPaths.FINANCE_APPROBATIONS; }
public String getComptabiliteGestion() { return ViewPaths.COMPTABILITE_GESTION; }
// ─── Épargne / Crédit ────────────────────────────────────────────────────
public String getEpargneComptes() { return ViewPaths.EPARGNE_COMPTES; }
public String getCreditDemandes() { return ViewPaths.CREDIT_DEMANDES; }
public String getCreditEvaluation() { return ViewPaths.CREDIT_EVALUATION; }
public String getCreditSuivi() { return ViewPaths.CREDIT_SUIVI; }
public String getCreditRemboursements() { return ViewPaths.CREDIT_REMBOURSEMENTS; }
public String getCreditStatistiques() { return ViewPaths.CREDIT_STATISTIQUES; }
// ─── Événements ──────────────────────────────────────────────────────────
public String getEvenementGestion() { return ViewPaths.EVENEMENT_GESTION; }
public String getEvenementCreation() { return ViewPaths.EVENEMENT_CREATION; }
public String getEvenementCalendrier() { return ViewPaths.EVENEMENT_CALENDRIER; }
public String getEvenementParticipants() { return ViewPaths.EVENEMENT_PARTICIPANTS; }
public String getEvenementPlanification() { return ViewPaths.EVENEMENT_PLANIFICATION; }
public String getEvenementLogistique() { return ViewPaths.EVENEMENT_LOGISTIQUE; }
public String getEvenementBilan() { return ViewPaths.EVENEMENT_BILAN; }
public String getEvenementBilanDetail() { return ViewPaths.EVENEMENT_BILAN_DETAIL; }
// ─── Aide / Support ──────────────────────────────────────────────────────
public String getAideDemande() { return ViewPaths.AIDE_DEMANDE; }
public String getAideRequests() { return ViewPaths.AIDE_REQUESTS; }
public String getAideApproved() { return ViewPaths.AIDE_APPROVED; }
public String getAideTraitement() { return ViewPaths.AIDE_TRAITEMENT; }
public String getAideHistorique() { return ViewPaths.AIDE_HISTORIQUE; }
public String getAideStatistiques() { return ViewPaths.AIDE_STATISTIQUES; }
public String getAideFaq() { return ViewPaths.AIDE_FAQ; }
public String getAideSupport() { return ViewPaths.AIDE_SUPPORT; }
public String getAideApropos() { return ViewPaths.AIDE_APROPOS; }
// ─── Communication / Documents ───────────────────────────────────────────
public String getCommunicationConversations() { return ViewPaths.COMMUNICATION_CONVERSATIONS; }
public String getCommunicationNotifications() { return ViewPaths.COMMUNICATION_NOTIFICATIONS; }
public String getDocumentsMesDocuments() { return ViewPaths.DOCUMENTS_MES_DOCUMENTS; }
// ─── Organisation ────────────────────────────────────────────────────────
public String getOrganisationListe() { return ViewPaths.ORGANISATION_LISTE; }
public String getOrganisationDetail() { return ViewPaths.ORGANISATION_DETAIL; }
public String getOrganisationNouvelle() { return ViewPaths.ORGANISATION_NOUVELLE; }
public String getOrganisationStatistiques() { return ViewPaths.ORGANISATION_STATISTIQUES; }
// ─── Conformité (Sprints 8, 11) ──────────────────────────────────────────
public String getConformiteDashboard() { return ViewPaths.CONFORMITE_DASHBOARD; }
public String getConformiteRapportsTrimestriels() { return ViewPaths.CONFORMITE_RAPPORTS_TRIMESTRIELS; }
public String getConformiteBeneficiairesEffectifs() { return ViewPaths.CONFORMITE_BENEFICIAIRES_EFFECTIFS; }
public String getConformiteAuditTrail() { return ViewPaths.CONFORMITE_AUDIT_TRAIL; }
public String getConformiteLiveFeed() { return ViewPaths.CONFORMITE_LIVE_FEED; }
// ─── Admin technique ─────────────────────────────────────────────────────
public String getAdminPispiReadiness() { return ViewPaths.ADMIN_PISPI_READINESS; }
public String getAdminRoleDelegations() { return ViewPaths.ADMIN_ROLE_DELEGATIONS; }
public String getAdminSauvegarde() { return ViewPaths.ADMIN_SAUVEGARDE; }
public String getAdminAuditJournal() { return ViewPaths.ADMIN_AUDIT_JOURNAL; }
public String getAdminLogsSysteme() { return ViewPaths.ADMIN_LOGS_SYSTEME; }
public String getAdminParametres() { return ViewPaths.ADMIN_PARAMETRES; }
public String getMembreParametres() { return ViewPaths.MEMBRE_PARAMETRES; }
// ─── Rapports ────────────────────────────────────────────────────────────
public String getRapportFinances() { return ViewPaths.RAPPORT_FINANCES; }
public String getRapportMembres() { return ViewPaths.RAPPORT_MEMBRES; }
public String getRapportActivites() { return ViewPaths.RAPPORT_ACTIVITES; }
public String getRapportTableauxBord() { return ViewPaths.RAPPORT_TABLEAUX_BORD; }
public String getRapportExport() { return ViewPaths.RAPPORT_EXPORT; }
// ─── Souscription ────────────────────────────────────────────────────────
public String getSouscriptionDashboard() { return ViewPaths.SOUSCRIPTION_DASHBOARD; }
// ─── Super-Admin ─────────────────────────────────────────────────────────
public String getSuperAdminDashboard() { return ViewPaths.SUPER_ADMIN_DASHBOARD; }
public String getSuperAdminRolesGestion() { return ViewPaths.SUPER_ADMIN_ROLES_GESTION; }
public String getSuperAdminTypesOrganisations() { return ViewPaths.SUPER_ADMIN_TYPES_ORGANISATIONS; }
public String getSuperAdminConfigurationSysteme() { return ViewPaths.SUPER_ADMIN_CONFIGURATION_SYSTEME; }
}

View File

@@ -63,11 +63,11 @@
<h:form>
<p:button value="Se reconnecter"
icon="pi pi-sign-in"
outcome="/"
outcome="#{paths.root}"
styleClass="ui-button-primary ui-button-lg" />
<p:button value="Page d'accueil"
icon="pi pi-home"
outcome="/index"
outcome="#{paths.index}"
styleClass="ui-button-secondary ui-button-outlined ui-button-lg" />
</h:form>
</div>

View File

@@ -32,7 +32,7 @@
<!-- Landing Topbar -->
<div class="landing-topbar">
<div class="landing-topbar-left">
<h:link id="logolink" outcome="/pages/secure/dashboard" styleClass="logo unionflow-brand unionflow-brand--landing">
<h:link id="logolink" outcome="#{paths.dashboard}" styleClass="logo unionflow-brand unionflow-brand--landing">
<h:graphicImage value="#{request.contextPath}/resources/freya-layout/images/unionflow-logo.png"
alt="Logo UnionFlow"
styleClass="unionflow-brand-icon" />

View File

@@ -12,7 +12,7 @@
<div class="card">
<h2>Administration - Audit</h2>
<p>Page d'administration en cours de développement...</p>
<p:button value="Retour" icon="pi pi-arrow-left" outcome="/pages/secure/dashboard"/>
<p:button value="Retour" icon="pi pi-arrow-left" outcome="#{paths.dashboard}"/>
</div>
</div>
</div>

View File

@@ -12,7 +12,7 @@
<div class="card">
<h2>Administration - Backup</h2>
<p>Page d'administration en cours de développement...</p>
<p:button value="Retour" icon="pi pi-arrow-left" outcome="/pages/secure/dashboard"/>
<p:button value="Retour" icon="pi pi-arrow-left" outcome="#{paths.dashboard}"/>
</div>
</div>
</div>

View File

@@ -0,0 +1,54 @@
<!DOCTYPE html>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/templates/main-template.xhtml">
<ui:define name="title">UnionFlow - Paramètres administrateur</ui:define>
<ui:define name="content">
<div class="grid">
<div class="col-12">
<div class="card">
<h2 class="text-900 font-medium text-3xl m-0">Paramètres administrateur</h2>
<p class="text-600 mt-1 mb-4">
Préférences personnelles de l'administrateur — interface, notifications, sécurité.
</p>
<div class="grid">
<div class="col-12 md:col-4">
<p:commandButton value="Profil personnel"
icon="pi pi-user"
action="#{paths.personnelProfil}"
styleClass="w-full p-button-outlined" />
</div>
<div class="col-12 md:col-4">
<p:commandButton value="Préférences"
icon="pi pi-cog"
action="#{paths.personnelPreferences}"
styleClass="w-full p-button-outlined" />
</div>
<div class="col-12 md:col-4">
<p:commandButton value="Notifications"
icon="pi pi-bell"
action="#{paths.personnelNotifications}"
styleClass="w-full p-button-outlined" />
</div>
</div>
<p:divider styleClass="my-4" />
<p:panel header="Page en construction" styleClass="surface-100">
<p class="text-700">
Cette page centralise les paramètres administrateur (configuration globale,
module d'audit, logs, sauvegarde). Les sous-sections sont accessibles via
les boutons ci-dessus en attendant la version complète.
</p>
</p:panel>
</div>
</div>
</div>
</ui:define>
</ui:composition>

View File

@@ -12,7 +12,7 @@
<div class="card">
<h2>Administration - Settings</h2>
<p>Page d'administration en cours de développement...</p>
<p:button value="Retour" icon="pi pi-arrow-left" outcome="/pages/secure/dashboard"/>
<p:button value="Retour" icon="pi pi-arrow-left" outcome="#{paths.dashboard}"/>
</div>
</div>
</div>

View File

@@ -12,7 +12,7 @@
<div class="card">
<h2>Administration - Users</h2>
<p>Page d'administration en cours de développement...</p>
<p:button value="Retour" icon="pi pi-arrow-left" outcome="/pages/secure/dashboard"/>
<p:button value="Retour" icon="pi pi-arrow-left" outcome="#{paths.dashboard}"/>
</div>
</div>
</div>

View File

@@ -0,0 +1,53 @@
<!DOCTYPE html>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/templates/main-template.xhtml">
<ui:define name="title">UnionFlow - Mes paramètres</ui:define>
<ui:define name="content">
<div class="grid">
<div class="col-12">
<div class="card">
<h2 class="text-900 font-medium text-3xl m-0">Mes paramètres</h2>
<p class="text-600 mt-1 mb-4">
Gérez votre profil, vos notifications et vos préférences d'affichage.
</p>
<div class="grid">
<div class="col-12 md:col-4">
<p:commandButton value="Mon profil"
icon="pi pi-user"
action="#{paths.personnelProfil}"
styleClass="w-full p-button-outlined" />
</div>
<div class="col-12 md:col-4">
<p:commandButton value="Mes préférences"
icon="pi pi-cog"
action="#{paths.personnelPreferences}"
styleClass="w-full p-button-outlined" />
</div>
<div class="col-12 md:col-4">
<p:commandButton value="Mes notifications"
icon="pi pi-bell"
action="#{paths.personnelNotifications}"
styleClass="w-full p-button-outlined" />
</div>
</div>
<p:divider styleClass="my-4" />
<p:panel header="Page en construction" styleClass="surface-100">
<p class="text-700">
Vue centralisée des paramètres membre. Sous-sections accessibles via
les boutons ci-dessus.
</p>
</p:panel>
</div>
</div>
</div>
</ui:define>
</ui:composition>

View File

@@ -0,0 +1,152 @@
<!DOCTYPE html>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/templates/public-template.xhtml">
<ui:define name="title">UnionFlow - KPI publics</ui:define>
<ui:define name="metadata">
<f:metadata>
<f:viewParam name="token" value="#{publicKpiBean.token}" />
<f:viewAction action="#{publicKpiBean.charger}" />
</f:metadata>
</ui:define>
<ui:define name="content">
<div class="grid">
<div class="col-12">
<div class="card">
<div class="text-center mb-4">
<i class="pi pi-shield text-primary" style="font-size: 3rem;" />
<h2 class="text-900 font-medium text-3xl mt-3 mb-2">
Indicateurs publics de conformité
</h2>
<p class="text-600 text-lg m-0">
Vue read-only destinée aux autorités externes (BCEAO, ARTCI, CENTIF).
</p>
</div>
<ui:fragment rendered="#{publicKpiBean.erreur != null}">
<p:messages closable="false" />
<div class="surface-100 border-round p-4 text-center">
<i class="pi pi-exclamation-triangle text-orange-500"
style="font-size: 2rem;" />
<h3 class="text-900 mt-2 mb-1">Accès impossible</h3>
<p class="text-700 m-0">#{publicKpiBean.erreur}</p>
</div>
</ui:fragment>
<ui:fragment rendered="#{publicKpiBean.charge}">
<!-- Score global -->
<div class="grid mb-4">
<div class="col-12 md:col-4">
<div class="surface-100 border-round p-4 text-center">
<div class="text-700 mb-2">Score conformité</div>
<div class="text-#{publicKpiBean.couleurScore eq 'success' ? 'green' : (publicKpiBean.couleurScore eq 'warning' ? 'orange' : 'red')}-600 font-bold"
style="font-size: 3.5rem; line-height: 1;">
#{publicKpiBean.snapshot.scoreGlobal}
</div>
<div class="text-600 mt-1">/ 100</div>
</div>
</div>
<div class="col-12 md:col-8">
<div class="surface-100 border-round p-4 h-full">
<div class="text-700 font-bold mb-2">Organisation</div>
<div class="text-900 text-2xl">#{publicKpiBean.snapshot.organisationNom}</div>
<div class="text-600 mt-3">
Référentiel comptable :
<span class="font-semibold">#{publicKpiBean.snapshot.referentielComptable}</span>
</div>
<div class="text-600 mt-2">
Snapshot généré le : <span class="font-mono">#{publicKpiBean.snapshot.dateGeneration}</span>
</div>
</div>
</div>
</div>
<!-- Indicateurs -->
<div class="grid">
<div class="col-12 md:col-6 lg:col-4">
<div class="surface-50 border-round p-3 h-full">
<div class="flex justify-content-between">
<span class="text-700">Compliance Officer</span>
<p:tag value="#{publicKpiBean.snapshot.complianceOfficerDesigne ? 'Désigné' : 'ABSENT'}"
severity="#{publicKpiBean.snapshot.complianceOfficerDesigne ? 'success' : 'danger'}" />
</div>
</div>
</div>
<div class="col-12 md:col-6 lg:col-4">
<div class="surface-50 border-round p-3 h-full">
<div class="flex justify-content-between">
<span class="text-700">AG annuelle</span>
<p:tag value="#{publicKpiBean.snapshot.agAnnuelleStatut}"
severity="#{publicKpiBean.getCouleurStatut(publicKpiBean.snapshot.agAnnuelleStatut)}" />
</div>
</div>
</div>
<div class="col-12 md:col-6 lg:col-4">
<div class="surface-50 border-round p-3 h-full">
<div class="flex justify-content-between">
<span class="text-700">Rapport AIRMS</span>
<p:tag value="#{publicKpiBean.snapshot.rapportAirmsStatut}"
severity="#{publicKpiBean.getCouleurStatut(publicKpiBean.snapshot.rapportAirmsStatut)}" />
</div>
</div>
</div>
<div class="col-12 md:col-6 lg:col-4">
<div class="surface-50 border-round p-3 h-full">
<div class="text-700">Dirigeants enrôlés CMU</div>
<div class="text-900 text-2xl mt-1">#{publicKpiBean.snapshot.dirigeantsAvecCmu}</div>
</div>
</div>
<div class="col-12 md:col-6 lg:col-4">
<div class="surface-50 border-round p-3 h-full">
<div class="text-700">Taux KYC à jour</div>
<div class="text-900 text-2xl mt-1">#{publicKpiBean.snapshot.tauxKycAJourPct} %</div>
</div>
</div>
<div class="col-12 md:col-6 lg:col-4">
<div class="surface-50 border-round p-3 h-full">
<div class="text-700">Taux formation LBC/FT</div>
<div class="text-900 text-2xl mt-1">#{publicKpiBean.snapshot.tauxFormationLbcFtPct} %</div>
</div>
</div>
<div class="col-12 md:col-6 lg:col-4">
<div class="surface-50 border-round p-3 h-full">
<div class="text-700">Couverture UBO</div>
<div class="text-900 text-2xl mt-1">#{publicKpiBean.snapshot.couvertureUboPct} %</div>
</div>
</div>
<div class="col-12 md:col-6 lg:col-4">
<div class="surface-50 border-round p-3 h-full">
<div class="flex justify-content-between">
<span class="text-700">Commissaire aux comptes</span>
<p:tag value="#{publicKpiBean.snapshot.commissaireAuxComptesStatut}"
severity="#{publicKpiBean.getCouleurStatut(publicKpiBean.snapshot.commissaireAuxComptesStatut)}" />
</div>
</div>
</div>
</div>
<p:divider styleClass="my-4" />
<div class="text-center text-500 text-sm">
<i class="pi pi-info-circle mr-1" />
Vue partagée temporairement via lien signé. Toute consultation est tracée
dans l'audit trail UnionFlow.
</div>
</ui:fragment>
</div>
</div>
</div>
</ui:define>
</ui:composition>

View File

@@ -0,0 +1,98 @@
<!DOCTYPE html>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/templates/main-template.xhtml">
<ui:define name="title">UnionFlow - PI-SPI Readiness</ui:define>
<ui:define name="content">
<div class="grid">
<div class="col-12">
<div class="card">
<div class="flex justify-content-between align-items-center mb-4">
<div>
<h2 class="text-900 font-medium text-3xl m-0">PI-SPI Readiness</h2>
<p class="text-600 mt-1 mb-0">
Vérification des pré-requis intégration PI-SPI BCEAO avant activation production.
</p>
</div>
<p:commandButton value="Rafraîchir"
icon="pi pi-refresh"
action="#{pispiReadinessBean.rafraichir}"
update=":form-readiness"
styleClass="p-button-outlined" />
</div>
<h:form id="form-readiness">
<p:messages closable="true" />
<ui:fragment rendered="#{pispiReadinessBean.readiness != null}">
<!-- Statut global -->
<div class="surface-100 border-round p-4 mb-4 text-center">
<div class="text-700 mb-2">Statut global</div>
<p:tag value="#{pispiReadinessBean.readiness.globalStatus}"
severity="#{pispiReadinessBean.couleurStatus}"
style="font-size: 1.5rem; padding: 0.5rem 1.5rem;" />
<div class="text-600 mt-3">
Base URL : <span class="font-mono">#{pispiReadinessBean.readiness.baseUrl}</span>
</div>
</div>
<!-- Blocages -->
<p:fieldset legend="Blocages critiques"
rendered="#{not empty pispiReadinessBean.readiness.blockingIssues}"
styleClass="mb-3 border-red-300">
<ul class="m-0">
<ui:repeat value="#{pispiReadinessBean.readiness.blockingIssues}" var="b">
<li class="text-red-600">#{b}</li>
</ui:repeat>
</ul>
</p:fieldset>
<!-- Warnings -->
<p:fieldset legend="Avertissements (non bloquants)"
rendered="#{not empty pispiReadinessBean.readiness.warnings}"
styleClass="mb-3 border-orange-300">
<ul class="m-0">
<ui:repeat value="#{pispiReadinessBean.readiness.warnings}" var="w">
<li class="text-orange-600">#{w}</li>
</ui:repeat>
</ul>
</p:fieldset>
<!-- Détail des checks -->
<p:dataTable value="#{pispiReadinessBean.readiness.checks}"
var="c"
emptyMessage="Aucun check disponible">
<p:column headerText="Vérification">
<h:outputText value="#{c.name}" styleClass="font-mono" />
</p:column>
<p:column headerText="Sévérité">
<p:tag value="#{c.severity}"
severity="#{c.severity eq 'BLOCKING' ? 'danger' : 'warning'}" />
</p:column>
<p:column headerText="Statut">
<p:tag value="#{c.status}"
severity="#{c.status eq 'PASS' ? 'success' : pispiReadinessBean.getCouleurCheck(c.severity, c.status)}"
icon="#{c.status eq 'PASS' ? 'pi pi-check' : 'pi pi-times'}" />
</p:column>
<p:column headerText="Message">
<h:outputText value="#{c.message}" />
</p:column>
</p:dataTable>
</ui:fragment>
<ui:fragment rendered="#{pispiReadinessBean.readiness == null}">
<div class="text-center text-600 p-5">
Impossible de récupérer le rapport readiness.
</div>
</ui:fragment>
</h:form>
</div>
</div>
</div>
</ui:define>
</ui:composition>

View File

@@ -0,0 +1,135 @@
<!DOCTYPE html>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/templates/main-template.xhtml">
<ui:define name="title">UnionFlow - Délégations de rôles</ui:define>
<ui:define name="content">
<div class="grid">
<div class="col-12">
<div class="card">
<h2 class="text-900 font-medium text-3xl m-0">Délégations temporaires de rôles</h2>
<p class="text-600 mt-1 mb-3">
Délégation d'un rôle pour absence prolongée. Vérification SoD à la création.
</p>
<h:form id="form-deleg">
<p:messages closable="true" />
<p:panel header="Recherche" styleClass="mb-3">
<div class="grid">
<div class="col-12 md:col-9">
<p:outputLabel for="org" value="Organisation (UUID)" />
<p:inputText id="org" value="#{roleDelegationBean.organisationId}"
styleClass="w-full" />
</div>
<div class="col-12 md:col-3 flex align-items-end">
<p:commandButton value="Rechercher"
icon="pi pi-search"
action="#{roleDelegationBean.rechercher}"
update="form-deleg"
styleClass="w-full" />
</div>
</div>
</p:panel>
<p:dataTable value="#{roleDelegationBean.delegations}"
var="d"
selection="#{roleDelegationBean.selection}"
rowKey="#{d.id}"
selectionMode="single"
emptyMessage="Aucune délégation">
<p:column headerText="Rôle délégué">
<h:outputText value="#{d.roleDelegue}" />
</p:column>
<p:column headerText="Déléguant">
<h:outputText value="#{d.delegantUserId}" />
</p:column>
<p:column headerText="Délégataire">
<h:outputText value="#{d.delegataireUserId}" />
</p:column>
<p:column headerText="Période">
<h:outputText value="du #{d.dateDebut} au #{d.dateFin}" />
</p:column>
<p:column headerText="Motif">
<h:outputText value="#{d.motif}" />
</p:column>
<p:column headerText="Statut">
<p:tag value="#{d.statut}"
severity="#{roleDelegationBean.getCouleurStatut(d.statut)}" />
</p:column>
<p:column headerText="Active">
<p:tag value="#{d.estActive ? 'OUI' : 'NON'}"
severity="#{d.estActive ? 'success' : 'secondary'}" />
</p:column>
</p:dataTable>
<div class="flex gap-2 mt-3">
<p:commandButton value="Révoquer (sélection)"
icon="pi pi-times"
action="#{roleDelegationBean.revoquerSelection}"
update="form-deleg"
styleClass="p-button-danger" />
</div>
<p:panel header="Nouvelle délégation" toggleable="true" collapsed="true" styleClass="mt-4">
<div class="grid">
<div class="col-12 md:col-6">
<p:outputLabel for="dgt" value="Déléguant (UUID utilisateur) *" />
<p:inputText id="dgt" value="#{roleDelegationBean.delegantUserId}"
styleClass="w-full" />
</div>
<div class="col-12 md:col-6">
<p:outputLabel for="dgtr" value="Délégataire (UUID utilisateur) *" />
<p:inputText id="dgtr" value="#{roleDelegationBean.delegataireUserId}"
styleClass="w-full" />
</div>
<div class="col-12 md:col-4">
<p:outputLabel for="role" value="Rôle délégué *" />
<p:selectOneMenu id="role" value="#{roleDelegationBean.roleDelegue}"
styleClass="w-full">
<f:selectItem itemValue="TRESORIER" itemLabel="TRESORIER" />
<f:selectItem itemValue="SECRETAIRE" itemLabel="SECRETAIRE" />
<f:selectItem itemValue="ADMIN_ORGANISATION" itemLabel="ADMIN_ORGANISATION" />
<f:selectItem itemValue="COMPLIANCE_OFFICER" itemLabel="COMPLIANCE_OFFICER" />
<f:selectItem itemValue="CONTROLEUR_INTERNE" itemLabel="CONTROLEUR_INTERNE" />
</p:selectOneMenu>
</div>
<div class="col-12 md:col-4">
<p:outputLabel for="rolesDeleg" value="Rôles existants délégataire (CSV)" />
<p:inputText id="rolesDeleg" value="#{roleDelegationBean.rolesDelegataireCsv}"
styleClass="w-full" placeholder="MEMBRE_ACTIF,MEMBRE_BUREAU" />
</div>
<div class="col-12 md:col-4">
<p:outputLabel for="motif" value="Motif" />
<p:inputText id="motif" value="#{roleDelegationBean.motif}"
styleClass="w-full" placeholder="Congé, mission..." />
</div>
<div class="col-12 md:col-6">
<p:outputLabel for="dd" value="Date début *" />
<p:datePicker id="dd" value="#{roleDelegationBean.dateDebut}"
showTime="true" styleClass="w-full" />
</div>
<div class="col-12 md:col-6">
<p:outputLabel for="df" value="Date fin *" />
<p:datePicker id="df" value="#{roleDelegationBean.dateFin}"
showTime="true" styleClass="w-full" />
</div>
<div class="col-12">
<p:commandButton value="Créer délégation"
icon="pi pi-plus"
action="#{roleDelegationBean.creer}"
update="form-deleg" />
</div>
</div>
</p:panel>
</h:form>
</div>
</div>
</div>
</ui:define>
</ui:composition>

View File

@@ -0,0 +1,112 @@
<!DOCTYPE html>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/templates/main-template.xhtml">
<ui:define name="title">UnionFlow - Audit Trail</ui:define>
<ui:define name="content">
<div class="grid">
<div class="col-12">
<div class="card">
<h2 class="text-900 font-medium text-3xl m-0">Audit Trail</h2>
<p class="text-600 mt-1 mb-3">
Historique enrichi des opérations sensibles + détection violations SoD.
</p>
<h:form id="form-audit">
<p:messages closable="true" />
<p:panel header="Filtres" styleClass="mb-3">
<div class="grid">
<div class="col-12 md:col-3">
<p:outputLabel for="mode" value="Mode" />
<p:selectOneMenu id="mode" value="#{auditTrailViewerBean.mode}"
styleClass="w-full">
<f:selectItem itemValue="ORG" itemLabel="Par organisation" />
<f:selectItem itemValue="USER" itemLabel="Par utilisateur" />
<f:selectItem itemValue="ENTITY" itemLabel="Historique entité" />
<f:selectItem itemValue="SOD_VIOLATIONS" itemLabel="Violations SoD" />
<f:selectItem itemValue="FINANCIAL" itemLabel="Opérations financières" />
</p:selectOneMenu>
</div>
<div class="col-12 md:col-3">
<p:outputLabel for="org" value="Organisation (UUID)" />
<p:inputText id="org" value="#{auditTrailViewerBean.orgId}" styleClass="w-full" />
</div>
<div class="col-12 md:col-3">
<p:outputLabel for="usr" value="Utilisateur (UUID)" />
<p:inputText id="usr" value="#{auditTrailViewerBean.userId}" styleClass="w-full" />
</div>
<div class="col-12 md:col-3 flex align-items-end">
<p:commandButton value="Rechercher"
icon="pi pi-search"
action="#{auditTrailViewerBean.rechercher}"
update="form-audit"
styleClass="w-full" />
</div>
</div>
</p:panel>
<p:dataTable value="#{auditTrailViewerBean.operations}"
var="o"
selection="#{auditTrailViewerBean.selection}"
rowKey="#{o.id}"
selectionMode="single"
emptyMessage="Aucune opération trouvée">
<p:column headerText="Date">
<h:outputText value="#{o.operationAt}" />
</p:column>
<p:column headerText="Utilisateur">
<h:outputText value="#{o.userEmail}" />
<h:outputText value=" (#{o.roleActif})" rendered="#{o.roleActif != null}"
styleClass="text-600 text-sm" />
</p:column>
<p:column headerText="Action">
<p:tag value="#{o.actionType}"
severity="#{auditTrailViewerBean.getCouleurAction(o.actionType)}" />
</p:column>
<p:column headerText="Entité">
<h:outputText value="#{o.entityType}" />
</p:column>
<p:column headerText="Description">
<h:outputText value="#{o.description}" />
</p:column>
<p:column headerText="SoD">
<p:tag value="#{o.sodCheckPassed ? 'OK' : 'VIOLATION'}"
severity="#{auditTrailViewerBean.getCouleurSod(o.sodCheckPassed)}"
rendered="#{o.sodCheckPassed != null}" />
<h:outputText value="—" rendered="#{o.sodCheckPassed == null}" />
</p:column>
</p:dataTable>
<p:panel header="Détail opération" rendered="#{auditTrailViewerBean.selection != null}"
styleClass="mt-3">
<div class="grid">
<div class="col-12">
<strong>SoD violations :</strong>
<h:outputText value="#{auditTrailViewerBean.selection.sodViolations}" />
</div>
<div class="col-12 md:col-6">
<strong>Payload avant :</strong>
<pre style="font-size: 0.85em; background:#f5f5f5; padding:8px;">#{auditTrailViewerBean.selection.payloadAvant}</pre>
</div>
<div class="col-12 md:col-6">
<strong>Payload après :</strong>
<pre style="font-size: 0.85em; background:#f5f5f5; padding:8px;">#{auditTrailViewerBean.selection.payloadApres}</pre>
</div>
<div class="col-12">
<strong>Métadonnées :</strong>
<pre style="font-size: 0.85em; background:#f5f5f5; padding:8px;">#{auditTrailViewerBean.selection.metadata}</pre>
</div>
</div>
</p:panel>
</h:form>
</div>
</div>
</div>
</ui:define>
</ui:composition>

View File

@@ -0,0 +1,147 @@
<!DOCTYPE html>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/templates/main-template.xhtml">
<ui:define name="title">UnionFlow - Bénéficiaires Effectifs (UBO)</ui:define>
<ui:define name="content">
<div class="grid">
<div class="col-12">
<div class="card">
<h2 class="text-900 font-medium text-3xl m-0">Bénéficiaires Effectifs (UBO)</h2>
<p class="text-600 mt-1 mb-3">
Instr. BCEAO 003-03-2025 — identification des personnes physiques contrôlant l'organisation.
</p>
<h:form id="form-ubo">
<p:messages closable="true" />
<p:panel header="Recherche" styleClass="mb-3">
<div class="grid">
<div class="col-12 md:col-4">
<p:outputLabel for="kyc" value="KycDossier ID (UUID)" />
<p:inputText id="kyc" value="#{beneficiaireEffectifBean.kycDossierId}"
styleClass="w-full" />
</div>
<div class="col-12 md:col-4">
<p:outputLabel for="org" value="Organisation cible (UUID)" />
<p:inputText id="org" value="#{beneficiaireEffectifBean.organisationCibleId}"
styleClass="w-full" />
</div>
<div class="col-12 md:col-2 flex align-items-end">
<p:selectBooleanCheckbox value="#{beneficiaireEffectifBean.filtrePep}"
itemLabel="PEP uniquement" />
</div>
<div class="col-12 md:col-2 flex align-items-end">
<p:commandButton value="Rechercher"
icon="pi pi-search"
action="#{beneficiaireEffectifBean.rechercher}"
update="form-ubo"
styleClass="w-full" />
</div>
</div>
</p:panel>
<p:dataTable value="#{beneficiaireEffectifBean.ubos}"
var="u"
selection="#{beneficiaireEffectifBean.selection}"
rowKey="#{u.id}"
selectionMode="single"
emptyMessage="Aucun UBO trouvé">
<p:column headerText="Nom complet">
#{u.prenoms} #{u.nom}
</p:column>
<p:column headerText="Nationalité">
<h:outputText value="#{u.nationalite}" />
</p:column>
<p:column headerText="% Capital">
<h:outputText value="#{u.pourcentageCapital}" />
</p:column>
<p:column headerText="Nature contrôle">
<h:outputText value="#{u.natureControle}" />
</p:column>
<p:column headerText="PEP">
<p:tag value="PEP" severity="warning" rendered="#{u.estPep}" />
<h:outputText value="—" rendered="#{not u.estPep}" />
</p:column>
<p:column headerText="Sanctions">
<p:tag value="LISTÉ" severity="danger" rendered="#{u.presenceListesSanctions}" />
<h:outputText value="—" rendered="#{not u.presenceListesSanctions}" />
</p:column>
<p:column headerText="Statut">
<p:tag value="#{u.actif ? 'ACTIF' : 'INACTIF'}"
severity="#{u.actif ? 'success' : 'secondary'}" />
</p:column>
</p:dataTable>
<div class="flex gap-2 mt-3">
<p:commandButton value="Marquer PEP"
icon="pi pi-flag"
action="#{beneficiaireEffectifBean.marquerPepSelection}"
update="form-ubo"
styleClass="p-button-warning" />
<p:commandButton value="Désactiver"
icon="pi pi-times"
action="#{beneficiaireEffectifBean.desactiverSelection}"
update="form-ubo"
styleClass="p-button-secondary" />
</div>
<p:panel header="Nouveau UBO" toggleable="true" collapsed="true" styleClass="mt-4">
<div class="grid">
<div class="col-12 md:col-4">
<p:outputLabel for="nom" value="Nom *" />
<p:inputText id="nom" value="#{beneficiaireEffectifBean.nom}" styleClass="w-full" />
</div>
<div class="col-12 md:col-4">
<p:outputLabel for="prenoms" value="Prénoms *" />
<p:inputText id="prenoms" value="#{beneficiaireEffectifBean.prenoms}" styleClass="w-full" />
</div>
<div class="col-12 md:col-4">
<p:outputLabel for="nat" value="Nationalité (ISO-3) *" />
<p:inputText id="nat" value="#{beneficiaireEffectifBean.nationalite}"
maxlength="3" styleClass="w-full" />
</div>
<div class="col-12 md:col-4">
<p:outputLabel for="pays" value="Pays résidence (ISO-3)" />
<p:inputText id="pays" value="#{beneficiaireEffectifBean.paysResidence}"
maxlength="3" styleClass="w-full" />
</div>
<div class="col-12 md:col-4">
<p:outputLabel for="pct" value="% Capital (0-100)" />
<p:inputText id="pct" value="#{beneficiaireEffectifBean.pourcentageCapital}"
styleClass="w-full" />
</div>
<div class="col-12 md:col-4">
<p:outputLabel for="nat-ctl" value="Nature contrôle *" />
<p:selectOneMenu id="nat-ctl" value="#{beneficiaireEffectifBean.natureControle}"
styleClass="w-full">
<f:selectItem itemValue="DETENTION_CAPITAL" itemLabel="Détention capital" />
<f:selectItem itemValue="DROITS_VOTE" itemLabel="Droits de vote" />
<f:selectItem itemValue="CONTROLE_DE_FAIT" itemLabel="Contrôle de fait" />
<f:selectItem itemValue="BENEFICIAIRE_ULTIME" itemLabel="Bénéficiaire ultime" />
<f:selectItem itemValue="MANDAT_REPRESENTATION" itemLabel="Mandat" />
</p:selectOneMenu>
</div>
<div class="col-12">
<p:selectBooleanCheckbox value="#{beneficiaireEffectifBean.estPep}"
itemLabel="Personne Politiquement Exposée (PEP)" />
</div>
<div class="col-12">
<p:commandButton value="Créer UBO"
icon="pi pi-plus"
action="#{beneficiaireEffectifBean.creer}"
update="form-ubo" />
</div>
</div>
</p:panel>
</h:form>
</div>
</div>
</div>
</ui:define>
</ui:composition>

View File

@@ -0,0 +1,164 @@
<!DOCTYPE html>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/templates/main-template.xhtml">
<ui:define name="title">UnionFlow - Tableau de bord conformité</ui:define>
<ui:define name="content">
<div class="grid">
<div class="col-12">
<div class="card">
<div class="flex justify-content-between align-items-center mb-4">
<div>
<h2 class="text-900 font-medium text-3xl m-0">Tableau de bord conformité</h2>
<p class="text-600 mt-1 mb-0">
Indicateurs BCEAO Instr. 001-03-2025 / ARTCI / OHADA / AIRMS
</p>
</div>
<p:commandButton value="Rafraîchir"
icon="pi pi-refresh"
action="#{conformiteDashboardBean.rafraichir}"
update=":form-conformite"
styleClass="p-button-outlined" />
</div>
<h:form id="form-conformite">
<p:messages closable="true" />
<ui:fragment rendered="#{conformiteDashboardBean.snapshot != null}">
<!-- Score global -->
<div class="grid mb-4">
<div class="col-12 md:col-4">
<div class="surface-100 border-round p-4 text-center">
<div class="text-700 mb-2">Score conformité global</div>
<div class="text-#{conformiteDashboardBean.couleurScore eq 'success' ? 'green' : (conformiteDashboardBean.couleurScore eq 'warning' ? 'orange' : 'red')}-600 font-bold"
style="font-size: 3.5rem; line-height: 1;">
#{conformiteDashboardBean.snapshot.scoreGlobal}
</div>
<div class="text-600 mt-1">/ 100</div>
</div>
</div>
<div class="col-12 md:col-8">
<div class="surface-100 border-round p-4 h-full">
<div class="text-700 mb-2 font-bold">Organisation</div>
<div class="text-900 text-xl">#{conformiteDashboardBean.snapshot.organisationNom}</div>
<div class="text-600 mt-2">
Référentiel comptable : <span class="font-semibold">#{conformiteDashboardBean.snapshot.referentielComptable}</span>
</div>
</div>
</div>
</div>
<!-- Indicateurs détaillés -->
<div class="grid">
<div class="col-12 md:col-6 lg:col-4">
<div class="surface-50 border-round p-3 h-full">
<div class="flex justify-content-between">
<span class="text-700">Compliance Officer</span>
<p:tag value="#{conformiteDashboardBean.snapshot.complianceOfficerDesigne ? 'Désigné' : 'ABSENT'}"
severity="#{conformiteDashboardBean.snapshot.complianceOfficerDesigne ? 'success' : 'danger'}" />
</div>
</div>
</div>
<div class="col-12 md:col-6 lg:col-4">
<div class="surface-50 border-round p-3 h-full">
<div class="flex justify-content-between">
<span class="text-700">AG annuelle</span>
<p:tag value="#{conformiteDashboardBean.snapshot.agAnnuelle.statut}"
severity="#{conformiteDashboardBean.snapshot.agAnnuelle.statut eq 'OK' ? 'success' : (conformiteDashboardBean.snapshot.agAnnuelle.statut eq 'RETARD' ? 'danger' : 'warning')}" />
</div>
<p class="text-600 text-sm mt-2 m-0">#{conformiteDashboardBean.snapshot.agAnnuelle.message}</p>
</div>
</div>
<div class="col-12 md:col-6 lg:col-4">
<div class="surface-50 border-round p-3 h-full">
<div class="flex justify-content-between">
<span class="text-700">Rapport AIRMS</span>
<p:tag value="#{conformiteDashboardBean.snapshot.rapportAirms.statut}"
severity="#{conformiteDashboardBean.snapshot.rapportAirms.statut eq 'OK' ? 'success' : 'warning'}" />
</div>
</div>
</div>
<div class="col-12 md:col-6 lg:col-4">
<div class="surface-50 border-round p-3 h-full">
<div class="text-700">Dirigeants enrôlés CMU</div>
<div class="text-900 text-2xl mt-1">#{conformiteDashboardBean.snapshot.dirigeantsAvecCmu}</div>
</div>
</div>
<div class="col-12 md:col-6 lg:col-4">
<div class="surface-50 border-round p-3 h-full">
<div class="text-700">Taux KYC à jour</div>
<div class="text-900 text-2xl mt-1">#{conformiteDashboardBean.snapshot.tauxKycAJourPct} %</div>
</div>
</div>
<div class="col-12 md:col-6 lg:col-4">
<div class="surface-50 border-round p-3 h-full">
<div class="text-700">Taux formation LBC/FT</div>
<div class="text-900 text-2xl mt-1">#{conformiteDashboardBean.snapshot.tauxFormationLbcFtPct} %</div>
</div>
</div>
<div class="col-12 md:col-6 lg:col-4">
<div class="surface-50 border-round p-3 h-full">
<div class="text-700">Couverture UBO</div>
<div class="text-900 text-2xl mt-1">#{conformiteDashboardBean.snapshot.couvertureUboPct} %</div>
</div>
</div>
<div class="col-12 md:col-6 lg:col-4">
<div class="surface-50 border-round p-3 h-full">
<div class="text-700">Commissaire aux comptes</div>
<p:tag value="#{conformiteDashboardBean.snapshot.commissaireAuxComptes.statut}"
styleClass="mt-1" />
</div>
</div>
<div class="col-12 md:col-6 lg:col-4">
<div class="surface-50 border-round p-3 h-full">
<div class="text-700">FOMUS-CI</div>
<p:tag value="#{conformiteDashboardBean.snapshot.fomusCi.statut}"
styleClass="mt-1" />
<p class="text-600 text-sm mt-2 m-0">#{conformiteDashboardBean.snapshot.fomusCi.message}</p>
</div>
</div>
</div>
<p:fieldset legend="Alertes critiques"
rendered="#{conformiteDashboardBean.hasAlertes}"
styleClass="mt-4 surface-100">
<ul class="m-0">
<li class="text-red-600" jsf:rendered="#{not conformiteDashboardBean.snapshot.complianceOfficerDesigne}">
Compliance Officer non désigné — Instruction BCEAO 001-03-2025 obligatoire
</li>
<li class="text-red-600" jsf:rendered="#{conformiteDashboardBean.snapshot.agAnnuelle.statut eq 'RETARD'}">
AG annuelle en retard — échéance 30 juin
</li>
<li class="text-orange-600" jsf:rendered="#{conformiteDashboardBean.snapshot.scoreGlobal lt 60}">
Score global &lt; 60 % — risque inspection BCEAO/ARTCI
</li>
</ul>
</p:fieldset>
</ui:fragment>
<ui:fragment rendered="#{conformiteDashboardBean.snapshot == null}">
<p:messages />
<div class="text-center text-600 p-5">
Aucune donnée de conformité disponible.
</div>
</ui:fragment>
</h:form>
</div>
</div>
</div>
</ui:define>
</ui:composition>

View File

@@ -0,0 +1,117 @@
<!DOCTYPE html>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/templates/main-template.xhtml">
<ui:define name="title">UnionFlow - Live Activity Feed</ui:define>
<ui:define name="content">
<div class="grid">
<div class="col-12">
<div class="card">
<div class="flex justify-content-between align-items-center mb-3">
<div>
<h2 class="text-900 font-medium text-3xl m-0">
<i class="pi pi-bolt mr-2 text-yellow-500" />
Live Activity Feed
</h2>
<p class="text-600 mt-1 mb-0">
Transparency opérationnelle — les opérations sensibles défilent en temps réel.
Refresh auto toutes les 10 secondes.
</p>
</div>
<p:tag value="Refresh ##{liveFeedBean.compteur}" severity="info"
icon="pi pi-refresh" />
</div>
<h:form id="form-livefeed">
<p:messages closable="true" />
<p:panel header="Scope" styleClass="mb-3">
<div class="grid">
<div class="col-12 md:col-3">
<p:outputLabel for="scope" value="Périmètre" />
<p:selectOneMenu id="scope" value="#{liveFeedBean.scope}"
styleClass="w-full">
<f:selectItem itemValue="SELF" itemLabel="Mes opérations" />
<f:selectItem itemValue="ORG" itemLabel="Mon organisation" />
<f:selectItem itemValue="ALL" itemLabel="Toutes (compliance/contrôleur)" />
<p:ajax event="change" listener="#{liveFeedBean.rafraichir}"
update="form-livefeed" />
</p:selectOneMenu>
</div>
<div class="col-12 md:col-4" jsf:rendered="#{liveFeedBean.scope eq 'ORG'}">
<p:outputLabel for="orgId" value="Organisation (UUID)" />
<p:inputText id="orgId" value="#{liveFeedBean.orgId}"
styleClass="w-full" />
</div>
<div class="col-12 md:col-4" jsf:rendered="#{liveFeedBean.scope eq 'SELF'}">
<p:outputLabel for="userId" value="Utilisateur (UUID)" />
<p:inputText id="userId" value="#{liveFeedBean.userId}"
styleClass="w-full" />
</div>
<div class="col-12 md:col-2">
<p:outputLabel for="lim" value="Limite (1-500)" />
<p:inputNumber id="lim" value="#{liveFeedBean.limit}"
minValue="1" maxValue="500"
decimalPlaces="0" />
</div>
<div class="col-12 md:col-3 flex align-items-end">
<p:commandButton value="Rafraîchir"
icon="pi pi-refresh"
action="#{liveFeedBean.rafraichir}"
update="form-livefeed"
styleClass="w-full" />
</div>
</div>
</p:panel>
<!-- Auto-refresh toutes les 10 secondes (10000 ms) -->
<p:poll interval="10" listener="#{liveFeedBean.rafraichir}"
update="form-livefeed" autoStart="true" />
<p:dataTable value="#{liveFeedBean.operations}" var="o"
emptyMessage="Aucune opération récente"
rowIndexVar="idx">
<p:column headerText="#" style="width: 60px;">
<h:outputText value="#{idx + 1}" />
</p:column>
<p:column headerText="Quand">
<h:outputText value="#{liveFeedBean.tempsRelatif(o.operationAt)}"
styleClass="text-700 font-semibold" />
<br/>
<small class="text-500">#{o.operationAt}</small>
</p:column>
<p:column headerText="Acteur">
<h:outputText value="#{o.userEmail}" />
<br/>
<small class="text-500" jsf:rendered="#{o.roleActif != null}">
#{o.roleActif}
</small>
</p:column>
<p:column headerText="Action">
<p:tag value="#{o.actionType}"
severity="#{liveFeedBean.getCouleurAction(o.actionType)}" />
</p:column>
<p:column headerText="Entité">
<h:outputText value="#{o.entityType}" />
</p:column>
<p:column headerText="Description">
<h:outputText value="#{o.description}" />
</p:column>
<p:column headerText="SoD">
<p:tag value="#{o.sodCheckPassed ? 'OK' : 'VIOLATION'}"
severity="#{liveFeedBean.getCouleurSod(o.sodCheckPassed)}"
rendered="#{o.sodCheckPassed != null}" />
<h:outputText value="—" rendered="#{o.sodCheckPassed == null}" />
</p:column>
</p:dataTable>
</h:form>
</div>
</div>
</div>
</ui:define>
</ui:composition>

View File

@@ -0,0 +1,125 @@
<!DOCTYPE html>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/templates/main-template.xhtml">
<ui:define name="title">UnionFlow - Rapports trimestriels Contrôleur Interne</ui:define>
<ui:define name="content">
<div class="grid">
<div class="col-12">
<div class="card">
<div class="flex justify-content-between align-items-center mb-4">
<div>
<h2 class="text-900 font-medium text-3xl m-0">Rapports trimestriels — Contrôleur Interne</h2>
<p class="text-600 mt-1 mb-0">
Source AG, inspections BCEAO/ARTCI. Cycle DRAFT → SIGNE → ARCHIVE.
</p>
</div>
</div>
<h:form id="form-rapports">
<p:messages closable="true" />
<!-- Filtres + génération -->
<p:panel header="Génération rapport" styleClass="mb-3">
<div class="grid">
<div class="col-12 md:col-3">
<p:outputLabel for="annee" value="Année" />
<p:inputNumber id="annee"
value="#{rapportsTrimestrielsBean.annee}"
minValue="2024" maxValue="2099"
thousandSeparator=""
styleClass="w-full" />
</div>
<div class="col-12 md:col-3">
<p:outputLabel for="trim" value="Trimestre" />
<p:selectOneMenu id="trim"
value="#{rapportsTrimestrielsBean.trimestreCible}"
styleClass="w-full">
<f:selectItem itemLabel="T1 (Jan-Mar)" itemValue="1" />
<f:selectItem itemLabel="T2 (Avr-Jun)" itemValue="2" />
<f:selectItem itemLabel="T3 (Jul-Sep)" itemValue="3" />
<f:selectItem itemLabel="T4 (Oct-Déc)" itemValue="4" />
</p:selectOneMenu>
</div>
<div class="col-12 md:col-3 flex align-items-end">
<p:commandButton value="Générer"
icon="pi pi-plus"
action="#{rapportsTrimestrielsBean.genererRapport}"
update="form-rapports"
styleClass="w-full" />
</div>
<div class="col-12 md:col-3 flex align-items-end">
<p:commandButton value="Rafraîchir"
icon="pi pi-refresh"
action="#{rapportsTrimestrielsBean.rafraichir}"
update="form-rapports"
styleClass="p-button-outlined w-full" />
</div>
</div>
</p:panel>
<!-- Tableau -->
<p:dataTable id="tbl-rapports"
value="#{rapportsTrimestrielsBean.rapports}"
var="r"
selection="#{rapportsTrimestrielsBean.selection}"
rowKey="#{r.id}"
selectionMode="single"
emptyMessage="Aucun rapport pour cette année"
styleClass="mb-3">
<p:column headerText="Trimestre">
<h:outputText value="T#{r.trimestre} / #{r.annee}" />
</p:column>
<p:column headerText="Date génération">
<h:outputText value="#{r.dateGeneration}" />
</p:column>
<p:column headerText="Score">
<p:tag value="#{r.scoreConformite}/100"
severity="#{r.scoreConformite ge 80 ? 'success' : (r.scoreConformite ge 60 ? 'warning' : 'danger')}" />
</p:column>
<p:column headerText="Statut">
<p:tag value="#{r.statut}"
severity="#{r.statut eq 'ARCHIVE' ? 'success' : (r.statut eq 'SIGNE' ? 'info' : 'warning')}" />
</p:column>
<p:column headerText="Hash signature">
<h:outputText value="#{r.hashSha256.substring(0, 16)}..." rendered="#{r.hashSha256 != null}" />
<h:outputText value="—" rendered="#{r.hashSha256 == null}" />
</p:column>
<p:column headerText="Actions" style="width: 120px;">
<p:commandButton icon="pi pi-download"
title="Télécharger PDF"
action="#{rapportsTrimestrielsBean.telechargerPdf(r)}"
ajax="false"
styleClass="p-button-text" />
</p:column>
</p:dataTable>
<div class="flex gap-2">
<p:commandButton value="Signer (sélection)"
icon="pi pi-check"
action="#{rapportsTrimestrielsBean.signerSelection(userSession.currentUser != null ? userSession.currentUser.id : null)}"
update="form-rapports"
styleClass="p-button-success" />
<p:commandButton value="Archiver (sélection)"
icon="pi pi-lock"
action="#{rapportsTrimestrielsBean.archiverSelection}"
update="form-rapports"
styleClass="p-button-secondary" />
</div>
</h:form>
</div>
</div>
</div>
</ui:define>
</ui:composition>

View File

@@ -0,0 +1,37 @@
<!DOCTYPE html>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:p="http://primefaces.org/ui"
template="/templates/main-template.xhtml">
<ui:define name="title">UnionFlow - Bilan détaillé événement</ui:define>
<ui:define name="content">
<div class="grid">
<div class="col-12">
<div class="card">
<div class="flex justify-content-between align-items-center mb-3">
<h2 class="text-900 font-medium text-3xl m-0">Bilan détaillé</h2>
<p:button value="Retour aux bilans" icon="pi pi-arrow-left"
outcome="#{paths.evenementBilan}"
styleClass="p-button-outlined" />
</div>
<p:messages />
<p:panel header="Page en construction" styleClass="surface-100">
<p class="text-700">
La vue détaillée du bilan d'un événement (statistiques participants,
recettes/dépenses, indicateurs satisfaction) est en cours de développement.
</p>
<p class="text-600 mt-2">
En attendant, consulte la liste des bilans pour la vue synthétique.
</p>
</p:panel>
</div>
</div>
</div>
</ui:define>
</ui:composition>

View File

@@ -170,7 +170,7 @@
<p:commandButton value="Voir bilan"
icon="pi pi-chart-bar"
styleClass="ui-button-rounded ui-button-text ui-button-secondary"
outcome="/pages/secure/evenement/bilan-detail" />
outcome="#{paths.evenementBilanDetail}" />
</p:column>
</p:dataTable>
</h:form>

View File

@@ -168,7 +168,7 @@
<div class="flex gap-2">
<p:commandButton value="Voir détails"
icon="pi pi-eye"
outcome="/pages/secure/evenement/gestion.xhtml"
outcome="#{paths.evenementGestion}"
styleClass="p-button-outlined" />
<p:commandButton value="Fermer"
icon="pi pi-times"

View File

@@ -616,7 +616,7 @@
</div>
<div style="font-weight:700;color:var(--text-color);">
<h:link value="#{organisationDetailBean.organisation.organisationParenteNom}"
outcome="/pages/secure/organisation/detail"
outcome="#{paths.organisationDetail}"
styleClass="text-cyan-700 font-bold no-underline hover:underline">
<f:param name="id" value="#{organisationDetailBean.organisation.organisationParenteId}" />
</h:link>

View File

@@ -38,7 +38,7 @@
<p:button value="Nouvelle organisation"
icon="pi pi-plus"
styleClass="ui-button-success"
outcome="/pages/secure/organisation/nouvelle" />
outcome="#{paths.organisationNouvelle}" />
</div>
</div>
</div>

View File

@@ -17,7 +17,7 @@
<p:button value="Retour au tableau de bord"
icon="pi pi-arrow-left"
outcome="/pages/secure/dashboard"/>
outcome="#{paths.dashboard}"/>
</div>
</div>
</div>

View File

@@ -28,37 +28,37 @@
<!-- ════════════════════════════════════════════════════════ -->
<!-- TABLEAU DE BORD -->
<!-- ════════════════════════════════════════════════════════ -->
<p:menuitem id="m_dashboard" value="Tableau de Bord" icon="pi pi-home" outcome="/pages/secure/dashboard" />
<p:menuitem id="m_dashboard" value="Tableau de Bord" icon="pi pi-home" outcome="#{paths.dashboard}" />
<!-- ════════════════════════════════════════════════════════ -->
<!-- SUPER ADMINISTRATION (SUPER_ADMIN uniquement) -->
<!-- ════════════════════════════════════════════════════════ -->
<p:submenu id="m_super_admin" label="Super Administration" icon="pi pi-shield" rendered="#{menuBean.superAdminMenuVisible}">
<p:menuitem id="m_super_dashboard" value="Dashboard Super-Admin" icon="pi pi-chart-bar" outcome="/pages/super-admin/dashboard" />
<p:menuitem id="m_roles" value="Rôles Applicatifs" icon="pi pi-key" outcome="/pages/super-admin/roles/gestion" />
<p:menuitem id="m_audit" value="Journal d'Audit" icon="pi pi-file-o" outcome="/pages/admin/audit/journal" />
<p:menuitem id="m_logs_systeme" value="Logs Système" icon="pi pi-list" outcome="/pages/admin/logs/systeme" />
<p:menuitem id="m_sauvegardes" value="Sauvegardes" icon="pi pi-save" outcome="/pages/secure/admin/sauvegarde" />
<p:menuitem id="m_config_systeme" value="Configuration Système" icon="pi pi-cog" outcome="/pages/super-admin/configuration/systeme" />
<p:menuitem id="m_super_dashboard" value="Dashboard Super-Admin" icon="pi pi-chart-bar" outcome="#{paths.superAdminDashboard}" />
<p:menuitem id="m_roles" value="Rôles Applicatifs" icon="pi pi-key" outcome="#{paths.superAdminRolesGestion}" />
<p:menuitem id="m_audit" value="Journal d'Audit" icon="pi pi-file-o" outcome="#{paths.adminAuditJournal}" />
<p:menuitem id="m_logs_systeme" value="Logs Système" icon="pi pi-list" outcome="#{paths.adminLogsSysteme}" />
<p:menuitem id="m_sauvegardes" value="Sauvegardes" icon="pi pi-save" outcome="#{paths.adminSauvegarde}" />
<p:menuitem id="m_config_systeme" value="Configuration Système" icon="pi pi-cog" outcome="#{paths.superAdminConfigurationSysteme}" />
</p:submenu>
<!-- ════════════════════════════════════════════════════════ -->
<!-- ORGANISATIONS -->
<!-- ════════════════════════════════════════════════════════ -->
<p:submenu id="m_organisations" label="Organisations" icon="pi pi-building" rendered="#{menuBean.superAdmin}">
<p:menuitem id="m_liste_organisations" value="Liste des Organisations" icon="pi pi-list" outcome="/pages/secure/organisation/liste" />
<p:menuitem id="m_nouvelle_organisation" value="Nouvelle Organisation" icon="pi pi-plus" outcome="/pages/secure/organisation/nouvelle" />
<p:menuitem id="m_statistiques_orga" value="Statistiques" icon="pi pi-chart-bar" outcome="/pages/secure/organisation/statistiques" />
<p:menuitem id="m_types_organisations" value="Types d'Organisation" icon="pi pi-tags" outcome="/pages/super-admin/types/organisations" />
<p:menuitem id="m_liste_organisations" value="Liste des Organisations" icon="pi pi-list" outcome="#{paths.organisationListe}" />
<p:menuitem id="m_nouvelle_organisation" value="Nouvelle Organisation" icon="pi pi-plus" outcome="#{paths.organisationNouvelle}" />
<p:menuitem id="m_statistiques_orga" value="Statistiques" icon="pi pi-chart-bar" outcome="#{paths.organisationStatistiques}" />
<p:menuitem id="m_types_organisations" value="Types d'Organisation" icon="pi pi-tags" outcome="#{paths.superAdminTypesOrganisations}" />
</p:submenu>
<p:submenu id="m_mon_organisation" label="Mon Organisation" icon="pi pi-building" rendered="#{menuBean.adminOrganisation and not menuBean.superAdmin}">
<p:menuitem id="m_detail_organisation" value="Détail &amp; Paramètres" icon="pi pi-building"
outcome="/pages/secure/organisation/detail">
outcome="#{paths.organisationDetail}">
<f:param name="id" value="#{userSession.activeOrganisationId}" />
</p:menuitem>
<p:menuitem id="m_mon_abonnement" value="Mon Abonnement" icon="pi pi-credit-card"
outcome="/pages/secure/souscription/dashboard" />
outcome="#{paths.souscriptionDashboard}" />
</p:submenu>
<!-- ════════════════════════════════════════════════════════ -->
@@ -66,64 +66,77 @@
<!-- Un seul sous-menu, items conditionnels par rôle -->
<!-- ════════════════════════════════════════════════════════ -->
<p:submenu id="m_membres" label="Membres" icon="pi pi-users" rendered="#{menuBean.annuaireMembresVisible}">
<p:menuitem id="m_liste_membres" value="Annuaire des Membres" icon="pi pi-list" outcome="/pages/secure/membre/liste" />
<p:menuitem id="m_recherche_membres" value="Rechercher un Membre" icon="pi pi-search" outcome="/pages/secure/membre/recherche" />
<p:menuitem id="m_inscription" value="Nouvelle Inscription" icon="pi pi-user-plus" outcome="/pages/secure/membre/inscription" rendered="#{menuBean.gestionMembresMenuVisible}" />
<p:menuitem id="m_validation_membres" value="Validation Inscriptions" icon="pi pi-check-circle" outcome="/pages/secure/membre/validation" rendered="#{menuBean.gestionMembresMenuVisible}" />
<p:menuitem id="m_import_membres" value="Import en Masse" icon="pi pi-upload" outcome="/pages/secure/membre/import" rendered="#{menuBean.gestionMembresMenuVisible}" />
<p:menuitem id="m_export_membres" value="Export Membres" icon="pi pi-download" outcome="/pages/secure/membre/export" rendered="#{menuBean.gestionMembresMenuVisible}" />
<p:menuitem id="m_liste_membres" value="Annuaire des Membres" icon="pi pi-list" outcome="#{paths.membreListe}" />
<p:menuitem id="m_recherche_membres" value="Rechercher un Membre" icon="pi pi-search" outcome="#{paths.membreRecherche}" />
<p:menuitem id="m_inscription" value="Nouvelle Inscription" icon="pi pi-user-plus" outcome="#{paths.membreInscription}" rendered="#{menuBean.gestionMembresMenuVisible}" />
<p:menuitem id="m_validation_membres" value="Validation Inscriptions" icon="pi pi-check-circle" outcome="#{paths.membreValidation}" rendered="#{menuBean.gestionMembresMenuVisible}" />
<p:menuitem id="m_import_membres" value="Import en Masse" icon="pi pi-upload" outcome="#{paths.membreImport}" rendered="#{menuBean.gestionMembresMenuVisible}" />
<p:menuitem id="m_export_membres" value="Export Membres" icon="pi pi-download" outcome="#{paths.membreExport}" rendered="#{menuBean.gestionMembresMenuVisible}" />
</p:submenu>
<!-- ════════════════════════════════════════════════════════ -->
<!-- ADHÉSIONS -->
<!-- ════════════════════════════════════════════════════════ -->
<p:submenu id="m_adhesions" label="Adhésions" icon="pi pi-bookmark" rendered="#{menuBean.adhesionsMenuVisible}">
<p:menuitem id="m_demande_adhesion" value="Nouvelle Demande" icon="pi pi-plus-circle" outcome="/pages/secure/adhesion/demande" />
<p:menuitem id="m_liste_adhesions" value="Toutes les Adhésions" icon="pi pi-list" outcome="/pages/secure/adhesion/liste" />
<p:menuitem id="m_validation_adhesion" value="Validation des Demandes" icon="pi pi-check-circle" outcome="/pages/secure/adhesion/validation" rendered="#{menuBean.validationAdhesionVisible}" />
<p:menuitem id="m_renouvellement" value="Renouvellements" icon="pi pi-refresh" outcome="/pages/secure/adhesion/renouvellement" />
<p:menuitem id="m_historique_adhesions" value="Historique" icon="pi pi-history" outcome="/pages/secure/adhesion/historique" />
<p:menuitem id="m_demande_adhesion" value="Nouvelle Demande" icon="pi pi-plus-circle" outcome="#{paths.adhesionDemande}" />
<p:menuitem id="m_liste_adhesions" value="Toutes les Adhésions" icon="pi pi-list" outcome="#{paths.adhesionListe}" />
<p:menuitem id="m_validation_adhesion" value="Validation des Demandes" icon="pi pi-check-circle" outcome="#{paths.adhesionValidation}" rendered="#{menuBean.validationAdhesionVisible}" />
<p:menuitem id="m_renouvellement" value="Renouvellements" icon="pi pi-refresh" outcome="#{paths.adhesionRenouvellement}" />
<p:menuitem id="m_historique_adhesions" value="Historique" icon="pi pi-history" outcome="#{paths.adhesionHistorique}" />
</p:submenu>
<!-- ════════════════════════════════════════════════════════ -->
<!-- MES FINANCES (perso — tous les membres) -->
<!-- ════════════════════════════════════════════════════════ -->
<p:submenu id="m_mes_finances" label="Mes Finances" icon="pi pi-wallet" rendered="#{menuBean.mesFinancesMenuVisible}">
<p:menuitem id="m_mes_cotisations" value="Mes Cotisations" icon="pi pi-credit-card" outcome="/pages/secure/membre/cotisations" />
<p:menuitem id="m_payer_cotisations" value="Payer mes Cotisations" icon="pi pi-dollar" outcome="/pages/secure/cotisation/paiement" />
<p:menuitem id="m_historique_finances" value="Historique" icon="pi pi-history" outcome="/pages/secure/cotisation/historique" />
<p:menuitem id="m_mes_cotisations" value="Mes Cotisations" icon="pi pi-credit-card" outcome="#{paths.membreCotisations}" />
<p:menuitem id="m_payer_cotisations" value="Payer mes Cotisations" icon="pi pi-dollar" outcome="#{paths.cotisationPaiement}" />
<p:menuitem id="m_historique_finances" value="Historique" icon="pi pi-history" outcome="#{paths.cotisationHistorique}" />
</p:submenu>
<!-- ════════════════════════════════════════════════════════ -->
<!-- GESTION FINANCIÈRE (admin — trésorier, admin org) -->
<!-- ════════════════════════════════════════════════════════ -->
<p:submenu id="m_gestion_finances" label="Gestion Financière" icon="pi pi-dollar" rendered="#{menuBean.gestionFinancesMenuVisible}">
<p:menuitem id="m_gestion_cotisations" value="Gestion Cotisations" icon="pi pi-dollar" outcome="/pages/admin/cotisations/gestion" />
<p:menuitem id="m_tresorerie" value="Trésorerie" icon="pi pi-wallet" outcome="/pages/secure/finance/tresorerie" />
<p:menuitem id="m_budgets" value="Gestion des Budgets" icon="pi pi-chart-pie" outcome="/pages/secure/finance/budgets" />
<p:menuitem id="m_approbations_finance" value="Approbations" icon="pi pi-check-square" outcome="/pages/secure/finance/approbations" />
<p:menuitem id="m_comptabilite" value="Comptabilité" icon="pi pi-calculator" outcome="/pages/secure/comptabilite/gestion" />
<p:menuitem id="m_relances" value="Relances Cotisations" icon="pi pi-bell" outcome="/pages/secure/cotisation/relances" />
<p:menuitem id="m_bilans" value="Bilans Financiers" icon="pi pi-chart-line" outcome="/pages/secure/finance/bilans" />
<p:menuitem id="m_gestion_cotisations" value="Gestion Cotisations" icon="pi pi-dollar" outcome="#{paths.cotisationsGestionAdmin}" />
<p:menuitem id="m_tresorerie" value="Trésorerie" icon="pi pi-wallet" outcome="#{paths.financeTresorerie}" />
<p:menuitem id="m_budgets" value="Gestion des Budgets" icon="pi pi-chart-pie" outcome="#{paths.financeBudgets}" />
<p:menuitem id="m_approbations_finance" value="Approbations" icon="pi pi-check-square" outcome="#{paths.financeApprobations}" />
<p:menuitem id="m_comptabilite" value="Comptabilité" icon="pi pi-calculator" outcome="#{paths.comptabiliteGestion}" />
<p:menuitem id="m_relances" value="Relances Cotisations" icon="pi pi-bell" outcome="#{paths.cotisationRelances}" />
<p:menuitem id="m_bilans" value="Bilans Financiers" icon="pi pi-chart-line" outcome="#{paths.financeBilans}" />
</p:submenu>
<!-- ════════════════════════════════════════════════════════ -->
<!-- CONFORMITÉ (Sprint 8 — BCEAO / ARTCI / OHADA) -->
<!-- ════════════════════════════════════════════════════════ -->
<p:submenu id="m_conformite" label="Conformité" icon="pi pi-verified" rendered="#{menuBean.conformiteDashboardVisible}">
<p:menuitem id="m_conformite_dashboard" value="Tableau de bord" icon="pi pi-chart-bar" outcome="#{paths.conformiteDashboard}" />
<p:menuitem id="m_rapports_trimestriels" value="Rapports trimestriels" icon="pi pi-file-pdf" outcome="#{paths.conformiteRapportsTrimestriels}" rendered="#{menuBean.rapportsTrimestrielsVisible}" />
<p:menuitem id="m_ubo" value="Bénéficiaires Effectifs" icon="pi pi-users" outcome="#{paths.conformiteBeneficiairesEffectifs}" rendered="#{menuBean.beneficiairesEffectifsVisible}" />
<p:menuitem id="m_audit_trail" value="Audit Trail" icon="pi pi-history" outcome="#{paths.conformiteAuditTrail}" rendered="#{menuBean.auditTrailViewerVisible}" />
<p:menuitem id="m_live_feed" value="Live Activity Feed" icon="pi pi-bolt" outcome="#{paths.conformiteLiveFeed}" />
<p:menuitem id="m_role_delegations" value="Délégations de rôles" icon="pi pi-share-alt" outcome="#{paths.adminRoleDelegations}" rendered="#{menuBean.roleDelegationsVisible}" />
<p:menuitem id="m_pispi_readiness" value="PI-SPI Readiness" icon="pi pi-cog" outcome="#{paths.adminPispiReadiness}" rendered="#{menuBean.pispiReadinessVisible}" />
</p:submenu>
<!-- ════════════════════════════════════════════════════════ -->
<!-- ÉPARGNE (module EPARGNE — mutuelle, coopérative) -->
<!-- ════════════════════════════════════════════════════════ -->
<p:submenu id="m_epargne" label="Épargne &amp; Crédit" icon="pi pi-wallet" rendered="#{menuBean.epargneMenuVisible}">
<p:menuitem id="m_epargne_comptes" value="Comptes Épargne" icon="pi pi-wallet" outcome="/pages/secure/epargne/comptes" />
<p:menuitem id="m_demandes_credit" value="Demandes de Crédit" icon="pi pi-inbox" outcome="/pages/secure/credit/demandes" rendered="#{menuBean.creditMenuVisible}" />
<p:menuitem id="m_evaluation_credit" value="Évaluation Solvabilité" icon="pi pi-search" outcome="/pages/secure/credit/evaluation" rendered="#{menuBean.creditMenuVisible}" />
<p:menuitem id="m_suivi_credits" value="Suivi des Crédits" icon="pi pi-eye" outcome="/pages/secure/credit/suivi" rendered="#{menuBean.creditMenuVisible}" />
<p:menuitem id="m_remboursements" value="Remboursements" icon="pi pi-replay" outcome="/pages/secure/credit/remboursements" rendered="#{menuBean.creditMenuVisible}" />
<p:menuitem id="m_stats_credit" value="Statistiques Crédit" icon="pi pi-chart-bar" outcome="/pages/secure/credit/statistiques" rendered="#{menuBean.creditMenuVisible}" />
<p:menuitem id="m_epargne_comptes" value="Comptes Épargne" icon="pi pi-wallet" outcome="#{paths.epargneComptes}" />
<p:menuitem id="m_demandes_credit" value="Demandes de Crédit" icon="pi pi-inbox" outcome="#{paths.creditDemandes}" rendered="#{menuBean.creditMenuVisible}" />
<p:menuitem id="m_evaluation_credit" value="Évaluation Solvabilité" icon="pi pi-search" outcome="#{paths.creditEvaluation}" rendered="#{menuBean.creditMenuVisible}" />
<p:menuitem id="m_suivi_credits" value="Suivi des Crédits" icon="pi pi-eye" outcome="#{paths.creditSuivi}" rendered="#{menuBean.creditMenuVisible}" />
<p:menuitem id="m_remboursements" value="Remboursements" icon="pi pi-replay" outcome="#{paths.creditRemboursements}" rendered="#{menuBean.creditMenuVisible}" />
<p:menuitem id="m_stats_credit" value="Statistiques Crédit" icon="pi pi-chart-bar" outcome="#{paths.creditStatistiques}" rendered="#{menuBean.creditMenuVisible}" />
</p:submenu>
<!-- Mon Épargne (membre — visible si pas admin épargne) -->
<p:submenu id="m_mon_epargne" label="Mon Épargne" icon="pi pi-wallet" rendered="#{menuBean.epargneMemberVisible and not menuBean.epargneMenuVisible}">
<p:menuitem id="m_mon_epargne_compte" value="Mon Compte Épargne" icon="pi pi-wallet" outcome="/pages/secure/epargne/comptes" />
<p:menuitem id="m_demande_pret" value="Demander un Prêt" icon="pi pi-plus" outcome="/pages/secure/credit/demandes" rendered="#{menuBean.creditMemberVisible}" />
<p:menuitem id="m_mes_credits" value="Mes Crédits en Cours" icon="pi pi-list" outcome="/pages/secure/credit/suivi" rendered="#{menuBean.creditMemberVisible}" />
<p:menuitem id="m_mon_epargne_compte" value="Mon Compte Épargne" icon="pi pi-wallet" outcome="#{paths.epargneComptes}" />
<p:menuitem id="m_demande_pret" value="Demander un Prêt" icon="pi pi-plus" outcome="#{paths.creditDemandes}" rendered="#{menuBean.creditMemberVisible}" />
<p:menuitem id="m_mes_credits" value="Mes Crédits en Cours" icon="pi pi-list" outcome="#{paths.creditSuivi}" rendered="#{menuBean.creditMemberVisible}" />
</p:submenu>
<!-- ════════════════════════════════════════════════════════ -->
@@ -163,70 +176,70 @@
<!-- AIDE SOCIALE -->
<!-- ════════════════════════════════════════════════════════ -->
<p:submenu id="m_mes_aides" label="Aide Sociale" icon="pi pi-heart" rendered="#{menuBean.mesAidesSocialesMenuVisible}">
<p:menuitem id="m_demande_aide" value="Faire une Demande" icon="pi pi-plus" outcome="/pages/secure/aide/demande" />
<p:menuitem id="m_mes_demandes_aide" value="Mes Demandes" icon="pi pi-list" outcome="/pages/secure/aide/requests" />
<p:menuitem id="m_historique_aides" value="Historique" icon="pi pi-clock" outcome="/pages/secure/aide/historique" />
<p:menuitem id="m_traitement_aide" value="Traitement des Demandes" icon="pi pi-cog" outcome="/pages/secure/aide/traitement" rendered="#{menuBean.gestionAidesSocialesMenuVisible}" />
<p:menuitem id="m_suivi_aide" value="Suivi des Bénéficiaires" icon="pi pi-eye" outcome="/pages/secure/aide/approved" rendered="#{menuBean.gestionAidesSocialesMenuVisible}" />
<p:menuitem id="m_statistiques_aides" value="Statistiques Sociales" icon="pi pi-chart-line" outcome="/pages/secure/aide/statistiques" rendered="#{menuBean.gestionAidesSocialesMenuVisible}" />
<p:menuitem id="m_demande_aide" value="Faire une Demande" icon="pi pi-plus" outcome="#{paths.aideDemande}" />
<p:menuitem id="m_mes_demandes_aide" value="Mes Demandes" icon="pi pi-list" outcome="#{paths.aideRequests}" />
<p:menuitem id="m_historique_aides" value="Historique" icon="pi pi-clock" outcome="#{paths.aideHistorique}" />
<p:menuitem id="m_traitement_aide" value="Traitement des Demandes" icon="pi pi-cog" outcome="#{paths.aideTraitement}" rendered="#{menuBean.gestionAidesSocialesMenuVisible}" />
<p:menuitem id="m_suivi_aide" value="Suivi des Bénéficiaires" icon="pi pi-eye" outcome="#{paths.aideApproved}" rendered="#{menuBean.gestionAidesSocialesMenuVisible}" />
<p:menuitem id="m_statistiques_aides" value="Statistiques Sociales" icon="pi pi-chart-line" outcome="#{paths.aideStatistiques}" rendered="#{menuBean.gestionAidesSocialesMenuVisible}" />
</p:submenu>
<!-- ════════════════════════════════════════════════════════ -->
<!-- ÉVÉNEMENTS -->
<!-- ════════════════════════════════════════════════════════ -->
<p:submenu id="m_evenements" label="Événements" icon="pi pi-calendar" rendered="#{menuBean.mesEvenementsMenuVisible}">
<p:menuitem id="m_calendrier" value="Calendrier" icon="pi pi-calendar-plus" outcome="/pages/secure/evenement/calendrier" />
<p:menuitem id="m_mes_inscriptions_events" value="Mes Inscriptions" icon="pi pi-list" outcome="/pages/secure/evenement/participants" />
<p:menuitem id="m_creation_evenement" value="Nouvel Événement" icon="pi pi-plus" outcome="/pages/secure/evenement/creation" rendered="#{menuBean.gestionEvenementsMenuVisible}" />
<p:menuitem id="m_planification" value="Planification" icon="pi pi-clock" outcome="/pages/secure/evenement/planification" rendered="#{menuBean.gestionEvenementsMenuVisible}" />
<p:menuitem id="m_logistique" value="Logistique" icon="pi pi-truck" outcome="/pages/secure/evenement/logistique" rendered="#{menuBean.gestionEvenementsMenuVisible}" />
<p:menuitem id="m_gestion_generale_evenements" value="Gestion Générale" icon="pi pi-cog" outcome="/pages/secure/evenement/gestion" rendered="#{menuBean.gestionEvenementsMenuVisible}" />
<p:menuitem id="m_bilan_evenements" value="Bilans" icon="pi pi-chart-bar" outcome="/pages/secure/evenement/bilan" rendered="#{menuBean.gestionEvenementsMenuVisible}" />
<p:menuitem id="m_calendrier" value="Calendrier" icon="pi pi-calendar-plus" outcome="#{paths.evenementCalendrier}" />
<p:menuitem id="m_mes_inscriptions_events" value="Mes Inscriptions" icon="pi pi-list" outcome="#{paths.evenementParticipants}" />
<p:menuitem id="m_creation_evenement" value="Nouvel Événement" icon="pi pi-plus" outcome="#{paths.evenementCreation}" rendered="#{menuBean.gestionEvenementsMenuVisible}" />
<p:menuitem id="m_planification" value="Planification" icon="pi pi-clock" outcome="#{paths.evenementPlanification}" rendered="#{menuBean.gestionEvenementsMenuVisible}" />
<p:menuitem id="m_logistique" value="Logistique" icon="pi pi-truck" outcome="#{paths.evenementLogistique}" rendered="#{menuBean.gestionEvenementsMenuVisible}" />
<p:menuitem id="m_gestion_generale_evenements" value="Gestion Générale" icon="pi pi-cog" outcome="#{paths.evenementGestion}" rendered="#{menuBean.gestionEvenementsMenuVisible}" />
<p:menuitem id="m_bilan_evenements" value="Bilans" icon="pi pi-chart-bar" outcome="#{paths.evenementBilan}" rendered="#{menuBean.gestionEvenementsMenuVisible}" />
</p:submenu>
<!-- ════════════════════════════════════════════════════════ -->
<!-- COMMUNICATION -->
<!-- ════════════════════════════════════════════════════════ -->
<p:submenu id="m_communications" label="Communication" icon="pi pi-envelope" rendered="#{menuBean.mesCommunicationsMenuVisible}">
<p:menuitem id="m_messagerie" value="Messagerie" icon="pi pi-comments" outcome="/pages/secure/communication/conversations" />
<p:menuitem id="m_mes_notifications" value="Mes Notifications" icon="pi pi-bell" outcome="/pages/secure/communication/notifications" />
<p:menuitem id="m_messagerie" value="Messagerie" icon="pi pi-comments" outcome="#{paths.communicationConversations}" />
<p:menuitem id="m_mes_notifications" value="Mes Notifications" icon="pi pi-bell" outcome="#{paths.communicationNotifications}" />
</p:submenu>
<!-- ════════════════════════════════════════════════════════ -->
<!-- DOCUMENTS -->
<!-- ════════════════════════════════════════════════════════ -->
<p:submenu id="m_documents" label="Documents" icon="pi pi-folder" rendered="#{menuBean.documentsMenuVisible}">
<p:menuitem id="m_mes_documents" value="Mes Documents" icon="pi pi-file" outcome="/pages/secure/documents/mes-documents" />
<p:menuitem id="m_mes_documents" value="Mes Documents" icon="pi pi-file" outcome="#{paths.documentsMesDocuments}" />
</p:submenu>
<!-- ════════════════════════════════════════════════════════ -->
<!-- RAPPORTS ET ANALYSES -->
<!-- ════════════════════════════════════════════════════════ -->
<p:submenu id="m_rapports" label="Rapports" icon="pi pi-chart-bar" rendered="#{menuBean.rapportsMenuVisible}">
<p:menuitem id="m_tableaux_bord" value="Tableaux de Bord" icon="pi pi-chart-line" outcome="/pages/secure/rapport/tableaux-bord" />
<p:menuitem id="m_rapport_membres" value="Rapport Membres" icon="pi pi-users" outcome="/pages/secure/rapport/membres" />
<p:menuitem id="m_rapport_finances" value="Rapport Financier" icon="pi pi-dollar" outcome="/pages/secure/rapport/finances" rendered="#{menuBean.rapportFinancierVisible}" />
<p:menuitem id="m_rapport_activites" value="Rapport d'Activités" icon="pi pi-chart-line" outcome="/pages/secure/rapport/activites" />
<p:menuitem id="m_export" value="Exports" icon="pi pi-download" outcome="/pages/secure/rapport/export" rendered="#{menuBean.exportsPersonnalisesVisible}" />
<p:menuitem id="m_tableaux_bord" value="Tableaux de Bord" icon="pi pi-chart-line" outcome="#{paths.rapportTableauxBord}" />
<p:menuitem id="m_rapport_membres" value="Rapport Membres" icon="pi pi-users" outcome="#{paths.rapportMembres}" />
<p:menuitem id="m_rapport_finances" value="Rapport Financier" icon="pi pi-dollar" outcome="#{paths.rapportFinances}" rendered="#{menuBean.rapportFinancierVisible}" />
<p:menuitem id="m_rapport_activites" value="Rapport d'Activités" icon="pi pi-chart-line" outcome="#{paths.rapportActivites}" />
<p:menuitem id="m_export" value="Exports" icon="pi pi-download" outcome="#{paths.rapportExport}" rendered="#{menuBean.exportsPersonnalisesVisible}" />
</p:submenu>
<!-- ════════════════════════════════════════════════════════ -->
<!-- MON ESPACE PERSONNEL -->
<!-- ════════════════════════════════════════════════════════ -->
<p:submenu id="m_personnel" label="Mon Espace" icon="pi pi-user">
<p:menuitem id="m_mon_profil" value="Mon Profil" icon="pi pi-user-edit" outcome="/pages/secure/personnel/profil" />
<p:menuitem id="m_mes_preferences" value="Mes Préférences" icon="pi pi-cog" outcome="/pages/secure/personnel/preferences" />
<p:menuitem id="m_personnel_notifications" value="Mes Notifications" icon="pi pi-bell" outcome="/pages/secure/personnel/notifications" />
<p:menuitem id="m_parametres_compte" value="Paramètres Compte" icon="pi pi-lock" outcome="/pages/secure/personnel/parametres" />
<p:menuitem id="m_mon_profil" value="Mon Profil" icon="pi pi-user-edit" outcome="#{paths.personnelProfil}" />
<p:menuitem id="m_mes_preferences" value="Mes Préférences" icon="pi pi-cog" outcome="#{paths.personnelPreferences}" />
<p:menuitem id="m_personnel_notifications" value="Mes Notifications" icon="pi pi-bell" outcome="#{paths.personnelNotifications}" />
<p:menuitem id="m_parametres_compte" value="Paramètres Compte" icon="pi pi-lock" outcome="#{paths.personnelParametres}" />
</p:submenu>
<!-- ════════════════════════════════════════════════════════ -->
<!-- AIDE ET SUPPORT -->
<!-- ════════════════════════════════════════════════════════ -->
<p:submenu id="m_aide_documentation" label="Aide" icon="pi pi-question-circle">
<p:menuitem id="m_faq" value="Questions Fréquentes" icon="pi pi-question" outcome="/pages/secure/aide/faq" />
<p:menuitem id="m_support" value="Contacter le Support" icon="pi pi-phone" outcome="/pages/secure/aide/support" />
<p:menuitem id="m_apropos" value="À Propos" icon="pi pi-info" outcome="/pages/secure/aide/apropos" />
<p:menuitem id="m_faq" value="Questions Fréquentes" icon="pi pi-question" outcome="#{paths.aideFaq}" />
<p:menuitem id="m_support" value="Contacter le Support" icon="pi pi-phone" outcome="#{paths.aideSupport}" />
<p:menuitem id="m_apropos" value="À Propos" icon="pi pi-info" outcome="#{paths.aideApropos}" />
</p:submenu>
</fr:menu>

View File

@@ -21,7 +21,7 @@
<a href="#" class="menu-button">
<i class="pi pi-bars"/>
</a>
<h:link id="logolink" outcome="/pages/secure/dashboard" styleClass="layout-topbar-logo" style="display:none"/>
<h:link id="logolink" outcome="#{paths.dashboard}" styleClass="layout-topbar-logo" style="display:none"/>
</div>
<!-- CENTER - Menu -->

View File

@@ -650,6 +650,55 @@
</div>
</p:fieldset>
<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- SECTION 6.5 : CONFORMITÉ (Sprint 1 — Instr. BCEAO + OHADA) -->
<!-- ═══════════════════════════════════════════════════════════════ -->
<p:fieldset legend="🛡️ Conformité réglementaire" toggleable="true" collapsed="true" styleClass="mb-3">
<div class="formgrid grid">
<!-- Référentiel comptable -->
<div class="field col-12 md:col-6">
<p:outputLabel for="referentiel" value="Référentiel comptable" styleClass="font-semibold" />
<p:selectOneMenu id="referentiel"
value="#{model.referentielComptable}">
<f:selectItem itemLabel="-- Auto (selon type d'organisation) --" itemValue="" />
<f:selectItem itemLabel="SYSCOHADA — Système comptable OHADA (associations, coopératives)" itemValue="SYSCOHADA" />
<f:selectItem itemLabel="SYCEBNL — Entités à but non lucratif (mutuelles santé, ONG)" itemValue="SYCEBNL" />
<f:selectItem itemLabel="PCSFD_UMOA — Plan comptable SFD UEMOA (microfinance art. 44)" itemValue="PCSFD_UMOA" />
</p:selectOneMenu>
<p:tooltip for="referentiel" position="top"
value="Détermine bilan, compte de résultat et annexes (Acte uniforme OHADA + BCEAO)" />
<small class="text-500">
<i class="pi pi-info-circle mr-1"/>
Si vide : appliqué automatiquement selon typeOrganisation
</small>
</div>
<!-- Compliance Officer (picker autoComplete) -->
<div class="field col-12 md:col-6">
<p:outputLabel for="complianceOfficer" value="Compliance Officer"
styleClass="font-semibold" />
<p:autoComplete id="complianceOfficer"
value="#{model.complianceOfficerId}"
completeMethod="#{complianceOfficerPickerBean.suggest}"
var="m"
itemLabel="#{complianceOfficerPickerBean.label(m)}"
itemValue="#{m.id}"
forceSelection="true"
minQueryLength="2"
queryDelay="300"
placeholder="Tapez 2+ lettres du nom ou prénom..." />
<p:tooltip for="complianceOfficer" position="top"
value="Désignation obligatoire selon Instr. BCEAO 001-03-2025 (LBC/FT). Membre rattaché à la direction, distinct du trésorier (séparation des pouvoirs)." />
<small class="text-500">
<i class="pi pi-shield mr-1"/>
Instr. BCEAO 001-03-2025 — LBC/FT
</small>
</div>
</div>
</p:fieldset>
<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- SECTION 7 : MISSION & ACTIVITÉS -->
<!-- ═══════════════════════════════════════════════════════════════ -->

View File

@@ -0,0 +1,54 @@
package dev.lions.unionflow.client.api.dto;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.time.LocalDateTime;
import java.util.UUID;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
class RapportTrimestrielDtoTest {
private RapportTrimestrielDto build(String statut) {
return new RapportTrimestrielDto(
UUID.randomUUID(), UUID.randomUUID(), 2026, 1,
LocalDateTime.now(), statut, 75, null, null, null);
}
@Test
@DisplayName("estDraft / estSigne / estArchive — DRAFT")
void draft() {
var r = build("DRAFT");
assertTrue(r.estDraft());
assertFalse(r.estSigne());
assertFalse(r.estArchive());
}
@Test
@DisplayName("estDraft / estSigne / estArchive — SIGNE")
void signe() {
var r = build("SIGNE");
assertFalse(r.estDraft());
assertTrue(r.estSigne());
assertFalse(r.estArchive());
}
@Test
@DisplayName("estDraft / estSigne / estArchive — ARCHIVE")
void archive() {
var r = build("ARCHIVE");
assertFalse(r.estDraft());
assertFalse(r.estSigne());
assertTrue(r.estArchive());
}
@Test
@DisplayName("Statut inconnu → tous false")
void inconnu() {
var r = build("AUTRE");
assertFalse(r.estDraft());
assertFalse(r.estSigne());
assertFalse(r.estArchive());
}
}

View File

@@ -0,0 +1,80 @@
package dev.lions.unionflow.client.constants;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
/**
* Vérifie que chaque constante {@link ViewPaths} pointe vers un fichier xhtml existant
* sous {@code META-INF/resources/}.
*
* <p>Garantie de non-régression : tout outcome utilisé en navigation existe physiquement.
*/
class ViewPathsConsistencyTest {
/** Constantes auxquelles aucun fichier physique n'est attaché (suffixes / racines techniques). */
private static final java.util.Set<String> NON_FILE_CONSTANTS = java.util.Set.of(
"REDIRECT_SUFFIX", "ROOT", "INDEX");
/**
* Pages référencées par le code mais pas encore implémentées.
* Sprint 13.B a créé les 3 stubs détectés par Sprint 12 — set vide.
*/
private static final java.util.Set<String> KNOWN_MISSING_PAGES = java.util.Set.of();
@Test
@DisplayName("Chaque constante ViewPaths pointe vers un .xhtml existant en classpath")
void chaquePathExisteCommeFichier() throws Exception {
List<String> manquants = new ArrayList<>();
for (Field f : ViewPaths.class.getDeclaredFields()) {
if (!Modifier.isStatic(f.getModifiers())
|| !Modifier.isFinal(f.getModifiers())
|| !f.getType().equals(String.class)) {
continue;
}
String name = f.getName();
if (NON_FILE_CONSTANTS.contains(name)) continue;
if (KNOWN_MISSING_PAGES.contains(name)) continue; // dette pré-Sprint 12
String path = (String) f.get(null);
String resourcePath = "META-INF/resources" + path + ".xhtml";
URL url = Thread.currentThread().getContextClassLoader().getResource(resourcePath);
if (url == null) {
manquants.add(name + "" + resourcePath);
}
}
if (!manquants.isEmpty()) {
fail("Paths sans fichier xhtml correspondant :\n " + String.join("\n ", manquants));
}
}
@Test
@DisplayName("REDIRECT_SUFFIX commence par '?'")
void redirectSuffixFormat() {
assertNotNull(ViewPaths.REDIRECT_SUFFIX);
assertTrue(ViewPaths.REDIRECT_SUFFIX.startsWith("?"));
assertTrue(ViewPaths.REDIRECT_SUFFIX.contains("faces-redirect=true"));
}
@Test
@DisplayName("Aucune constante n'est null ou blanche")
void aucuneConstanteVide() throws Exception {
for (Field f : ViewPaths.class.getDeclaredFields()) {
if (!Modifier.isStatic(f.getModifiers())
|| !f.getType().equals(String.class)) continue;
String value = (String) f.get(null);
assertNotNull(value, "Constante " + f.getName() + " null");
assertTrue(!value.isBlank(), "Constante " + f.getName() + " vide");
}
}
}

View File

@@ -0,0 +1,76 @@
package dev.lions.unionflow.client.view;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
class AuditTrailViewerBeanTest {
private AuditTrailViewerBean bean;
@BeforeEach
void setUp() {
bean = new AuditTrailViewerBean();
}
@Test
@DisplayName("getCouleurAction — DELETE → danger")
void couleurDelete() {
assertEquals("danger", bean.getCouleurAction("DELETE"));
assertEquals("danger", bean.getCouleurAction("PAYMENT_FAILED"));
}
@Test
@DisplayName("getCouleurAction — VALIDATE / payment confirmé / aide approuvée → success")
void couleurSuccess() {
assertEquals("success", bean.getCouleurAction("VALIDATE"));
assertEquals("success", bean.getCouleurAction("PAYMENT_CONFIRMED"));
assertEquals("success", bean.getCouleurAction("AID_REQUEST_APPROVED"));
}
@Test
@DisplayName("getCouleurAction — UPDATE / payment initié / budget approuvé → info")
void couleurInfo() {
assertEquals("info", bean.getCouleurAction("UPDATE"));
assertEquals("info", bean.getCouleurAction("PAYMENT_INITIATED"));
assertEquals("info", bean.getCouleurAction("BUDGET_APPROVED"));
}
@Test
@DisplayName("getCouleurAction — CREATE → primary")
void couleurCreate() {
assertEquals("primary", bean.getCouleurAction("CREATE"));
}
@Test
@DisplayName("getCouleurAction — EXPORT → warning")
void couleurExport() {
assertEquals("warning", bean.getCouleurAction("EXPORT"));
}
@Test
@DisplayName("getCouleurAction — null/inconnu → secondary")
void couleurDefault() {
assertEquals("secondary", bean.getCouleurAction(null));
assertEquals("secondary", bean.getCouleurAction("AUTRE"));
}
@Test
@DisplayName("getCouleurSod — true → success, false → danger, null → secondary")
void couleurSod() {
assertEquals("success", bean.getCouleurSod(true));
assertEquals("danger", bean.getCouleurSod(false));
assertEquals("secondary", bean.getCouleurSod(null));
}
@Test
@DisplayName("Defaults — mode = ORG, plage = 30 derniers jours")
void defaults() {
assertEquals("ORG", bean.getMode());
// plage from 30 jours avant maintenant — vérifié pas null
org.junit.jupiter.api.Assertions.assertNotNull(bean.getFrom());
org.junit.jupiter.api.Assertions.assertNotNull(bean.getTo());
}
}

View File

@@ -0,0 +1,96 @@
package dev.lions.unionflow.client.view;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import dev.lions.unionflow.server.api.dto.membre.response.MembreSummaryResponse;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
class ComplianceOfficerPickerBeanTest {
private ComplianceOfficerPickerBean bean;
@BeforeEach
void setUp() {
bean = new ComplianceOfficerPickerBean();
}
// ── label ────────────────────────────────────────────────────────────
@Test
@DisplayName("label — null → vide")
void labelNull() {
assertEquals("", bean.label(null));
}
@Test
@DisplayName("label — Prénom NOM (numéro)")
void labelComplet() {
MembreSummaryResponse m = new MembreSummaryResponse();
m.setPrenom("Jean Pierre");
m.setNom("dupont");
m.setNumeroMembre("MBR-2026-0042");
assertEquals("Jean Pierre DUPONT (MBR-2026-0042)", bean.label(m));
}
@Test
@DisplayName("label — sans numéro membre")
void labelSansNumero() {
MembreSummaryResponse m = new MembreSummaryResponse();
m.setPrenom("Marie");
m.setNom("Kone");
assertEquals("Marie KONE", bean.label(m));
}
@Test
@DisplayName("label — uniquement nom")
void labelNomSeul() {
MembreSummaryResponse m = new MembreSummaryResponse();
m.setNom("Diallo");
assertEquals("DIALLO", bean.label(m));
}
@Test
@DisplayName("label — uniquement prénom")
void labelPrenomSeul() {
MembreSummaryResponse m = new MembreSummaryResponse();
m.setPrenom("Aminata");
assertEquals("Aminata", bean.label(m));
}
@Test
@DisplayName("label — entité minimaliste sans nom/prénom → fallback id")
void labelMinimaliste() {
MembreSummaryResponse m = new MembreSummaryResponse();
m.setId(java.util.UUID.fromString("00000000-0000-0000-0000-000000000001"));
String label = bean.label(m);
assertTrue(label.contains("00000000-0000-0000-0000-000000000001"));
assertTrue(label.startsWith("(membre"));
}
// ── suggest avec query vide/null ──────────────────────────────────────
@Test
@DisplayName("suggest — query null → liste vide")
void suggestNull() {
// pas besoin de mock REST : retour direct sans appel réseau
assertTrue(bean.suggest(null).isEmpty());
}
@Test
@DisplayName("suggest — query blank → liste vide")
void suggestBlank() {
assertTrue(bean.suggest(" ").isEmpty());
}
// ── resoudre id null ──────────────────────────────────────────────────
@Test
@DisplayName("resoudre — UUID null → null")
void resoudreNull() {
assertNull(bean.resoudre(null));
}
}

View File

@@ -0,0 +1,109 @@
package dev.lions.unionflow.client.view;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import dev.lions.unionflow.client.api.dto.ComplianceSnapshotDto;
import dev.lions.unionflow.client.api.dto.ComplianceSnapshotDto.ConformiteIndicateurDto;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.util.UUID;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
/**
* Tests purs logique du bean conformité — pas d'invocation REST réelle.
*
* @since 2026-04-25 (Sprint 8)
*/
class ConformiteDashboardBeanTest {
private ConformiteDashboardBean bean;
@BeforeEach
void setUp() {
bean = new ConformiteDashboardBean();
}
private void setSnapshot(ConformiteDashboardBean b, ComplianceSnapshotDto s) throws Exception {
Field f = ConformiteDashboardBean.class.getDeclaredField("snapshot");
f.setAccessible(true);
f.set(b, s);
}
private ComplianceSnapshotDto snapshotAvec(int score, boolean officer, String agStatut) {
return new ComplianceSnapshotDto(
UUID.randomUUID(), "Test Mutuelle", "SYSCOHADA",
officer,
new ConformiteIndicateurDto(agStatut, ""),
new ConformiteIndicateurDto("OK", ""),
3, new BigDecimal("80"), new BigDecimal("70"),
new ConformiteIndicateurDto("OPTIONNEL", ""),
new ConformiteIndicateurDto("EN_VEILLE", ""),
new BigDecimal("60"),
score);
}
@Test
@DisplayName("couleurScore — score >=80 → success")
void couleurSuccess() throws Exception {
setSnapshot(bean, snapshotAvec(85, true, "OK"));
assertEquals("success", bean.getCouleurScore());
}
@Test
@DisplayName("couleurScore — 60..79 → warning")
void couleurWarning() throws Exception {
setSnapshot(bean, snapshotAvec(70, true, "OK"));
assertEquals("warning", bean.getCouleurScore());
}
@Test
@DisplayName("couleurScore — <60 → danger")
void couleurDanger() throws Exception {
setSnapshot(bean, snapshotAvec(45, true, "OK"));
assertEquals("danger", bean.getCouleurScore());
}
@Test
@DisplayName("couleurScore — snapshot null → secondary")
void couleurSecondaire() {
assertEquals("secondary", bean.getCouleurScore());
}
@Test
@DisplayName("hasAlertes — score 85 + officer OK + AG OK → false")
void pasDAlertes() throws Exception {
setSnapshot(bean, snapshotAvec(85, true, "OK"));
assertFalse(bean.hasAlertes());
}
@Test
@DisplayName("hasAlertes — officer absent → true")
void alerteOfficerAbsent() throws Exception {
setSnapshot(bean, snapshotAvec(85, false, "OK"));
assertTrue(bean.hasAlertes());
}
@Test
@DisplayName("hasAlertes — AG en RETARD → true")
void alerteAgRetard() throws Exception {
setSnapshot(bean, snapshotAvec(85, true, "RETARD"));
assertTrue(bean.hasAlertes());
}
@Test
@DisplayName("hasAlertes — score <60 → true")
void alerteScoreFaible() throws Exception {
setSnapshot(bean, snapshotAvec(50, true, "OK"));
assertTrue(bean.hasAlertes());
}
@Test
@DisplayName("hasAlertes — snapshot null → false")
void hasAlertesNull() {
assertFalse(bean.hasAlertes());
}
}

View File

@@ -0,0 +1,130 @@
package dev.lions.unionflow.client.view;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.time.LocalDateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
class LiveFeedBeanTest {
private LiveFeedBean bean;
@BeforeEach
void setUp() {
bean = new LiveFeedBean();
}
// ── couleurAction (mêmes règles que AuditTrailViewerBean) ────────────
@Test
@DisplayName("couleurAction — DELETE/PAYMENT_FAILED → danger")
void couleurDanger() {
assertEquals("danger", bean.getCouleurAction("DELETE"));
assertEquals("danger", bean.getCouleurAction("PAYMENT_FAILED"));
}
@Test
@DisplayName("couleurAction — VALIDATE/PAYMENT_CONFIRMED/AID_REQUEST_APPROVED → success")
void couleurSuccess() {
assertEquals("success", bean.getCouleurAction("VALIDATE"));
assertEquals("success", bean.getCouleurAction("PAYMENT_CONFIRMED"));
assertEquals("success", bean.getCouleurAction("AID_REQUEST_APPROVED"));
}
@Test
@DisplayName("couleurAction — UPDATE/PAYMENT_INITIATED/BUDGET_APPROVED → info")
void couleurInfo() {
assertEquals("info", bean.getCouleurAction("UPDATE"));
assertEquals("info", bean.getCouleurAction("PAYMENT_INITIATED"));
assertEquals("info", bean.getCouleurAction("BUDGET_APPROVED"));
}
@Test
@DisplayName("couleurAction — CREATE → primary, EXPORT → warning, autres → secondary")
void couleurAutres() {
assertEquals("primary", bean.getCouleurAction("CREATE"));
assertEquals("warning", bean.getCouleurAction("EXPORT"));
assertEquals("secondary", bean.getCouleurAction(null));
assertEquals("secondary", bean.getCouleurAction("INCONNU"));
}
@Test
@DisplayName("couleurSod — true→success, false→danger, null→secondary")
void couleurSod() {
assertEquals("success", bean.getCouleurSod(true));
assertEquals("danger", bean.getCouleurSod(false));
assertEquals("secondary", bean.getCouleurSod(null));
}
// ── tempsRelatif ─────────────────────────────────────────────────────
@Test
@DisplayName("tempsRelatif — null → '—'")
void tempsRelatifNull() {
assertEquals("", bean.tempsRelatif(null));
}
@Test
@DisplayName("tempsRelatif — 30s ago → 'il y a 30s'")
void tempsRelatifSecondes() {
LocalDateTime past = LocalDateTime.now().minusSeconds(30);
assertTrue(bean.tempsRelatif(past).startsWith("il y a "));
assertTrue(bean.tempsRelatif(past).endsWith("s"));
}
@Test
@DisplayName("tempsRelatif — 5min ago → 'il y a 5m'")
void tempsRelatifMinutes() {
LocalDateTime past = LocalDateTime.now().minusMinutes(5);
assertEquals("il y a 5m", bean.tempsRelatif(past));
}
@Test
@DisplayName("tempsRelatif — 2h ago → 'il y a 2h'")
void tempsRelatifHeures() {
LocalDateTime past = LocalDateTime.now().minusHours(2);
assertEquals("il y a 2h", bean.tempsRelatif(past));
}
@Test
@DisplayName("tempsRelatif — 3 jours ago → 'il y a 3j'")
void tempsRelatifJours() {
LocalDateTime past = LocalDateTime.now().minusDays(3);
assertEquals("il y a 3j", bean.tempsRelatif(past));
}
@Test
@DisplayName("tempsRelatif — futur → 'à l'instant'")
void tempsRelatifFutur() {
LocalDateTime futur = LocalDateTime.now().plusMinutes(1);
assertEquals("à l'instant", bean.tempsRelatif(futur));
}
// ── Limit clamping ───────────────────────────────────────────────────
@Test
@DisplayName("setLimit clamp [1, 500]")
void setLimitClamp() {
bean.setLimit(0);
assertEquals(1, bean.getLimit());
bean.setLimit(1000);
assertEquals(500, bean.getLimit());
bean.setLimit(50);
assertEquals(50, bean.getLimit());
bean.setLimit(-100);
assertEquals(1, bean.getLimit());
}
// ── Defaults ─────────────────────────────────────────────────────────
@Test
@DisplayName("Defaults — scope=SELF, limit=50, compteur=0")
void defaults() {
assertEquals("SELF", bean.getScope());
assertEquals(50, bean.getLimit());
assertEquals(0, bean.getCompteur());
}
}

View File

@@ -0,0 +1,85 @@
package dev.lions.unionflow.client.view;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import dev.lions.unionflow.client.api.dto.PispiReadinessDto;
import java.lang.reflect.Field;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
class PispiReadinessBeanTest {
private PispiReadinessBean bean;
@BeforeEach
void setUp() {
bean = new PispiReadinessBean();
}
private void setReadiness(PispiReadinessDto r) throws Exception {
Field f = PispiReadinessBean.class.getDeclaredField("readiness");
f.setAccessible(true);
f.set(bean, r);
}
@Test
@DisplayName("couleurStatus — null → secondary")
void couleurNull() {
assertEquals("secondary", bean.getCouleurStatus());
}
@Test
@DisplayName("couleurStatus — READY → success")
void couleurReady() throws Exception {
setReadiness(new PispiReadinessDto("READY", "url", List.of(), List.of(), List.of()));
assertEquals("success", bean.getCouleurStatus());
}
@Test
@DisplayName("couleurStatus — DEGRADED → warning")
void couleurDegraded() throws Exception {
setReadiness(new PispiReadinessDto("DEGRADED", "url", List.of(), List.of(), List.of()));
assertEquals("warning", bean.getCouleurStatus());
}
@Test
@DisplayName("couleurStatus — BLOCKED → danger")
void couleurBlocked() throws Exception {
setReadiness(new PispiReadinessDto("BLOCKED", "url", List.of(), List.of(), List.of()));
assertEquals("danger", bean.getCouleurStatus());
}
@Test
@DisplayName("couleurCheck — PASS → success quel que soit severity")
void couleurCheckPass() {
assertEquals("success", bean.getCouleurCheck("BLOCKING", "PASS"));
assertEquals("success", bean.getCouleurCheck("WARNING", "PASS"));
}
@Test
@DisplayName("couleurCheck — FAIL BLOCKING → danger")
void couleurCheckFailBlocking() {
assertEquals("danger", bean.getCouleurCheck("BLOCKING", "FAIL"));
}
@Test
@DisplayName("couleurCheck — FAIL WARNING → warning")
void couleurCheckFailWarning() {
assertEquals("warning", bean.getCouleurCheck("WARNING", "FAIL"));
}
@Test
@DisplayName("DTO estReady / estBlocked")
void dtoStateHelpers() {
var ready = new PispiReadinessDto("READY", "u", List.of(), List.of(), List.of());
var blocked = new PispiReadinessDto("BLOCKED", "u", List.of(), List.of(), List.of());
assertTrue(ready.estReady());
assertFalse(ready.estBlocked());
assertFalse(blocked.estReady());
assertTrue(blocked.estBlocked());
}
}

View File

@@ -0,0 +1,79 @@
package dev.lions.unionflow.client.view;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import dev.lions.unionflow.server.api.dto.compliance.response.KpiPublicSnapshot;
import java.lang.reflect.Field;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
class PublicKpiBeanTest {
private PublicKpiBean bean;
@BeforeEach
void setUp() {
bean = new PublicKpiBean();
}
private void setSnapshot(KpiPublicSnapshot s) throws Exception {
Field f = PublicKpiBean.class.getDeclaredField("snapshot");
f.setAccessible(true);
f.set(bean, s);
}
// ── couleurScore ─────────────────────────────────────────────────────
@Test
@DisplayName("couleurScore — score >= 80 → success")
void couleurSuccess() throws Exception {
setSnapshot(KpiPublicSnapshot.builder().scoreGlobal(85).build());
assertEquals("success", bean.getCouleurScore());
}
@Test
@DisplayName("couleurScore — 60..79 → warning")
void couleurWarning() throws Exception {
setSnapshot(KpiPublicSnapshot.builder().scoreGlobal(70).build());
assertEquals("warning", bean.getCouleurScore());
}
@Test
@DisplayName("couleurScore — < 60 → danger")
void couleurDanger() throws Exception {
setSnapshot(KpiPublicSnapshot.builder().scoreGlobal(50).build());
assertEquals("danger", bean.getCouleurScore());
}
@Test
@DisplayName("couleurScore — null snapshot → secondary")
void couleurNull() {
assertEquals("secondary", bean.getCouleurScore());
}
// ── couleurStatut ────────────────────────────────────────────────────
@Test
@DisplayName("couleurStatut — règles")
void couleurStatut() {
assertEquals("success", bean.getCouleurStatut("OK"));
assertEquals("danger", bean.getCouleurStatut("RETARD"));
assertEquals("warning", bean.getCouleurStatut("EN_ATTENTE"));
assertEquals("info", bean.getCouleurStatut("OBLIGATOIRE"));
assertEquals("secondary", bean.getCouleurStatut(null));
assertEquals("secondary", bean.getCouleurStatut("AUTRE"));
}
// ── État charge ──────────────────────────────────────────────────────
@Test
@DisplayName("isCharge — false par défaut, true quand snapshot présent")
void isCharge() throws Exception {
assertFalse(bean.isCharge());
setSnapshot(KpiPublicSnapshot.builder().scoreGlobal(75).build());
assertTrue(bean.isCharge());
}
}

View File

@@ -0,0 +1,36 @@
package dev.lions.unionflow.client.view;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
class RoleDelegationBeanTest {
private RoleDelegationBean bean;
@BeforeEach
void setUp() {
bean = new RoleDelegationBean();
}
@Test
@DisplayName("getCouleurStatut — ACTIVE/REVOQUEE/EXPIREE/null/inconnu")
void couleur() {
assertEquals("success", bean.getCouleurStatut("ACTIVE"));
assertEquals("danger", bean.getCouleurStatut("REVOQUEE"));
assertEquals("warning", bean.getCouleurStatut("EXPIREE"));
assertEquals("secondary", bean.getCouleurStatut(null));
assertEquals("secondary", bean.getCouleurStatut("AUTRE"));
}
@Test
@DisplayName("Defaults — rôle TRESORIER, dates initialisées")
void defaults() {
assertEquals("TRESORIER", bean.getRoleDelegue());
assertNotNull(bean.getDateDebut());
assertNotNull(bean.getDateFin());
}
}