Compare commits
5 Commits
51265fb0fa
...
9e23db1728
| Author | SHA1 | Date | |
|---|---|---|---|
| 9e23db1728 | |||
| 9a41b4ca17 | |||
| 106e8f7c88 | |||
| ac4146132b | |||
|
|
f0959abd75 |
76
.gitea/workflows/ci.yml
Normal file
76
.gitea/workflows/ci.yml
Normal file
@@ -0,0 +1,76 @@
|
||||
# ============================================================================
|
||||
# Template — .gitea/workflows/ci.yml
|
||||
# Drop this file into each app repo (adjust LIONS_JAVA_VERSION +
|
||||
# LIONS_APP_NAME + optional --deploy-repo-url). It runs inside the
|
||||
# registry.lions.dev/lionsdev/lionsctl-ci:latest image, so lionsctl,
|
||||
# kubectl, helm, docker CLI, JDK 17+21 and Maven are all pre-installed.
|
||||
#
|
||||
# Required Gitea repo secrets:
|
||||
# LIONS_REGISTRY_USERNAME (typically "lionsregistry")
|
||||
# LIONS_REGISTRY_PASSWORD
|
||||
# LIONS_GIT_USERNAME (typically "lionsdev")
|
||||
# LIONS_GIT_ACCESS_TOKEN (Gitea PAT with write:repository, write:package)
|
||||
# LIONS_GIT_PASSWORD (Gitea password for same user — Helm mode)
|
||||
# SMTP_HOST SMTP_PORT SMTP_USERNAME SMTP_PASSWORD SMTP_FROM
|
||||
# ============================================================================
|
||||
name: CI/CD Pipeline
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
workflow_dispatch: {}
|
||||
|
||||
env:
|
||||
# Adjust per repo:
|
||||
# - unionflow-server-impl-quarkus -> 21
|
||||
# - all others -> 17
|
||||
LIONS_JAVA_VERSION: "17"
|
||||
LIONS_CLUSTER: "k1"
|
||||
|
||||
jobs:
|
||||
pipeline:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: registry.lions.dev/lionsdev/lionsctl-ci:latest
|
||||
credentials:
|
||||
username: ${{ secrets.LIONS_REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.LIONS_REGISTRY_PASSWORD }}
|
||||
# Mount the host docker socket so `docker build/push` inside the
|
||||
# container hits the runner's daemon (DinD-free).
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
|
||||
steps:
|
||||
- name: Show tooling
|
||||
run: |
|
||||
lionsctl --version || true
|
||||
docker --version
|
||||
kubectl version --client=true
|
||||
helm version --short
|
||||
mvn --version | head -n2
|
||||
|
||||
- name: Pipeline deploy
|
||||
env:
|
||||
LIONS_REGISTRY_USERNAME: ${{ secrets.LIONS_REGISTRY_USERNAME }}
|
||||
LIONS_REGISTRY_PASSWORD: ${{ secrets.LIONS_REGISTRY_PASSWORD }}
|
||||
LIONS_GIT_USERNAME: ${{ secrets.LIONS_GIT_USERNAME }}
|
||||
LIONS_GIT_ACCESS_TOKEN: ${{ secrets.LIONS_GIT_ACCESS_TOKEN }}
|
||||
LIONS_GIT_PASSWORD: ${{ secrets.LIONS_GIT_PASSWORD }}
|
||||
SMTP_HOST: ${{ secrets.SMTP_HOST }}
|
||||
SMTP_PORT: ${{ secrets.SMTP_PORT }}
|
||||
SMTP_USERNAME: ${{ secrets.SMTP_USERNAME }}
|
||||
SMTP_PASSWORD: ${{ secrets.SMTP_PASSWORD }}
|
||||
SMTP_FROM: ${{ secrets.SMTP_FROM }}
|
||||
# No actions/checkout — lionsctl clones internally using git_access_token.
|
||||
run: |
|
||||
# For btpxpress-backend add: --deploy-repo-url https://git.lions.dev/lionsdev/btpxpress-server-k1
|
||||
# For btpxpress-frontend add: --deploy-repo-url https://git.lions.dev/lionsdev/btpxpress-client-k1
|
||||
lionsctl pipeline \
|
||||
-u ${{ gitea.server_url }}/${{ gitea.repository }} \
|
||||
-b ${{ gitea.ref_name }} \
|
||||
-j ${{ env.LIONS_JAVA_VERSION }} \
|
||||
-e production \
|
||||
-c ${{ env.LIONS_CLUSTER }} \
|
||||
-p prod \
|
||||
--deploy-repo-url https://git.lions.dev/lionsdev/lionsdev-client-impl-quarkus-k1 \
|
||||
-m admin@lions.dev
|
||||
22
Dockerfile
Normal file
22
Dockerfile
Normal file
@@ -0,0 +1,22 @@
|
||||
# Dockerfile for lionsdev-client-impl-quarkus
|
||||
# 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" ]
|
||||
34
pom.xml
34
pom.xml
@@ -14,9 +14,9 @@
|
||||
<!-- Versions -->
|
||||
<compiler-plugin.version>3.13.0</compiler-plugin.version>
|
||||
<maven.compiler.release>17</maven.compiler.release>
|
||||
<myfaces.version>4.0.1</myfaces.version>
|
||||
<primefaces.version>13.0.5</primefaces.version>
|
||||
<quarkus.platform.version>3.7.3</quarkus.platform.version>
|
||||
<myfaces.version>4.0.2</myfaces.version>
|
||||
<primefaces.version>14.0.0</primefaces.version>
|
||||
<quarkus.platform.version>3.27.3</quarkus.platform.version>
|
||||
<lombok.version>1.18.32</lombok.version>
|
||||
<jackson.version>2.17.0</jackson.version>
|
||||
|
||||
@@ -85,12 +85,23 @@
|
||||
<dependency>
|
||||
<groupId>io.quarkiverse.primefaces</groupId>
|
||||
<artifactId>quarkus-primefaces</artifactId>
|
||||
<version>3.14.0</version>
|
||||
<version>3.15.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.primefaces</groupId>
|
||||
<artifactId>primefaces</artifactId>
|
||||
<version>14.0.0</version>
|
||||
<classifier>jakarta</classifier>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.myfaces.core.extensions.quarkus</groupId>
|
||||
<artifactId>myfaces-quarkus</artifactId>
|
||||
<version>4.0.1</version>
|
||||
<version>4.0.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>dev.lions</groupId>
|
||||
<artifactId>primefaces-freya-extension</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Persistence -->
|
||||
@@ -123,6 +134,13 @@
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-mailer</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- PDF Generation -->
|
||||
<dependency>
|
||||
<groupId>com.itextpdf</groupId>
|
||||
<artifactId>itextpdf</artifactId>
|
||||
<version>5.5.13.3</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-websockets</artifactId>
|
||||
@@ -197,7 +215,11 @@
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-resteasy-reactive</artifactId>
|
||||
<artifactId>quarkus-rest</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-rest-jackson</artifactId>
|
||||
</dependency>
|
||||
<!-- Sécurité -->
|
||||
<dependency>
|
||||
|
||||
393
src/main/java/dev/lions/audit/AuditDataInitializer.java
Normal file
393
src/main/java/dev/lions/audit/AuditDataInitializer.java
Normal file
@@ -0,0 +1,393 @@
|
||||
package dev.lions.audit;
|
||||
|
||||
import dev.lions.quote.ModuleCatalog;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.enterprise.event.Observes;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.transaction.Transactional;
|
||||
import io.quarkus.runtime.StartupEvent;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Initialisation des questions d'audit spécifiques aux PME ivoiriennes
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class AuditDataInitializer {
|
||||
|
||||
@Inject
|
||||
EntityManager em;
|
||||
|
||||
@Transactional
|
||||
public void initializeAuditQuestions(@Observes StartupEvent event) {
|
||||
// Vérifier si les questions existent déjà
|
||||
Long questionCount = em.createQuery("SELECT COUNT(q) FROM AuditQuestion q", Long.class)
|
||||
.getSingleResult();
|
||||
|
||||
if (questionCount == 0) {
|
||||
createCommercialQuestions();
|
||||
createStockQuestions();
|
||||
createComptabiliteQuestions();
|
||||
createRhQuestions();
|
||||
createInfrastructureQuestions();
|
||||
}
|
||||
|
||||
// Vérifier si le catalogue existe déjà
|
||||
Long catalogCount = em.createQuery("SELECT COUNT(m) FROM ModuleCatalog m", Long.class)
|
||||
.getSingleResult();
|
||||
|
||||
if (catalogCount == 0) {
|
||||
createModuleCatalog();
|
||||
}
|
||||
}
|
||||
|
||||
private void createCommercialQuestions() {
|
||||
// Question 1: Gestion des clients
|
||||
AuditQuestion q1 = new AuditQuestion();
|
||||
q1.setCategory("commercial");
|
||||
q1.setQuestion("Comment gérez-vous actuellement vos contacts clients ?");
|
||||
q1.setOptions(Arrays.asList(
|
||||
"Carnet papier ou fichier Excel basique",
|
||||
"Fichier Excel avec historique des contacts",
|
||||
"Logiciel de gestion simple (contacts + ventes)",
|
||||
"CRM complet avec suivi des opportunités",
|
||||
"CRM avancé avec automation marketing"
|
||||
));
|
||||
q1.setScores(Arrays.asList(1, 2, 3, 4, 5));
|
||||
q1.setWeight(3);
|
||||
q1.setDisplayOrder(1);
|
||||
q1.setRecommendation("Un CRM adapté améliore la relation client et augmente les ventes de 20-30%");
|
||||
em.persist(q1);
|
||||
|
||||
// Question 2: Facturation
|
||||
AuditQuestion q2 = new AuditQuestion();
|
||||
q2.setCategory("commercial");
|
||||
q2.setQuestion("Comment établissez-vous vos factures ?");
|
||||
q2.setOptions(Arrays.asList(
|
||||
"Factures manuscrites ou Word/Excel",
|
||||
"Modèle Excel avec calculs automatiques",
|
||||
"Logiciel de facturation simple",
|
||||
"Système intégré (devis → facture → paiement)",
|
||||
"Facturation automatisée avec relances"
|
||||
));
|
||||
q2.setScores(Arrays.asList(1, 2, 3, 4, 5));
|
||||
q2.setWeight(3);
|
||||
q2.setDisplayOrder(2);
|
||||
q2.setRecommendation("La facturation automatisée réduit les erreurs et accélère les paiements");
|
||||
em.persist(q2);
|
||||
|
||||
// Question 3: Suivi des ventes
|
||||
AuditQuestion q3 = new AuditQuestion();
|
||||
q3.setCategory("commercial");
|
||||
q3.setQuestion("Avez-vous une visibilité sur vos performances commerciales ?");
|
||||
q3.setOptions(Arrays.asList(
|
||||
"Aucun suivi régulier",
|
||||
"Calculs manuels mensuels",
|
||||
"Tableaux de bord Excel",
|
||||
"Rapports automatiques hebdomadaires",
|
||||
"Dashboard temps réel avec KPI"
|
||||
));
|
||||
q3.setScores(Arrays.asList(1, 2, 3, 4, 5));
|
||||
q3.setWeight(2);
|
||||
q3.setDisplayOrder(3);
|
||||
em.persist(q3);
|
||||
|
||||
// Question 4: Gestion des devis
|
||||
AuditQuestion q4 = new AuditQuestion();
|
||||
q4.setCategory("commercial");
|
||||
q4.setQuestion("Comment gérez-vous vos devis et propositions commerciales ?");
|
||||
q4.setOptions(Arrays.asList(
|
||||
"Devis manuscrits ou Word",
|
||||
"Modèles Excel personnalisables",
|
||||
"Logiciel de devis avec bibliothèque",
|
||||
"Système intégré avec signature électronique",
|
||||
"Configurateur automatique avec approbation workflow"
|
||||
));
|
||||
q4.setScores(Arrays.asList(1, 2, 3, 4, 5));
|
||||
q4.setWeight(2);
|
||||
q4.setDisplayOrder(4);
|
||||
em.persist(q4);
|
||||
}
|
||||
|
||||
private void createStockQuestions() {
|
||||
// Question 1: Inventaire
|
||||
AuditQuestion q1 = new AuditQuestion();
|
||||
q1.setCategory("stock");
|
||||
q1.setQuestion("Comment suivez-vous vos stocks ?");
|
||||
q1.setOptions(Arrays.asList(
|
||||
"Comptage manuel périodique",
|
||||
"Fichier Excel mis à jour manuellement",
|
||||
"Logiciel de stock simple",
|
||||
"Système avec codes-barres",
|
||||
"Gestion automatisée avec alertes"
|
||||
));
|
||||
q1.setScores(Arrays.asList(1, 2, 3, 4, 5));
|
||||
q1.setWeight(3);
|
||||
q1.setDisplayOrder(1);
|
||||
em.persist(q1);
|
||||
|
||||
// Question 2: Approvisionnement
|
||||
AuditQuestion q2 = new AuditQuestion();
|
||||
q2.setCategory("stock");
|
||||
q2.setQuestion("Comment planifiez-vous vos achats et approvisionnements ?");
|
||||
q2.setOptions(Arrays.asList(
|
||||
"Achats au feeling quand stock bas",
|
||||
"Planning mensuel basique",
|
||||
"Calculs de stock minimum",
|
||||
"Prévisions basées sur l'historique",
|
||||
"Optimisation automatique avec IA"
|
||||
));
|
||||
q2.setScores(Arrays.asList(1, 2, 3, 4, 5));
|
||||
q2.setWeight(2);
|
||||
q2.setDisplayOrder(2);
|
||||
em.persist(q2);
|
||||
|
||||
// Question 3: Valorisation
|
||||
AuditQuestion q3 = new AuditQuestion();
|
||||
q3.setCategory("stock");
|
||||
q3.setQuestion("Connaissez-vous la valeur exacte de votre stock ?");
|
||||
q3.setOptions(Arrays.asList(
|
||||
"Estimation approximative",
|
||||
"Calcul manuel périodique",
|
||||
"Valorisation Excel",
|
||||
"Valorisation automatique temps réel",
|
||||
"Analyse de rotation et obsolescence"
|
||||
));
|
||||
q3.setScores(Arrays.asList(1, 2, 3, 4, 5));
|
||||
q3.setWeight(2);
|
||||
q3.setDisplayOrder(3);
|
||||
em.persist(q3);
|
||||
}
|
||||
|
||||
private void createComptabiliteQuestions() {
|
||||
// Question 1: Tenue comptable
|
||||
AuditQuestion q1 = new AuditQuestion();
|
||||
q1.setCategory("comptabilite");
|
||||
q1.setQuestion("Comment tenez-vous votre comptabilité ?");
|
||||
q1.setOptions(Arrays.asList(
|
||||
"Cahiers manuscrits",
|
||||
"Excel avec saisie manuelle",
|
||||
"Logiciel comptable simple",
|
||||
"Logiciel intégré avec automatisation",
|
||||
"ERP complet avec expert-comptable connecté"
|
||||
));
|
||||
q1.setScores(Arrays.asList(1, 2, 3, 4, 5));
|
||||
q1.setWeight(3);
|
||||
q1.setDisplayOrder(1);
|
||||
em.persist(q1);
|
||||
|
||||
// Question 2: Déclarations fiscales
|
||||
AuditQuestion q2 = new AuditQuestion();
|
||||
q2.setCategory("comptabilite");
|
||||
q2.setQuestion("Comment gérez-vous vos obligations fiscales (TVA, IS, etc.) ?");
|
||||
q2.setOptions(Arrays.asList(
|
||||
"Calculs manuels avec expert-comptable",
|
||||
"Excel avec formules de calcul",
|
||||
"Logiciel avec aide au calcul",
|
||||
"Génération automatique des déclarations",
|
||||
"Télédéclaration automatisée"
|
||||
));
|
||||
q2.setScores(Arrays.asList(1, 2, 3, 4, 5));
|
||||
q2.setWeight(3);
|
||||
q2.setDisplayOrder(2);
|
||||
em.persist(q2);
|
||||
|
||||
// Question 3: Analyse financière
|
||||
AuditQuestion q3 = new AuditQuestion();
|
||||
q3.setCategory("comptabilite");
|
||||
q3.setQuestion("Avez-vous une vision claire de votre situation financière ?");
|
||||
q3.setOptions(Arrays.asList(
|
||||
"Bilan annuel uniquement",
|
||||
"Situation trimestrielle",
|
||||
"Tableaux de bord mensuels",
|
||||
"Reporting automatique hebdomadaire",
|
||||
"Dashboard temps réel avec prévisions"
|
||||
));
|
||||
q3.setScores(Arrays.asList(1, 2, 3, 4, 5));
|
||||
q3.setWeight(2);
|
||||
q3.setDisplayOrder(3);
|
||||
em.persist(q3);
|
||||
}
|
||||
|
||||
private void createRhQuestions() {
|
||||
// Question 1: Gestion du personnel
|
||||
AuditQuestion q1 = new AuditQuestion();
|
||||
q1.setCategory("rh");
|
||||
q1.setQuestion("Comment gérez-vous les dossiers de vos employés ?");
|
||||
q1.setOptions(Arrays.asList(
|
||||
"Dossiers papier classés",
|
||||
"Fichiers Excel par employé",
|
||||
"Logiciel RH basique",
|
||||
"SIRH avec self-service employé",
|
||||
"SIRH complet avec workflow"
|
||||
));
|
||||
q1.setScores(Arrays.asList(1, 2, 3, 4, 5));
|
||||
q1.setWeight(2);
|
||||
q1.setDisplayOrder(1);
|
||||
em.persist(q1);
|
||||
|
||||
// Question 2: Paie et CNPS
|
||||
AuditQuestion q2 = new AuditQuestion();
|
||||
q2.setCategory("rh");
|
||||
q2.setQuestion("Comment calculez-vous la paie et les cotisations CNPS ?");
|
||||
q2.setOptions(Arrays.asList(
|
||||
"Calculs manuels",
|
||||
"Excel avec formules",
|
||||
"Logiciel de paie simple",
|
||||
"Paie automatisée avec déclarations",
|
||||
"Intégration complète CNPS/DGI"
|
||||
));
|
||||
q2.setScores(Arrays.asList(1, 2, 3, 4, 5));
|
||||
q2.setWeight(3);
|
||||
q2.setDisplayOrder(2);
|
||||
em.persist(q2);
|
||||
|
||||
// Question 3: Gestion des congés
|
||||
AuditQuestion q3 = new AuditQuestion();
|
||||
q3.setCategory("rh");
|
||||
q3.setQuestion("Comment gérez-vous les demandes de congés ?");
|
||||
q3.setOptions(Arrays.asList(
|
||||
"Demandes papier",
|
||||
"Email et validation manuelle",
|
||||
"Fichier Excel partagé",
|
||||
"Système de workflow digital",
|
||||
"Application mobile avec approbation"
|
||||
));
|
||||
q3.setScores(Arrays.asList(1, 2, 3, 4, 5));
|
||||
q3.setWeight(1);
|
||||
q3.setDisplayOrder(3);
|
||||
em.persist(q3);
|
||||
}
|
||||
|
||||
private void createInfrastructureQuestions() {
|
||||
// Question 1: Équipement informatique
|
||||
AuditQuestion q1 = new AuditQuestion();
|
||||
q1.setCategory("infrastructure");
|
||||
q1.setQuestion("Quel est l'état de votre parc informatique ?");
|
||||
q1.setOptions(Arrays.asList(
|
||||
"Ordinateurs anciens (>5 ans), pas de réseau",
|
||||
"Mix d'ordinateurs, réseau basique",
|
||||
"Parc récent, réseau WiFi, serveur local",
|
||||
"Infrastructure moderne, cloud hybride",
|
||||
"Infrastructure cloud-native sécurisée"
|
||||
));
|
||||
q1.setScores(Arrays.asList(1, 2, 3, 4, 5));
|
||||
q1.setWeight(2);
|
||||
q1.setDisplayOrder(1);
|
||||
em.persist(q1);
|
||||
|
||||
// Question 2: Sauvegardes
|
||||
AuditQuestion q2 = new AuditQuestion();
|
||||
q2.setCategory("infrastructure");
|
||||
q2.setQuestion("Comment protégez-vous vos données importantes ?");
|
||||
q2.setOptions(Arrays.asList(
|
||||
"Aucune sauvegarde régulière",
|
||||
"Copies manuelles sur clé USB",
|
||||
"Sauvegarde externe périodique",
|
||||
"Sauvegarde automatique cloud",
|
||||
"Sauvegarde redondante avec plan de reprise"
|
||||
));
|
||||
q2.setScores(Arrays.asList(1, 2, 3, 4, 5));
|
||||
q2.setWeight(3);
|
||||
q2.setDisplayOrder(2);
|
||||
em.persist(q2);
|
||||
|
||||
// Question 3: Sécurité
|
||||
AuditQuestion q3 = new AuditQuestion();
|
||||
q3.setCategory("infrastructure");
|
||||
q3.setQuestion("Quelles mesures de sécurité informatique avez-vous ?");
|
||||
q3.setOptions(Arrays.asList(
|
||||
"Antivirus basique uniquement",
|
||||
"Antivirus + mots de passe",
|
||||
"Sécurité réseau + formation utilisateurs",
|
||||
"Sécurité multicouche + monitoring",
|
||||
"Sécurité enterprise avec audit régulier"
|
||||
));
|
||||
q3.setScores(Arrays.asList(1, 2, 3, 4, 5));
|
||||
q3.setWeight(3);
|
||||
q3.setDisplayOrder(3);
|
||||
em.persist(q3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée le catalogue des modules avec tarification
|
||||
*/
|
||||
private void createModuleCatalog() {
|
||||
// Module CRM Commercial
|
||||
ModuleCatalog crmModule = new ModuleCatalog("CRM", "Gestion Commerciale CRM", "commercial");
|
||||
crmModule.setDescription("Solution complète de gestion de la relation client avec suivi des prospects, opportunités et ventes");
|
||||
crmModule.setFeatures("• Gestion contacts et prospects\n• Pipeline de ventes\n• Suivi des opportunités\n• Facturation intégrée\n• Rapports commerciaux\n• Tableaux de bord");
|
||||
crmModule.setBasicPrice(150000.0); // 150K FCFA
|
||||
crmModule.setStandardPrice(250000.0); // 250K FCFA
|
||||
crmModule.setAdvancedPrice(400000.0); // 400K FCFA
|
||||
crmModule.setEnterprisePrice(650000.0); // 650K FCFA
|
||||
crmModule.setBaseImplementationDays(10);
|
||||
crmModule.setMaxUsers(50);
|
||||
crmModule.setSupportLevel("Email + Téléphone");
|
||||
crmModule.setTechnicalRequirements("Windows/Mac/Linux, Navigateur web moderne, 4GB RAM minimum");
|
||||
crmModule.setDisplayOrder(1);
|
||||
crmModule.setPopular(true);
|
||||
em.persist(crmModule);
|
||||
|
||||
// Module Gestion de Stock
|
||||
ModuleCatalog stockModule = new ModuleCatalog("STOCK", "Gestion des Stocks", "stock");
|
||||
stockModule.setDescription("Système de gestion des stocks avec codes-barres, inventaires et approvisionnements automatisés");
|
||||
stockModule.setFeatures("• Gestion multi-entrepôts\n• Codes-barres et QR codes\n• Inventaires automatisés\n• Alertes stock minimum\n• Prévisions d'achat\n• Valorisation FIFO/LIFO");
|
||||
stockModule.setBasicPrice(120000.0);
|
||||
stockModule.setStandardPrice(200000.0);
|
||||
stockModule.setAdvancedPrice(350000.0);
|
||||
stockModule.setEnterprisePrice(550000.0);
|
||||
stockModule.setBaseImplementationDays(8);
|
||||
stockModule.setMaxUsers(20);
|
||||
stockModule.setSupportLevel("Email + Téléphone");
|
||||
stockModule.setTechnicalRequirements("Windows/Mac/Linux, Lecteur codes-barres (optionnel), Imprimante étiquettes");
|
||||
stockModule.setDisplayOrder(2);
|
||||
em.persist(stockModule);
|
||||
|
||||
// Module Comptabilité
|
||||
ModuleCatalog comptaModule = new ModuleCatalog("COMPTA", "Comptabilité Intégrée", "comptabilite");
|
||||
comptaModule.setDescription("Comptabilité complète conforme aux normes ivoiriennes avec déclarations fiscales automatisées");
|
||||
comptaModule.setFeatures("• Plan comptable SYSCOHADA\n• Saisie automatisée\n• Déclarations TVA/IS\n• Bilan et compte de résultat\n• Rapports DGI\n• Intégration bancaire");
|
||||
comptaModule.setBasicPrice(180000.0);
|
||||
comptaModule.setStandardPrice(300000.0);
|
||||
comptaModule.setAdvancedPrice(500000.0);
|
||||
comptaModule.setEnterprisePrice(800000.0);
|
||||
comptaModule.setBaseImplementationDays(12);
|
||||
comptaModule.setMaxUsers(10);
|
||||
comptaModule.setSupportLevel("Email + Téléphone + Expert-comptable");
|
||||
comptaModule.setTechnicalRequirements("Windows/Mac/Linux, Connexion internet sécurisée, Sauvegarde automatique");
|
||||
comptaModule.setDisplayOrder(3);
|
||||
comptaModule.setPopular(true);
|
||||
em.persist(comptaModule);
|
||||
|
||||
// Module RH
|
||||
ModuleCatalog rhModule = new ModuleCatalog("RH", "Gestion des Ressources Humaines", "rh");
|
||||
rhModule.setDescription("SIRH complet avec paie, congés, formation et conformité CNPS");
|
||||
rhModule.setFeatures("• Dossiers employés\n• Calcul de paie CNPS\n• Gestion des congés\n• Formation et évaluation\n• Déclarations sociales\n• Self-service employé");
|
||||
rhModule.setBasicPrice(100000.0);
|
||||
rhModule.setStandardPrice(180000.0);
|
||||
rhModule.setAdvancedPrice(320000.0);
|
||||
rhModule.setEnterprisePrice(500000.0);
|
||||
rhModule.setBaseImplementationDays(6);
|
||||
rhModule.setMaxUsers(100);
|
||||
rhModule.setSupportLevel("Email + Téléphone");
|
||||
rhModule.setTechnicalRequirements("Windows/Mac/Linux, Connexion CNPS (optionnelle), Scanner documents");
|
||||
rhModule.setDisplayOrder(4);
|
||||
em.persist(rhModule);
|
||||
|
||||
// Module Infrastructure
|
||||
ModuleCatalog infraModule = new ModuleCatalog("INFRA", "Infrastructure IT", "infrastructure");
|
||||
infraModule.setDescription("Mise en place et sécurisation de l'infrastructure informatique avec sauvegarde cloud");
|
||||
infraModule.setFeatures("• Audit infrastructure\n• Sécurisation réseau\n• Sauvegarde automatique\n• Antivirus enterprise\n• Monitoring 24/7\n• Support technique");
|
||||
infraModule.setBasicPrice(200000.0);
|
||||
infraModule.setStandardPrice(350000.0);
|
||||
infraModule.setAdvancedPrice(600000.0);
|
||||
infraModule.setEnterprisePrice(1000000.0);
|
||||
infraModule.setBaseImplementationDays(5);
|
||||
infraModule.setMaxUsers(999);
|
||||
infraModule.setSupportLevel("Email + Téléphone + Intervention sur site");
|
||||
infraModule.setTechnicalRequirements("Réseau existant, Serveur ou Cloud, Postes de travail Windows/Mac");
|
||||
infraModule.setDisplayOrder(5);
|
||||
em.persist(infraModule);
|
||||
}
|
||||
}
|
||||
86
src/main/java/dev/lions/audit/AuditQuestion.java
Normal file
86
src/main/java/dev/lions/audit/AuditQuestion.java
Normal file
@@ -0,0 +1,86 @@
|
||||
package dev.lions.audit;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Question d'audit pour évaluer la maturité digitale des PME
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "audit_questions")
|
||||
public class AuditQuestion {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String category; // "commercial", "stock", "comptabilite", "rh", "infrastructure"
|
||||
|
||||
@Column(nullable = false, length = 500)
|
||||
private String question;
|
||||
|
||||
@Column(name = "question_en", length = 500)
|
||||
private String questionEn; // Version anglaise
|
||||
|
||||
@ElementCollection
|
||||
@CollectionTable(name = "audit_question_options")
|
||||
private List<String> options;
|
||||
|
||||
@ElementCollection
|
||||
@CollectionTable(name = "audit_question_scores")
|
||||
private List<Integer> scores; // Score pour chaque option
|
||||
|
||||
@Column(nullable = false)
|
||||
private Integer weight = 1; // Poids de la question
|
||||
|
||||
@Column(length = 1000)
|
||||
private String recommendation; // Recommandation selon la réponse
|
||||
|
||||
@Column(nullable = false)
|
||||
private Boolean active = true;
|
||||
|
||||
@Column(name = "display_order")
|
||||
private Integer displayOrder;
|
||||
|
||||
// Constructeurs
|
||||
public AuditQuestion() {}
|
||||
|
||||
public AuditQuestion(String category, String question, List<String> options, List<Integer> scores) {
|
||||
this.category = category;
|
||||
this.question = question;
|
||||
this.options = options;
|
||||
this.scores = scores;
|
||||
}
|
||||
|
||||
// Getters et Setters
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public String getCategory() { return category; }
|
||||
public void setCategory(String category) { this.category = category; }
|
||||
|
||||
public String getQuestion() { return question; }
|
||||
public void setQuestion(String question) { this.question = question; }
|
||||
|
||||
public String getQuestionEn() { return questionEn; }
|
||||
public void setQuestionEn(String questionEn) { this.questionEn = questionEn; }
|
||||
|
||||
public List<String> getOptions() { return options; }
|
||||
public void setOptions(List<String> options) { this.options = options; }
|
||||
|
||||
public List<Integer> getScores() { return scores; }
|
||||
public void setScores(List<Integer> scores) { this.scores = scores; }
|
||||
|
||||
public Integer getWeight() { return weight; }
|
||||
public void setWeight(Integer weight) { this.weight = weight; }
|
||||
|
||||
public String getRecommendation() { return recommendation; }
|
||||
public void setRecommendation(String recommendation) { this.recommendation = recommendation; }
|
||||
|
||||
public Boolean getActive() { return active; }
|
||||
public void setActive(Boolean active) { this.active = active; }
|
||||
|
||||
public Integer getDisplayOrder() { return displayOrder; }
|
||||
public void setDisplayOrder(Integer displayOrder) { this.displayOrder = displayOrder; }
|
||||
}
|
||||
305
src/main/java/dev/lions/audit/AuditReportService.java
Normal file
305
src/main/java/dev/lions/audit/AuditReportService.java
Normal file
@@ -0,0 +1,305 @@
|
||||
package dev.lions.audit;
|
||||
|
||||
import com.itextpdf.text.*;
|
||||
import com.itextpdf.text.pdf.*;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import io.quarkus.mailer.Mail;
|
||||
import io.quarkus.mailer.Mailer;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Service de génération de rapports PDF d'audit
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class AuditReportService {
|
||||
|
||||
@Inject
|
||||
Mailer mailer;
|
||||
|
||||
private static final Font TITLE_FONT = new Font(Font.FontFamily.HELVETICA, 18, Font.BOLD, BaseColor.DARK_GRAY);
|
||||
private static final Font HEADER_FONT = new Font(Font.FontFamily.HELVETICA, 14, Font.BOLD, BaseColor.BLACK);
|
||||
private static final Font NORMAL_FONT = new Font(Font.FontFamily.HELVETICA, 11, Font.NORMAL, BaseColor.BLACK);
|
||||
private static final Font SMALL_FONT = new Font(Font.FontFamily.HELVETICA, 9, Font.NORMAL, BaseColor.GRAY);
|
||||
|
||||
/**
|
||||
* Génère le rapport PDF d'audit
|
||||
*/
|
||||
public byte[] generateAuditReport(AuditResponse audit) throws Exception {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
Document document = new Document(PageSize.A4, 50, 50, 50, 50);
|
||||
PdfWriter writer = PdfWriter.getInstance(document, baos);
|
||||
|
||||
document.open();
|
||||
|
||||
// En-tête Lions Dev
|
||||
addHeader(document);
|
||||
|
||||
// Informations entreprise
|
||||
addCompanyInfo(document, audit);
|
||||
|
||||
// Résumé exécutif
|
||||
addExecutiveSummary(document, audit);
|
||||
|
||||
// Scores par catégorie
|
||||
addCategoryScores(document, audit);
|
||||
|
||||
// Recommandations détaillées
|
||||
addRecommendations(document, audit);
|
||||
|
||||
// Estimation budgétaire
|
||||
addBudgetEstimation(document, audit);
|
||||
|
||||
// Prochaines étapes
|
||||
addNextSteps(document);
|
||||
|
||||
// Pied de page
|
||||
addFooter(document);
|
||||
|
||||
document.close();
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
private void addHeader(Document document) throws DocumentException {
|
||||
// Logo et titre Lions Dev
|
||||
Paragraph title = new Paragraph("LIONS DEV", TITLE_FONT);
|
||||
title.setAlignment(Element.ALIGN_CENTER);
|
||||
title.setSpacingAfter(10);
|
||||
document.add(title);
|
||||
|
||||
Paragraph subtitle = new Paragraph("Audit de Maturité Digitale", HEADER_FONT);
|
||||
subtitle.setAlignment(Element.ALIGN_CENTER);
|
||||
subtitle.setSpacingAfter(20);
|
||||
document.add(subtitle);
|
||||
|
||||
// Ligne de séparation
|
||||
com.itextpdf.text.pdf.draw.LineSeparator line = new com.itextpdf.text.pdf.draw.LineSeparator();
|
||||
document.add(new Chunk(line));
|
||||
document.add(Chunk.NEWLINE);
|
||||
}
|
||||
|
||||
private void addCompanyInfo(Document document, AuditResponse audit) throws DocumentException {
|
||||
Paragraph section = new Paragraph("INFORMATIONS ENTREPRISE", HEADER_FONT);
|
||||
section.setSpacingBefore(10);
|
||||
section.setSpacingAfter(10);
|
||||
document.add(section);
|
||||
|
||||
PdfPTable table = new PdfPTable(2);
|
||||
table.setWidthPercentage(100);
|
||||
table.setSpacingAfter(15);
|
||||
|
||||
addTableRow(table, "Entreprise:", audit.getCompanyName());
|
||||
addTableRow(table, "Contact:", audit.getContactName());
|
||||
addTableRow(table, "Email:", audit.getEmail());
|
||||
addTableRow(table, "Téléphone:", audit.getPhone());
|
||||
addTableRow(table, "Secteur:", audit.getSector());
|
||||
addTableRow(table, "Employés:", audit.getEmployeeCount() + " personnes");
|
||||
addTableRow(table, "CA annuel:", audit.getTurnover());
|
||||
addTableRow(table, "Date audit:", audit.getSubmittedAt().format(DateTimeFormatter.ofPattern("dd/MM/yyyy")));
|
||||
|
||||
document.add(table);
|
||||
}
|
||||
|
||||
private void addExecutiveSummary(Document document, AuditResponse audit) throws DocumentException {
|
||||
Paragraph section = new Paragraph("RÉSUMÉ EXÉCUTIF", HEADER_FONT);
|
||||
section.setSpacingBefore(15);
|
||||
section.setSpacingAfter(10);
|
||||
document.add(section);
|
||||
|
||||
// Score global avec couleur
|
||||
String maturityLevel = getMaturityLevel(audit.getMaturityPercentage());
|
||||
BaseColor scoreColor = getScoreColor(audit.getMaturityPercentage());
|
||||
|
||||
Paragraph scoreText = new Paragraph();
|
||||
scoreText.add(new Chunk("Score de maturité digitale: ", NORMAL_FONT));
|
||||
scoreText.add(new Chunk(String.format("%.1f%% (%s)", audit.getMaturityPercentage(), maturityLevel),
|
||||
new Font(Font.FontFamily.HELVETICA, 12, Font.BOLD, scoreColor)));
|
||||
scoreText.setSpacingAfter(15);
|
||||
document.add(scoreText);
|
||||
|
||||
// Graphique en barres des scores par catégorie
|
||||
addCategoryChart(document, audit);
|
||||
}
|
||||
|
||||
private void addCategoryScores(Document document, AuditResponse audit) throws DocumentException {
|
||||
Paragraph section = new Paragraph("ANALYSE DÉTAILLÉE PAR DOMAINE", HEADER_FONT);
|
||||
section.setSpacingBefore(15);
|
||||
section.setSpacingAfter(10);
|
||||
document.add(section);
|
||||
|
||||
PdfPTable table = new PdfPTable(3);
|
||||
table.setWidthPercentage(100);
|
||||
table.setWidths(new float[]{3, 1, 2});
|
||||
|
||||
// En-têtes
|
||||
addTableHeader(table, "Domaine");
|
||||
addTableHeader(table, "Score");
|
||||
addTableHeader(table, "Niveau");
|
||||
|
||||
for (Map.Entry<String, Integer> entry : audit.getCategoryScores().entrySet()) {
|
||||
String category = getCategoryDisplayName(entry.getKey());
|
||||
Integer score = entry.getValue();
|
||||
// Calcul du pourcentage (approximatif)
|
||||
double percentage = (double) score / 100 * 100; // À ajuster selon le scoring réel
|
||||
String level = getMaturityLevel(percentage);
|
||||
|
||||
addTableRow(table, category, score.toString() + " - " + level);
|
||||
}
|
||||
|
||||
document.add(table);
|
||||
}
|
||||
|
||||
private void addRecommendations(Document document, AuditResponse audit) throws DocumentException {
|
||||
Paragraph section = new Paragraph("RECOMMANDATIONS PRIORITAIRES", HEADER_FONT);
|
||||
section.setSpacingBefore(15);
|
||||
section.setSpacingAfter(10);
|
||||
document.add(section);
|
||||
|
||||
Paragraph recommendations = new Paragraph(audit.getRecommendations(), NORMAL_FONT);
|
||||
recommendations.setSpacingAfter(10);
|
||||
document.add(recommendations);
|
||||
|
||||
if (audit.getPriorityActions() != null && !audit.getPriorityActions().isEmpty()) {
|
||||
Paragraph actions = new Paragraph("Actions prioritaires: " + audit.getPriorityActions(), NORMAL_FONT);
|
||||
actions.setSpacingAfter(15);
|
||||
document.add(actions);
|
||||
}
|
||||
}
|
||||
|
||||
private void addBudgetEstimation(Document document, AuditResponse audit) throws DocumentException {
|
||||
if (audit.getEstimatedBudgetMin() != null && audit.getEstimatedBudgetMax() != null) {
|
||||
Paragraph section = new Paragraph("ESTIMATION BUDGÉTAIRE", HEADER_FONT);
|
||||
section.setSpacingBefore(15);
|
||||
section.setSpacingAfter(10);
|
||||
document.add(section);
|
||||
|
||||
String budgetText = String.format("Investissement estimé pour la digitalisation: %,.0f - %,.0f FCFA",
|
||||
audit.getEstimatedBudgetMin(), audit.getEstimatedBudgetMax());
|
||||
Paragraph budget = new Paragraph(budgetText, NORMAL_FONT);
|
||||
budget.setSpacingAfter(10);
|
||||
document.add(budget);
|
||||
|
||||
Paragraph note = new Paragraph("* Estimation basée sur votre niveau de maturité actuel. " +
|
||||
"Un devis personnalisé sera établi après analyse détaillée.", SMALL_FONT);
|
||||
note.setSpacingAfter(15);
|
||||
document.add(note);
|
||||
}
|
||||
}
|
||||
|
||||
private void addNextSteps(Document document) throws DocumentException {
|
||||
Paragraph section = new Paragraph("PROCHAINES ÉTAPES", HEADER_FONT);
|
||||
section.setSpacingBefore(15);
|
||||
section.setSpacingAfter(10);
|
||||
document.add(section);
|
||||
|
||||
List list = new List(List.ORDERED);
|
||||
list.add(new ListItem("Rendez-vous diagnostic approfondi (gratuit)", NORMAL_FONT));
|
||||
list.add(new ListItem("Analyse détaillée de vos processus métier", NORMAL_FONT));
|
||||
list.add(new ListItem("Proposition de solution personnalisée", NORMAL_FONT));
|
||||
list.add(new ListItem("Planification du déploiement", NORMAL_FONT));
|
||||
list.add(new ListItem("Formation de vos équipes", NORMAL_FONT));
|
||||
|
||||
document.add(list);
|
||||
}
|
||||
|
||||
private void addFooter(Document document) throws DocumentException {
|
||||
Paragraph footer = new Paragraph("\nLions Dev - Solutions Digitales Innovantes\n" +
|
||||
"Abidjan, Côte d'Ivoire | +225 01 01 75 95 25 | contact@lions.dev", SMALL_FONT);
|
||||
footer.setAlignment(Element.ALIGN_CENTER);
|
||||
footer.setSpacingBefore(20);
|
||||
document.add(footer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Envoie le rapport par email
|
||||
*/
|
||||
public void sendAuditReportByEmail(AuditResponse audit, byte[] pdfReport) {
|
||||
try {
|
||||
Mail mail = Mail.withHtml(audit.getEmail(),
|
||||
"Votre Audit de Maturité Digitale - Lions Dev",
|
||||
generateEmailContent(audit))
|
||||
.addAttachment("audit-" + audit.getCompanyName() + ".pdf",
|
||||
pdfReport, "application/pdf");
|
||||
|
||||
mailer.send(mail);
|
||||
} catch (Exception e) {
|
||||
// Log l'erreur mais ne fait pas échouer le processus
|
||||
System.err.println("Erreur envoi email: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private String generateEmailContent(AuditResponse audit) {
|
||||
return String.format("""
|
||||
<h2>Bonjour %s,</h2>
|
||||
|
||||
<p>Merci d'avoir réalisé l'audit de maturité digitale avec Lions Dev.</p>
|
||||
|
||||
<p><strong>Votre score global: %.1f%%</strong></p>
|
||||
|
||||
<p>Vous trouverez en pièce jointe votre rapport détaillé avec nos recommandations personnalisées.</p>
|
||||
|
||||
<p>Notre équipe vous contactera dans les 24h pour planifier un rendez-vous diagnostic gratuit.</p>
|
||||
|
||||
<p>Cordialement,<br>
|
||||
L'équipe Lions Dev<br>
|
||||
+225 01 01 75 95 25</p>
|
||||
""", audit.getContactName(), audit.getMaturityPercentage());
|
||||
}
|
||||
|
||||
// Méthodes utilitaires
|
||||
private void addTableRow(PdfPTable table, String label, String value) {
|
||||
table.addCell(new PdfPCell(new Phrase(label, NORMAL_FONT)));
|
||||
table.addCell(new PdfPCell(new Phrase(value != null ? value : "-", NORMAL_FONT)));
|
||||
}
|
||||
|
||||
private void addTableHeader(PdfPTable table, String header) {
|
||||
PdfPCell cell = new PdfPCell(new Phrase(header, HEADER_FONT));
|
||||
cell.setBackgroundColor(BaseColor.LIGHT_GRAY);
|
||||
table.addCell(cell);
|
||||
}
|
||||
|
||||
private String getMaturityLevel(double percentage) {
|
||||
if (percentage < 30) return "Débutant";
|
||||
if (percentage < 60) return "Intermédiaire";
|
||||
if (percentage < 80) return "Avancé";
|
||||
return "Expert";
|
||||
}
|
||||
|
||||
private BaseColor getScoreColor(double percentage) {
|
||||
if (percentage < 30) return BaseColor.RED;
|
||||
if (percentage < 60) return BaseColor.ORANGE;
|
||||
if (percentage < 80) return BaseColor.BLUE;
|
||||
return BaseColor.GREEN;
|
||||
}
|
||||
|
||||
private String getCategoryDisplayName(String category) {
|
||||
return switch (category) {
|
||||
case "commercial" -> "Gestion Commerciale";
|
||||
case "stock" -> "Gestion des Stocks";
|
||||
case "comptabilite" -> "Comptabilité";
|
||||
case "rh" -> "Ressources Humaines";
|
||||
case "infrastructure" -> "Infrastructure IT";
|
||||
default -> category;
|
||||
};
|
||||
}
|
||||
|
||||
private void addCategoryChart(Document document, AuditResponse audit) throws DocumentException {
|
||||
// Graphique simple en texte (pour une vraie implémentation, utiliser JFreeChart)
|
||||
Paragraph chart = new Paragraph("Répartition des scores par domaine:", NORMAL_FONT);
|
||||
chart.setSpacingAfter(5);
|
||||
document.add(chart);
|
||||
|
||||
for (Map.Entry<String, Integer> entry : audit.getCategoryScores().entrySet()) {
|
||||
String category = getCategoryDisplayName(entry.getKey());
|
||||
Integer score = entry.getValue();
|
||||
String bar = "█".repeat(Math.max(1, score / 10)) + " " + score + "%";
|
||||
|
||||
Paragraph barChart = new Paragraph(category + ": " + bar, SMALL_FONT);
|
||||
document.add(barChart);
|
||||
}
|
||||
|
||||
document.add(Chunk.NEWLINE);
|
||||
}
|
||||
}
|
||||
198
src/main/java/dev/lions/audit/AuditResource.java
Normal file
198
src/main/java/dev/lions/audit/AuditResource.java
Normal file
@@ -0,0 +1,198 @@
|
||||
package dev.lions.audit;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.ws.rs.*;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* API REST pour l'outil d'audit de maturité digitale
|
||||
*/
|
||||
@Path("/api/audit")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public class AuditResource {
|
||||
|
||||
@Inject
|
||||
AuditService auditService;
|
||||
|
||||
@Inject
|
||||
AuditReportService reportService;
|
||||
|
||||
/**
|
||||
* Récupère toutes les questions d'audit par catégorie
|
||||
*/
|
||||
@GET
|
||||
@Path("/questions")
|
||||
public Response getQuestions(@QueryParam("lang") @DefaultValue("fr") String language) {
|
||||
try {
|
||||
Map<String, List<AuditQuestion>> questions = auditService.getQuestionsByCategory();
|
||||
return Response.ok(questions).build();
|
||||
} catch (Exception e) {
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(Map.of("error", "Erreur lors du chargement des questions"))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Soumet les réponses d'audit et génère le rapport
|
||||
*/
|
||||
@POST
|
||||
@Path("/submit")
|
||||
public Response submitAudit(AuditSubmissionDTO submission) {
|
||||
try {
|
||||
// Validation des données
|
||||
if (submission.getCompanyName() == null || submission.getCompanyName().trim().isEmpty()) {
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(Map.of("error", "Le nom de l'entreprise est requis"))
|
||||
.build();
|
||||
}
|
||||
|
||||
if (submission.getEmail() == null || !isValidEmail(submission.getEmail())) {
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(Map.of("error", "Email valide requis"))
|
||||
.build();
|
||||
}
|
||||
|
||||
// Création de la réponse d'audit
|
||||
AuditResponse response = new AuditResponse();
|
||||
response.setCompanyName(submission.getCompanyName());
|
||||
response.setContactName(submission.getContactName());
|
||||
response.setEmail(submission.getEmail());
|
||||
response.setPhone(submission.getPhone());
|
||||
response.setSector(submission.getSector());
|
||||
response.setEmployeeCount(submission.getEmployeeCount());
|
||||
response.setTurnover(submission.getTurnover());
|
||||
response.setAnswers(submission.getAnswers());
|
||||
|
||||
// Traitement de l'audit
|
||||
AuditResponse processedResponse = auditService.processAuditResponse(response);
|
||||
|
||||
// Génération du rapport PDF
|
||||
byte[] pdfReport = reportService.generateAuditReport(processedResponse);
|
||||
|
||||
// Envoi par email
|
||||
reportService.sendAuditReportByEmail(processedResponse, pdfReport);
|
||||
|
||||
// Réponse avec résumé
|
||||
AuditResultDTO result = new AuditResultDTO();
|
||||
result.setAuditId(processedResponse.getId());
|
||||
result.setMaturityPercentage(processedResponse.getMaturityPercentage());
|
||||
result.setCategoryScores(processedResponse.getCategoryScores());
|
||||
result.setRecommendations(processedResponse.getRecommendations());
|
||||
result.setPriorityActions(processedResponse.getPriorityActions());
|
||||
result.setEstimatedBudgetMin(processedResponse.getEstimatedBudgetMin());
|
||||
result.setEstimatedBudgetMax(processedResponse.getEstimatedBudgetMax());
|
||||
|
||||
return Response.ok(result).build();
|
||||
|
||||
} catch (Exception e) {
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(Map.of("error", "Erreur lors du traitement de l'audit: " + e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Télécharge le rapport PDF d'un audit
|
||||
*/
|
||||
@GET
|
||||
@Path("/report/{auditId}")
|
||||
@Produces("application/pdf")
|
||||
public Response downloadReport(@PathParam("auditId") Long auditId) {
|
||||
try {
|
||||
AuditResponse audit = auditService.getAuditById(auditId);
|
||||
if (audit == null) {
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity("Audit non trouvé")
|
||||
.build();
|
||||
}
|
||||
|
||||
byte[] pdfReport = reportService.generateAuditReport(audit);
|
||||
|
||||
return Response.ok(pdfReport)
|
||||
.header("Content-Disposition",
|
||||
"attachment; filename=\"audit-" + audit.getCompanyName() + ".pdf\"")
|
||||
.build();
|
||||
|
||||
} catch (Exception e) {
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity("Erreur lors de la génération du rapport")
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les statistiques d'audit pour le dashboard admin
|
||||
*/
|
||||
@GET
|
||||
@Path("/stats")
|
||||
public Response getAuditStats() {
|
||||
try {
|
||||
Map<String, Object> stats = auditService.getAuditStatistics();
|
||||
return Response.ok(stats).build();
|
||||
} catch (Exception e) {
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(Map.of("error", "Erreur lors du chargement des statistiques"))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les audits non contactés (pour équipe commerciale)
|
||||
*/
|
||||
@GET
|
||||
@Path("/leads")
|
||||
public Response getUncontactedLeads() {
|
||||
try {
|
||||
List<AuditResponse> leads = auditService.getUncontactedAudits();
|
||||
return Response.ok(leads).build();
|
||||
} catch (Exception e) {
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(Map.of("error", "Erreur lors du chargement des leads"))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marque un audit comme contacté
|
||||
*/
|
||||
@PUT
|
||||
@Path("/contact/{auditId}")
|
||||
public Response markAsContacted(@PathParam("auditId") Long auditId,
|
||||
Map<String, String> notes) {
|
||||
try {
|
||||
auditService.markAsContacted(auditId, notes.get("notes"));
|
||||
return Response.ok(Map.of("success", true)).build();
|
||||
} catch (Exception e) {
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(Map.of("error", "Erreur lors de la mise à jour"))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Demande de rendez-vous après audit
|
||||
*/
|
||||
@POST
|
||||
@Path("/request-meeting")
|
||||
public Response requestMeeting(MeetingRequestDTO request) {
|
||||
try {
|
||||
auditService.processMeetingRequest(request);
|
||||
return Response.ok(Map.of("success", true,
|
||||
"message", "Demande de rendez-vous enregistrée"))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(Map.of("error", "Erreur lors de l'enregistrement"))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isValidEmail(String email) {
|
||||
return email != null && email.matches("^[A-Za-z0-9+_.-]+@(.+)$");
|
||||
}
|
||||
}
|
||||
145
src/main/java/dev/lions/audit/AuditResponse.java
Normal file
145
src/main/java/dev/lions/audit/AuditResponse.java
Normal file
@@ -0,0 +1,145 @@
|
||||
package dev.lions.audit;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Réponse d'audit d'une PME avec scoring et recommandations
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "audit_responses")
|
||||
public class AuditResponse {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
// Informations entreprise
|
||||
@Column(nullable = false)
|
||||
private String companyName;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String contactName;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String email;
|
||||
|
||||
private String phone;
|
||||
private String sector; // Secteur d'activité
|
||||
private Integer employeeCount;
|
||||
private String turnover; // Chiffre d'affaires
|
||||
|
||||
// Audit
|
||||
@ElementCollection
|
||||
@CollectionTable(name = "audit_answers")
|
||||
@MapKeyColumn(name = "question_id")
|
||||
@Column(name = "answer_index")
|
||||
private Map<Long, Integer> answers; // questionId -> index de réponse
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime submittedAt;
|
||||
|
||||
// Scoring
|
||||
private Integer totalScore;
|
||||
private Integer maxPossibleScore;
|
||||
private Double maturityPercentage;
|
||||
|
||||
@ElementCollection
|
||||
@CollectionTable(name = "audit_category_scores")
|
||||
@MapKeyColumn(name = "category")
|
||||
@Column(name = "score")
|
||||
private Map<String, Integer> categoryScores;
|
||||
|
||||
// Recommandations
|
||||
@Column(length = 2000)
|
||||
private String recommendations;
|
||||
|
||||
@Column(length = 1000)
|
||||
private String priorityActions;
|
||||
|
||||
// Estimation budgétaire
|
||||
private Double estimatedBudgetMin;
|
||||
private Double estimatedBudgetMax;
|
||||
|
||||
// Suivi commercial
|
||||
private Boolean contacted = false;
|
||||
private LocalDateTime contactedAt;
|
||||
private String salesNotes;
|
||||
|
||||
// Constructeurs
|
||||
public AuditResponse() {
|
||||
this.submittedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
public AuditResponse(String companyName, String contactName, String email) {
|
||||
this();
|
||||
this.companyName = companyName;
|
||||
this.contactName = contactName;
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
// Getters et Setters
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public String getCompanyName() { return companyName; }
|
||||
public void setCompanyName(String companyName) { this.companyName = companyName; }
|
||||
|
||||
public String getContactName() { return contactName; }
|
||||
public void setContactName(String contactName) { this.contactName = contactName; }
|
||||
|
||||
public String getEmail() { return email; }
|
||||
public void setEmail(String email) { this.email = email; }
|
||||
|
||||
public String getPhone() { return phone; }
|
||||
public void setPhone(String phone) { this.phone = phone; }
|
||||
|
||||
public String getSector() { return sector; }
|
||||
public void setSector(String sector) { this.sector = sector; }
|
||||
|
||||
public Integer getEmployeeCount() { return employeeCount; }
|
||||
public void setEmployeeCount(Integer employeeCount) { this.employeeCount = employeeCount; }
|
||||
|
||||
public String getTurnover() { return turnover; }
|
||||
public void setTurnover(String turnover) { this.turnover = turnover; }
|
||||
|
||||
public Map<Long, Integer> getAnswers() { return answers; }
|
||||
public void setAnswers(Map<Long, Integer> answers) { this.answers = answers; }
|
||||
|
||||
public LocalDateTime getSubmittedAt() { return submittedAt; }
|
||||
public void setSubmittedAt(LocalDateTime submittedAt) { this.submittedAt = submittedAt; }
|
||||
|
||||
public Integer getTotalScore() { return totalScore; }
|
||||
public void setTotalScore(Integer totalScore) { this.totalScore = totalScore; }
|
||||
|
||||
public Integer getMaxPossibleScore() { return maxPossibleScore; }
|
||||
public void setMaxPossibleScore(Integer maxPossibleScore) { this.maxPossibleScore = maxPossibleScore; }
|
||||
|
||||
public Double getMaturityPercentage() { return maturityPercentage; }
|
||||
public void setMaturityPercentage(Double maturityPercentage) { this.maturityPercentage = maturityPercentage; }
|
||||
|
||||
public Map<String, Integer> getCategoryScores() { return categoryScores; }
|
||||
public void setCategoryScores(Map<String, Integer> categoryScores) { this.categoryScores = categoryScores; }
|
||||
|
||||
public String getRecommendations() { return recommendations; }
|
||||
public void setRecommendations(String recommendations) { this.recommendations = recommendations; }
|
||||
|
||||
public String getPriorityActions() { return priorityActions; }
|
||||
public void setPriorityActions(String priorityActions) { this.priorityActions = priorityActions; }
|
||||
|
||||
public Double getEstimatedBudgetMin() { return estimatedBudgetMin; }
|
||||
public void setEstimatedBudgetMin(Double estimatedBudgetMin) { this.estimatedBudgetMin = estimatedBudgetMin; }
|
||||
|
||||
public Double getEstimatedBudgetMax() { return estimatedBudgetMax; }
|
||||
public void setEstimatedBudgetMax(Double estimatedBudgetMax) { this.estimatedBudgetMax = estimatedBudgetMax; }
|
||||
|
||||
public Boolean getContacted() { return contacted; }
|
||||
public void setContacted(Boolean contacted) { this.contacted = contacted; }
|
||||
|
||||
public LocalDateTime getContactedAt() { return contactedAt; }
|
||||
public void setContactedAt(LocalDateTime contactedAt) { this.contactedAt = contactedAt; }
|
||||
|
||||
public String getSalesNotes() { return salesNotes; }
|
||||
public void setSalesNotes(String salesNotes) { this.salesNotes = salesNotes; }
|
||||
}
|
||||
247
src/main/java/dev/lions/audit/AuditService.java
Normal file
247
src/main/java/dev/lions/audit/AuditService.java
Normal file
@@ -0,0 +1,247 @@
|
||||
package dev.lions.audit;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.transaction.Transactional;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Service de gestion des audits de maturité digitale
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class AuditService {
|
||||
|
||||
@Inject
|
||||
EntityManager em;
|
||||
|
||||
/**
|
||||
* Récupère toutes les questions d'audit actives par catégorie
|
||||
*/
|
||||
public Map<String, List<AuditQuestion>> getQuestionsByCategory() {
|
||||
List<AuditQuestion> questions = em.createQuery(
|
||||
"SELECT q FROM AuditQuestion q WHERE q.active = true ORDER BY q.displayOrder",
|
||||
AuditQuestion.class
|
||||
).getResultList();
|
||||
|
||||
return questions.stream()
|
||||
.collect(Collectors.groupingBy(AuditQuestion::getCategory));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule le score d'un audit et génère les recommandations
|
||||
*/
|
||||
@Transactional
|
||||
public AuditResponse processAuditResponse(AuditResponse response) {
|
||||
Map<String, List<AuditQuestion>> questionsByCategory = getQuestionsByCategory();
|
||||
Map<String, Integer> categoryScores = new HashMap<>();
|
||||
Map<String, Integer> categoryMaxScores = new HashMap<>();
|
||||
|
||||
int totalScore = 0;
|
||||
int maxPossibleScore = 0;
|
||||
|
||||
// Calcul des scores par catégorie
|
||||
for (Map.Entry<String, List<AuditQuestion>> entry : questionsByCategory.entrySet()) {
|
||||
String category = entry.getKey();
|
||||
List<AuditQuestion> questions = entry.getValue();
|
||||
|
||||
int categoryScore = 0;
|
||||
int categoryMaxScore = 0;
|
||||
|
||||
for (AuditQuestion question : questions) {
|
||||
Integer answerIndex = response.getAnswers().get(question.getId());
|
||||
if (answerIndex != null && answerIndex < question.getScores().size()) {
|
||||
int questionScore = question.getScores().get(answerIndex) * question.getWeight();
|
||||
categoryScore += questionScore;
|
||||
totalScore += questionScore;
|
||||
}
|
||||
|
||||
int maxQuestionScore = Collections.max(question.getScores()) * question.getWeight();
|
||||
categoryMaxScore += maxQuestionScore;
|
||||
maxPossibleScore += maxQuestionScore;
|
||||
}
|
||||
|
||||
categoryScores.put(category, categoryScore);
|
||||
categoryMaxScores.put(category, categoryMaxScore);
|
||||
}
|
||||
|
||||
// Mise à jour des scores
|
||||
response.setTotalScore(totalScore);
|
||||
response.setMaxPossibleScore(maxPossibleScore);
|
||||
response.setMaturityPercentage((double) totalScore / maxPossibleScore * 100);
|
||||
response.setCategoryScores(categoryScores);
|
||||
|
||||
// Génération des recommandations
|
||||
generateRecommendations(response, categoryScores, categoryMaxScores);
|
||||
|
||||
// Estimation budgétaire
|
||||
estimateBudget(response, categoryScores);
|
||||
|
||||
// Sauvegarde
|
||||
em.persist(response);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère les recommandations personnalisées
|
||||
*/
|
||||
private void generateRecommendations(AuditResponse response,
|
||||
Map<String, Integer> categoryScores,
|
||||
Map<String, Integer> categoryMaxScores) {
|
||||
|
||||
StringBuilder recommendations = new StringBuilder();
|
||||
List<String> priorities = new ArrayList<>();
|
||||
|
||||
// Analyse par catégorie
|
||||
for (Map.Entry<String, Integer> entry : categoryScores.entrySet()) {
|
||||
String category = entry.getKey();
|
||||
int score = entry.getValue();
|
||||
int maxScore = categoryMaxScores.get(category);
|
||||
double percentage = (double) score / maxScore * 100;
|
||||
|
||||
String categoryName = getCategoryDisplayName(category);
|
||||
|
||||
if (percentage < 30) {
|
||||
recommendations.append("🚨 ").append(categoryName).append(" : Niveau critique - Digitalisation urgente nécessaire\n");
|
||||
priorities.add("Digitaliser " + categoryName.toLowerCase());
|
||||
} else if (percentage < 60) {
|
||||
recommendations.append("⚠️ ").append(categoryName).append(" : Niveau faible - Améliorations importantes recommandées\n");
|
||||
priorities.add("Améliorer " + categoryName.toLowerCase());
|
||||
} else if (percentage < 80) {
|
||||
recommendations.append("✅ ").append(categoryName).append(" : Niveau correct - Optimisations possibles\n");
|
||||
} else {
|
||||
recommendations.append("🏆 ").append(categoryName).append(" : Excellent niveau de digitalisation\n");
|
||||
}
|
||||
}
|
||||
|
||||
response.setRecommendations(recommendations.toString());
|
||||
response.setPriorityActions(String.join(", ", priorities));
|
||||
}
|
||||
|
||||
/**
|
||||
* Estime le budget nécessaire selon les scores
|
||||
*/
|
||||
private void estimateBudget(AuditResponse response, Map<String, Integer> categoryScores) {
|
||||
double budgetMin = 0;
|
||||
double budgetMax = 0;
|
||||
|
||||
// Tarification par module selon le niveau de maturité
|
||||
Map<String, Double[]> modulePricing = Map.of(
|
||||
"commercial", new Double[]{150000.0, 300000.0}, // CRM
|
||||
"stock", new Double[]{100000.0, 250000.0}, // Gestion stocks
|
||||
"comptabilite", new Double[]{200000.0, 400000.0}, // Comptabilité
|
||||
"rh", new Double[]{80000.0, 200000.0}, // RH
|
||||
"infrastructure", new Double[]{100000.0, 300000.0} // IT
|
||||
);
|
||||
|
||||
for (Map.Entry<String, Integer> entry : categoryScores.entrySet()) {
|
||||
String category = entry.getKey();
|
||||
int score = entry.getValue();
|
||||
|
||||
if (score < 50 && modulePricing.containsKey(category)) { // Besoin d'amélioration
|
||||
Double[] pricing = modulePricing.get(category);
|
||||
budgetMin += pricing[0];
|
||||
budgetMax += pricing[1];
|
||||
}
|
||||
}
|
||||
|
||||
response.setEstimatedBudgetMin(budgetMin);
|
||||
response.setEstimatedBudgetMax(budgetMax);
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les audits non contactés pour le suivi commercial
|
||||
*/
|
||||
public List<AuditResponse> getUncontactedAudits() {
|
||||
return em.createQuery(
|
||||
"SELECT a FROM AuditResponse a WHERE a.contacted = false ORDER BY a.submittedAt DESC",
|
||||
AuditResponse.class
|
||||
).getResultList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Marque un audit comme contacté
|
||||
*/
|
||||
@Transactional
|
||||
public void markAsContacted(Long auditId, String notes) {
|
||||
AuditResponse audit = em.find(AuditResponse.class, auditId);
|
||||
if (audit != null) {
|
||||
audit.setContacted(true);
|
||||
audit.setContactedAt(java.time.LocalDateTime.now());
|
||||
audit.setSalesNotes(notes);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère un audit par ID
|
||||
*/
|
||||
public AuditResponse getAuditById(Long auditId) {
|
||||
return em.find(AuditResponse.class, auditId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les statistiques d'audit
|
||||
*/
|
||||
public Map<String, Object> getAuditStatistics() {
|
||||
Map<String, Object> stats = new HashMap<>();
|
||||
|
||||
// Nombre total d'audits
|
||||
Long totalAudits = em.createQuery("SELECT COUNT(a) FROM AuditResponse a", Long.class)
|
||||
.getSingleResult();
|
||||
stats.put("totalAudits", totalAudits);
|
||||
|
||||
// Audits cette semaine
|
||||
Long weeklyAudits = em.createQuery(
|
||||
"SELECT COUNT(a) FROM AuditResponse a WHERE a.submittedAt >= :weekStart", Long.class)
|
||||
.setParameter("weekStart", java.time.LocalDateTime.now().minusDays(7))
|
||||
.getSingleResult();
|
||||
stats.put("weeklyAudits", weeklyAudits);
|
||||
|
||||
// Score moyen
|
||||
Double avgScore = em.createQuery(
|
||||
"SELECT AVG(a.maturityPercentage) FROM AuditResponse a", Double.class)
|
||||
.getSingleResult();
|
||||
stats.put("averageScore", avgScore != null ? avgScore : 0.0);
|
||||
|
||||
// Répartition par secteur
|
||||
List<Object[]> sectorStats = em.createQuery(
|
||||
"SELECT a.sector, COUNT(a) FROM AuditResponse a GROUP BY a.sector", Object[].class)
|
||||
.getResultList();
|
||||
Map<String, Long> sectorDistribution = new HashMap<>();
|
||||
for (Object[] row : sectorStats) {
|
||||
sectorDistribution.put((String) row[0], (Long) row[1]);
|
||||
}
|
||||
stats.put("sectorDistribution", sectorDistribution);
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Traite une demande de rendez-vous
|
||||
*/
|
||||
@Transactional
|
||||
public void processMeetingRequest(MeetingRequestDTO request) {
|
||||
// Ici on pourrait créer une entité MeetingRequest
|
||||
// Pour l'instant, on met à jour les notes de l'audit
|
||||
AuditResponse audit = em.find(AuditResponse.class, request.getAuditId());
|
||||
if (audit != null) {
|
||||
String meetingNote = String.format("RDV demandé: %s à %s (%s) - %s",
|
||||
request.getPreferredDate(), request.getPreferredTime(),
|
||||
request.getMeetingType(), request.getMessage());
|
||||
audit.setSalesNotes(meetingNote);
|
||||
}
|
||||
}
|
||||
|
||||
private String getCategoryDisplayName(String category) {
|
||||
return switch (category) {
|
||||
case "commercial" -> "Gestion Commerciale";
|
||||
case "stock" -> "Gestion des Stocks";
|
||||
case "comptabilite" -> "Comptabilité";
|
||||
case "rh" -> "Ressources Humaines";
|
||||
case "infrastructure" -> "Infrastructure IT";
|
||||
default -> category;
|
||||
};
|
||||
}
|
||||
}
|
||||
129
src/main/java/dev/lions/audit/AuditSubmissionDTO.java
Normal file
129
src/main/java/dev/lions/audit/AuditSubmissionDTO.java
Normal file
@@ -0,0 +1,129 @@
|
||||
package dev.lions.audit;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* DTO pour la soumission d'un audit
|
||||
*/
|
||||
public class AuditSubmissionDTO {
|
||||
|
||||
// Informations entreprise
|
||||
private String companyName;
|
||||
private String contactName;
|
||||
private String email;
|
||||
private String phone;
|
||||
private String sector;
|
||||
private Integer employeeCount;
|
||||
private String turnover;
|
||||
|
||||
// Réponses aux questions
|
||||
private Map<Long, Integer> answers; // questionId -> answerIndex
|
||||
|
||||
// Préférences
|
||||
private String preferredContactTime;
|
||||
private String additionalComments;
|
||||
private Boolean acceptsMarketing = false;
|
||||
|
||||
// Constructeurs
|
||||
public AuditSubmissionDTO() {}
|
||||
|
||||
// Getters et Setters
|
||||
public String getCompanyName() { return companyName; }
|
||||
public void setCompanyName(String companyName) { this.companyName = companyName; }
|
||||
|
||||
public String getContactName() { return contactName; }
|
||||
public void setContactName(String contactName) { this.contactName = contactName; }
|
||||
|
||||
public String getEmail() { return email; }
|
||||
public void setEmail(String email) { this.email = email; }
|
||||
|
||||
public String getPhone() { return phone; }
|
||||
public void setPhone(String phone) { this.phone = phone; }
|
||||
|
||||
public String getSector() { return sector; }
|
||||
public void setSector(String sector) { this.sector = sector; }
|
||||
|
||||
public Integer getEmployeeCount() { return employeeCount; }
|
||||
public void setEmployeeCount(Integer employeeCount) { this.employeeCount = employeeCount; }
|
||||
|
||||
public String getTurnover() { return turnover; }
|
||||
public void setTurnover(String turnover) { this.turnover = turnover; }
|
||||
|
||||
public Map<Long, Integer> getAnswers() { return answers; }
|
||||
public void setAnswers(Map<Long, Integer> answers) { this.answers = answers; }
|
||||
|
||||
public String getPreferredContactTime() { return preferredContactTime; }
|
||||
public void setPreferredContactTime(String preferredContactTime) { this.preferredContactTime = preferredContactTime; }
|
||||
|
||||
public String getAdditionalComments() { return additionalComments; }
|
||||
public void setAdditionalComments(String additionalComments) { this.additionalComments = additionalComments; }
|
||||
|
||||
public Boolean getAcceptsMarketing() { return acceptsMarketing; }
|
||||
public void setAcceptsMarketing(Boolean acceptsMarketing) { this.acceptsMarketing = acceptsMarketing; }
|
||||
}
|
||||
|
||||
/**
|
||||
* DTO pour le résultat d'audit
|
||||
*/
|
||||
class AuditResultDTO {
|
||||
private Long auditId;
|
||||
private Double maturityPercentage;
|
||||
private Map<String, Integer> categoryScores;
|
||||
private String recommendations;
|
||||
private String priorityActions;
|
||||
private Double estimatedBudgetMin;
|
||||
private Double estimatedBudgetMax;
|
||||
private String nextSteps;
|
||||
|
||||
// Getters et Setters
|
||||
public Long getAuditId() { return auditId; }
|
||||
public void setAuditId(Long auditId) { this.auditId = auditId; }
|
||||
|
||||
public Double getMaturityPercentage() { return maturityPercentage; }
|
||||
public void setMaturityPercentage(Double maturityPercentage) { this.maturityPercentage = maturityPercentage; }
|
||||
|
||||
public Map<String, Integer> getCategoryScores() { return categoryScores; }
|
||||
public void setCategoryScores(Map<String, Integer> categoryScores) { this.categoryScores = categoryScores; }
|
||||
|
||||
public String getRecommendations() { return recommendations; }
|
||||
public void setRecommendations(String recommendations) { this.recommendations = recommendations; }
|
||||
|
||||
public String getPriorityActions() { return priorityActions; }
|
||||
public void setPriorityActions(String priorityActions) { this.priorityActions = priorityActions; }
|
||||
|
||||
public Double getEstimatedBudgetMin() { return estimatedBudgetMin; }
|
||||
public void setEstimatedBudgetMin(Double estimatedBudgetMin) { this.estimatedBudgetMin = estimatedBudgetMin; }
|
||||
|
||||
public Double getEstimatedBudgetMax() { return estimatedBudgetMax; }
|
||||
public void setEstimatedBudgetMax(Double estimatedBudgetMax) { this.estimatedBudgetMax = estimatedBudgetMax; }
|
||||
|
||||
public String getNextSteps() { return nextSteps; }
|
||||
public void setNextSteps(String nextSteps) { this.nextSteps = nextSteps; }
|
||||
}
|
||||
|
||||
/**
|
||||
* DTO pour demande de rendez-vous
|
||||
*/
|
||||
class MeetingRequestDTO {
|
||||
private Long auditId;
|
||||
private String preferredDate;
|
||||
private String preferredTime;
|
||||
private String meetingType; // "phone", "video", "onsite"
|
||||
private String message;
|
||||
|
||||
// Getters et Setters
|
||||
public Long getAuditId() { return auditId; }
|
||||
public void setAuditId(Long auditId) { this.auditId = auditId; }
|
||||
|
||||
public String getPreferredDate() { return preferredDate; }
|
||||
public void setPreferredDate(String preferredDate) { this.preferredDate = preferredDate; }
|
||||
|
||||
public String getPreferredTime() { return preferredTime; }
|
||||
public void setPreferredTime(String preferredTime) { this.preferredTime = preferredTime; }
|
||||
|
||||
public String getMeetingType() { return meetingType; }
|
||||
public void setMeetingType(String meetingType) { this.meetingType = meetingType; }
|
||||
|
||||
public String getMessage() { return message; }
|
||||
public void setMessage(String message) { this.message = message; }
|
||||
}
|
||||
534
src/main/java/dev/lions/compliance/IvorianTaxService.java
Normal file
534
src/main/java/dev/lions/compliance/IvorianTaxService.java
Normal file
@@ -0,0 +1,534 @@
|
||||
package dev.lions.compliance;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import java.time.LocalDate;
|
||||
import java.time.YearMonth;
|
||||
import java.util.Map;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* Service de conformité fiscale ivoirienne
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class IvorianTaxService {
|
||||
|
||||
// Taux de TVA en Côte d'Ivoire
|
||||
public static final double TVA_RATE = 18.0; // 18%
|
||||
public static final double TVA_REDUCED_RATE = 9.0; // 9% pour certains produits
|
||||
|
||||
// Taux d'impôt sur les sociétés
|
||||
public static final double IS_RATE = 25.0; // 25%
|
||||
public static final double IS_REDUCED_RATE = 20.0; // 20% pour PME
|
||||
|
||||
// Seuils PME
|
||||
public static final double PME_TURNOVER_THRESHOLD = 200_000_000; // 200M FCFA
|
||||
|
||||
/**
|
||||
* Calcule la TVA sur un montant
|
||||
*/
|
||||
public TaxCalculation calculateTVA(double amountHT, boolean reducedRate) {
|
||||
double rate = reducedRate ? TVA_REDUCED_RATE : TVA_RATE;
|
||||
double tvaAmount = amountHT * (rate / 100);
|
||||
double amountTTC = amountHT + tvaAmount;
|
||||
|
||||
TaxCalculation calculation = new TaxCalculation();
|
||||
calculation.setAmountHT(amountHT);
|
||||
calculation.setTaxRate(rate);
|
||||
calculation.setTaxAmount(tvaAmount);
|
||||
calculation.setAmountTTC(amountTTC);
|
||||
calculation.setTaxType("TVA");
|
||||
|
||||
return calculation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule l'impôt sur les sociétés
|
||||
*/
|
||||
public TaxCalculation calculateIS(double annualProfit, boolean isPME) {
|
||||
double rate = isPME ? IS_REDUCED_RATE : IS_RATE;
|
||||
double isAmount = annualProfit * (rate / 100);
|
||||
|
||||
TaxCalculation calculation = new TaxCalculation();
|
||||
calculation.setAmountHT(annualProfit);
|
||||
calculation.setTaxRate(rate);
|
||||
calculation.setTaxAmount(isAmount);
|
||||
calculation.setAmountTTC(annualProfit - isAmount); // Bénéfice net après IS
|
||||
calculation.setTaxType("IS");
|
||||
|
||||
return calculation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère la déclaration TVA mensuelle
|
||||
*/
|
||||
public TVADeclaration generateTVADeclaration(String tenantId, YearMonth period,
|
||||
List<TVATransaction> transactions) {
|
||||
TVADeclaration declaration = new TVADeclaration();
|
||||
declaration.setTenantId(tenantId);
|
||||
declaration.setPeriod(period);
|
||||
declaration.setDeclarationType("MENSUELLE");
|
||||
|
||||
double totalVentesHT = 0;
|
||||
double totalTVACollectee = 0;
|
||||
double totalAchatsHT = 0;
|
||||
double totalTVADeductible = 0;
|
||||
|
||||
for (TVATransaction transaction : transactions) {
|
||||
if (transaction.getType() == TransactionType.VENTE) {
|
||||
totalVentesHT += transaction.getAmountHT();
|
||||
totalTVACollectee += transaction.getTvaAmount();
|
||||
} else if (transaction.getType() == TransactionType.ACHAT) {
|
||||
totalAchatsHT += transaction.getAmountHT();
|
||||
totalTVADeductible += transaction.getTvaAmount();
|
||||
}
|
||||
}
|
||||
|
||||
double tvaAVerser = totalTVACollectee - totalTVADeductible;
|
||||
|
||||
declaration.setVentesHT(totalVentesHT);
|
||||
declaration.setTvaCollectee(totalTVACollectee);
|
||||
declaration.setAchatsHT(totalAchatsHT);
|
||||
declaration.setTvaDeductible(totalTVADeductible);
|
||||
declaration.setTvaAVerser(Math.max(0, tvaAVerser));
|
||||
declaration.setCreditTVA(Math.max(0, -tvaAVerser));
|
||||
|
||||
return declaration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère la déclaration IS annuelle
|
||||
*/
|
||||
public ISDeclaration generateISDeclaration(String tenantId, int year,
|
||||
double chiffreAffaires, double charges,
|
||||
double amortissements) {
|
||||
ISDeclaration declaration = new ISDeclaration();
|
||||
declaration.setTenantId(tenantId);
|
||||
declaration.setYear(year);
|
||||
|
||||
double beneficeBrut = chiffreAffaires - charges;
|
||||
double beneficeImposable = beneficeBrut - amortissements;
|
||||
|
||||
boolean isPME = chiffreAffaires <= PME_TURNOVER_THRESHOLD;
|
||||
TaxCalculation isCalculation = calculateIS(beneficeImposable, isPME);
|
||||
|
||||
declaration.setChiffreAffaires(chiffreAffaires);
|
||||
declaration.setCharges(charges);
|
||||
declaration.setAmortissements(amortissements);
|
||||
declaration.setBeneficeBrut(beneficeBrut);
|
||||
declaration.setBeneficeImposable(beneficeImposable);
|
||||
declaration.setTauxIS(isCalculation.getTaxRate());
|
||||
declaration.setMontantIS(isCalculation.getTaxAmount());
|
||||
declaration.setBeneficeNet(isCalculation.getAmountTTC());
|
||||
declaration.setIsPME(isPME);
|
||||
|
||||
return declaration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule les cotisations CNPS
|
||||
*/
|
||||
public CNPSCalculation calculateCNPS(double salaireBrut) {
|
||||
CNPSCalculation calculation = new CNPSCalculation();
|
||||
|
||||
// Plafond CNPS (à ajuster selon les barèmes en vigueur)
|
||||
double plafondCNPS = 1_800_000; // 1.8M FCFA par an
|
||||
double salaireImposable = Math.min(salaireBrut, plafondCNPS);
|
||||
|
||||
// Taux CNPS
|
||||
double tauxEmployeur = 16.75; // 16.75%
|
||||
double tauxEmploye = 6.3; // 6.3%
|
||||
|
||||
double cotisationEmployeur = salaireImposable * (tauxEmployeur / 100);
|
||||
double cotisationEmploye = salaireImposable * (tauxEmploye / 100);
|
||||
double cotisationTotale = cotisationEmployeur + cotisationEmploye;
|
||||
|
||||
calculation.setSalaireBrut(salaireBrut);
|
||||
calculation.setSalaireImposable(salaireImposable);
|
||||
calculation.setTauxEmployeur(tauxEmployeur);
|
||||
calculation.setTauxEmploye(tauxEmploye);
|
||||
calculation.setCotisationEmployeur(cotisationEmployeur);
|
||||
calculation.setCotisationEmploye(cotisationEmploye);
|
||||
calculation.setCotisationTotale(cotisationTotale);
|
||||
calculation.setSalaireNet(salaireBrut - cotisationEmploye);
|
||||
|
||||
return calculation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie la conformité d'une entreprise
|
||||
*/
|
||||
public ComplianceReport checkCompliance(String tenantId) {
|
||||
ComplianceReport report = new ComplianceReport();
|
||||
report.setTenantId(tenantId);
|
||||
report.setCheckDate(LocalDate.now());
|
||||
|
||||
List<ComplianceIssue> issues = new ArrayList<>();
|
||||
|
||||
// Vérifications de base
|
||||
if (!hasValidTaxNumber(tenantId)) {
|
||||
issues.add(new ComplianceIssue("TAX_NUMBER", "Numéro contribuable DGI manquant", "HIGH"));
|
||||
}
|
||||
|
||||
if (!hasValidCNPSNumber(tenantId)) {
|
||||
issues.add(new ComplianceIssue("CNPS_NUMBER", "Numéro CNPS manquant", "HIGH"));
|
||||
}
|
||||
|
||||
if (!hasValidRCCM(tenantId)) {
|
||||
issues.add(new ComplianceIssue("RCCM", "Numéro RCCM manquant", "MEDIUM"));
|
||||
}
|
||||
|
||||
// Vérifications déclarations
|
||||
if (!hasRecentTVADeclaration(tenantId)) {
|
||||
issues.add(new ComplianceIssue("TVA_DECLARATION", "Déclaration TVA en retard", "HIGH"));
|
||||
}
|
||||
|
||||
if (!hasRecentISDeclaration(tenantId)) {
|
||||
issues.add(new ComplianceIssue("IS_DECLARATION", "Déclaration IS en retard", "HIGH"));
|
||||
}
|
||||
|
||||
report.setIssues(issues);
|
||||
report.setComplianceScore(calculateComplianceScore(issues));
|
||||
report.setStatus(issues.isEmpty() ? "COMPLIANT" : "NON_COMPLIANT");
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère les échéances fiscales
|
||||
*/
|
||||
public List<TaxDeadline> getTaxDeadlines(int year) {
|
||||
List<TaxDeadline> deadlines = new ArrayList<>();
|
||||
|
||||
// Déclarations TVA mensuelles (15 de chaque mois)
|
||||
for (int month = 1; month <= 12; month++) {
|
||||
deadlines.add(new TaxDeadline(
|
||||
"TVA_MENSUELLE",
|
||||
"Déclaration TVA " + getMonthName(month),
|
||||
LocalDate.of(year, month, 15),
|
||||
"HIGH"
|
||||
));
|
||||
}
|
||||
|
||||
// Déclaration IS annuelle (30 avril)
|
||||
deadlines.add(new TaxDeadline(
|
||||
"IS_ANNUELLE",
|
||||
"Déclaration Impôt sur les Sociétés " + year,
|
||||
LocalDate.of(year + 1, 4, 30),
|
||||
"HIGH"
|
||||
));
|
||||
|
||||
// Déclarations CNPS trimestrielles
|
||||
deadlines.add(new TaxDeadline("CNPS_T1", "Déclaration CNPS T1", LocalDate.of(year, 4, 15), "MEDIUM"));
|
||||
deadlines.add(new TaxDeadline("CNPS_T2", "Déclaration CNPS T2", LocalDate.of(year, 7, 15), "MEDIUM"));
|
||||
deadlines.add(new TaxDeadline("CNPS_T3", "Déclaration CNPS T3", LocalDate.of(year, 10, 15), "MEDIUM"));
|
||||
deadlines.add(new TaxDeadline("CNPS_T4", "Déclaration CNPS T4", LocalDate.of(year + 1, 1, 15), "MEDIUM"));
|
||||
|
||||
return deadlines;
|
||||
}
|
||||
|
||||
// Méthodes utilitaires privées
|
||||
private boolean hasValidTaxNumber(String tenantId) {
|
||||
// Simulation - en réalité, vérifier en base
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean hasValidCNPSNumber(String tenantId) {
|
||||
// Simulation - en réalité, vérifier en base
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean hasValidRCCM(String tenantId) {
|
||||
// Simulation - en réalité, vérifier en base
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean hasRecentTVADeclaration(String tenantId) {
|
||||
// Simulation - en réalité, vérifier les déclarations récentes
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean hasRecentISDeclaration(String tenantId) {
|
||||
// Simulation - en réalité, vérifier les déclarations récentes
|
||||
return true;
|
||||
}
|
||||
|
||||
private double calculateComplianceScore(List<ComplianceIssue> issues) {
|
||||
if (issues.isEmpty()) return 100.0;
|
||||
|
||||
double penalty = 0;
|
||||
for (ComplianceIssue issue : issues) {
|
||||
switch (issue.getSeverity()) {
|
||||
case "HIGH" -> penalty += 20;
|
||||
case "MEDIUM" -> penalty += 10;
|
||||
case "LOW" -> penalty += 5;
|
||||
}
|
||||
}
|
||||
|
||||
return Math.max(0, 100 - penalty);
|
||||
}
|
||||
|
||||
private String getMonthName(int month) {
|
||||
String[] months = {"", "Janvier", "Février", "Mars", "Avril", "Mai", "Juin",
|
||||
"Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre"};
|
||||
return months[month];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcul fiscal
|
||||
*/
|
||||
class TaxCalculation {
|
||||
private double amountHT;
|
||||
private double taxRate;
|
||||
private double taxAmount;
|
||||
private double amountTTC;
|
||||
private String taxType;
|
||||
|
||||
// Getters et Setters
|
||||
public double getAmountHT() { return amountHT; }
|
||||
public void setAmountHT(double amountHT) { this.amountHT = amountHT; }
|
||||
|
||||
public double getTaxRate() { return taxRate; }
|
||||
public void setTaxRate(double taxRate) { this.taxRate = taxRate; }
|
||||
|
||||
public double getTaxAmount() { return taxAmount; }
|
||||
public void setTaxAmount(double taxAmount) { this.taxAmount = taxAmount; }
|
||||
|
||||
public double getAmountTTC() { return amountTTC; }
|
||||
public void setAmountTTC(double amountTTC) { this.amountTTC = amountTTC; }
|
||||
|
||||
public String getTaxType() { return taxType; }
|
||||
public void setTaxType(String taxType) { this.taxType = taxType; }
|
||||
}
|
||||
|
||||
/**
|
||||
* Transaction TVA
|
||||
*/
|
||||
class TVATransaction {
|
||||
private TransactionType type;
|
||||
private double amountHT;
|
||||
private double tvaAmount;
|
||||
private LocalDate date;
|
||||
|
||||
// Constructeurs et getters/setters
|
||||
public TransactionType getType() { return type; }
|
||||
public void setType(TransactionType type) { this.type = type; }
|
||||
|
||||
public double getAmountHT() { return amountHT; }
|
||||
public void setAmountHT(double amountHT) { this.amountHT = amountHT; }
|
||||
|
||||
public double getTvaAmount() { return tvaAmount; }
|
||||
public void setTvaAmount(double tvaAmount) { this.tvaAmount = tvaAmount; }
|
||||
|
||||
public LocalDate getDate() { return date; }
|
||||
public void setDate(LocalDate date) { this.date = date; }
|
||||
}
|
||||
|
||||
enum TransactionType {
|
||||
VENTE, ACHAT
|
||||
}
|
||||
|
||||
/**
|
||||
* Déclaration TVA
|
||||
*/
|
||||
class TVADeclaration {
|
||||
private String tenantId;
|
||||
private YearMonth period;
|
||||
private String declarationType;
|
||||
private double ventesHT;
|
||||
private double tvaCollectee;
|
||||
private double achatsHT;
|
||||
private double tvaDeductible;
|
||||
private double tvaAVerser;
|
||||
private double creditTVA;
|
||||
|
||||
// Getters et Setters complets...
|
||||
public String getTenantId() { return tenantId; }
|
||||
public void setTenantId(String tenantId) { this.tenantId = tenantId; }
|
||||
|
||||
public YearMonth getPeriod() { return period; }
|
||||
public void setPeriod(YearMonth period) { this.period = period; }
|
||||
|
||||
public String getDeclarationType() { return declarationType; }
|
||||
public void setDeclarationType(String declarationType) { this.declarationType = declarationType; }
|
||||
|
||||
public double getVentesHT() { return ventesHT; }
|
||||
public void setVentesHT(double ventesHT) { this.ventesHT = ventesHT; }
|
||||
|
||||
public double getTvaCollectee() { return tvaCollectee; }
|
||||
public void setTvaCollectee(double tvaCollectee) { this.tvaCollectee = tvaCollectee; }
|
||||
|
||||
public double getAchatsHT() { return achatsHT; }
|
||||
public void setAchatsHT(double achatsHT) { this.achatsHT = achatsHT; }
|
||||
|
||||
public double getTvaDeductible() { return tvaDeductible; }
|
||||
public void setTvaDeductible(double tvaDeductible) { this.tvaDeductible = tvaDeductible; }
|
||||
|
||||
public double getTvaAVerser() { return tvaAVerser; }
|
||||
public void setTvaAVerser(double tvaAVerser) { this.tvaAVerser = tvaAVerser; }
|
||||
|
||||
public double getCreditTVA() { return creditTVA; }
|
||||
public void setCreditTVA(double creditTVA) { this.creditTVA = creditTVA; }
|
||||
}
|
||||
|
||||
/**
|
||||
* Déclaration IS
|
||||
*/
|
||||
class ISDeclaration {
|
||||
private String tenantId;
|
||||
private int year;
|
||||
private double chiffreAffaires;
|
||||
private double charges;
|
||||
private double amortissements;
|
||||
private double beneficeBrut;
|
||||
private double beneficeImposable;
|
||||
private double tauxIS;
|
||||
private double montantIS;
|
||||
private double beneficeNet;
|
||||
private boolean isPME;
|
||||
|
||||
// Getters et Setters complets...
|
||||
public String getTenantId() { return tenantId; }
|
||||
public void setTenantId(String tenantId) { this.tenantId = tenantId; }
|
||||
|
||||
public int getYear() { return year; }
|
||||
public void setYear(int year) { this.year = year; }
|
||||
|
||||
public double getChiffreAffaires() { return chiffreAffaires; }
|
||||
public void setChiffreAffaires(double chiffreAffaires) { this.chiffreAffaires = chiffreAffaires; }
|
||||
|
||||
public double getCharges() { return charges; }
|
||||
public void setCharges(double charges) { this.charges = charges; }
|
||||
|
||||
public double getAmortissements() { return amortissements; }
|
||||
public void setAmortissements(double amortissements) { this.amortissements = amortissements; }
|
||||
|
||||
public double getBeneficeBrut() { return beneficeBrut; }
|
||||
public void setBeneficeBrut(double beneficeBrut) { this.beneficeBrut = beneficeBrut; }
|
||||
|
||||
public double getBeneficeImposable() { return beneficeImposable; }
|
||||
public void setBeneficeImposable(double beneficeImposable) { this.beneficeImposable = beneficeImposable; }
|
||||
|
||||
public double getTauxIS() { return tauxIS; }
|
||||
public void setTauxIS(double tauxIS) { this.tauxIS = tauxIS; }
|
||||
|
||||
public double getMontantIS() { return montantIS; }
|
||||
public void setMontantIS(double montantIS) { this.montantIS = montantIS; }
|
||||
|
||||
public double getBeneficeNet() { return beneficeNet; }
|
||||
public void setBeneficeNet(double beneficeNet) { this.beneficeNet = beneficeNet; }
|
||||
|
||||
public boolean getIsPME() { return isPME; }
|
||||
public void setIsPME(boolean isPME) { this.isPME = isPME; }
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcul CNPS
|
||||
*/
|
||||
class CNPSCalculation {
|
||||
private double salaireBrut;
|
||||
private double salaireImposable;
|
||||
private double tauxEmployeur;
|
||||
private double tauxEmploye;
|
||||
private double cotisationEmployeur;
|
||||
private double cotisationEmploye;
|
||||
private double cotisationTotale;
|
||||
private double salaireNet;
|
||||
|
||||
// Getters et Setters complets...
|
||||
public double getSalaireBrut() { return salaireBrut; }
|
||||
public void setSalaireBrut(double salaireBrut) { this.salaireBrut = salaireBrut; }
|
||||
|
||||
public double getSalaireImposable() { return salaireImposable; }
|
||||
public void setSalaireImposable(double salaireImposable) { this.salaireImposable = salaireImposable; }
|
||||
|
||||
public double getTauxEmployeur() { return tauxEmployeur; }
|
||||
public void setTauxEmployeur(double tauxEmployeur) { this.tauxEmployeur = tauxEmployeur; }
|
||||
|
||||
public double getTauxEmploye() { return tauxEmploye; }
|
||||
public void setTauxEmploye(double tauxEmploye) { this.tauxEmploye = tauxEmploye; }
|
||||
|
||||
public double getCotisationEmployeur() { return cotisationEmployeur; }
|
||||
public void setCotisationEmployeur(double cotisationEmployeur) { this.cotisationEmployeur = cotisationEmployeur; }
|
||||
|
||||
public double getCotisationEmploye() { return cotisationEmploye; }
|
||||
public void setCotisationEmploye(double cotisationEmploye) { this.cotisationEmploye = cotisationEmploye; }
|
||||
|
||||
public double getCotisationTotale() { return cotisationTotale; }
|
||||
public void setCotisationTotale(double cotisationTotale) { this.cotisationTotale = cotisationTotale; }
|
||||
|
||||
public double getSalaireNet() { return salaireNet; }
|
||||
public void setSalaireNet(double salaireNet) { this.salaireNet = salaireNet; }
|
||||
}
|
||||
|
||||
/**
|
||||
* Rapport de conformité
|
||||
*/
|
||||
class ComplianceReport {
|
||||
private String tenantId;
|
||||
private LocalDate checkDate;
|
||||
private List<ComplianceIssue> issues;
|
||||
private double complianceScore;
|
||||
private String status;
|
||||
|
||||
// Getters et Setters
|
||||
public String getTenantId() { return tenantId; }
|
||||
public void setTenantId(String tenantId) { this.tenantId = tenantId; }
|
||||
|
||||
public LocalDate getCheckDate() { return checkDate; }
|
||||
public void setCheckDate(LocalDate checkDate) { this.checkDate = checkDate; }
|
||||
|
||||
public List<ComplianceIssue> getIssues() { return issues; }
|
||||
public void setIssues(List<ComplianceIssue> issues) { this.issues = issues; }
|
||||
|
||||
public double getComplianceScore() { return complianceScore; }
|
||||
public void setComplianceScore(double complianceScore) { this.complianceScore = complianceScore; }
|
||||
|
||||
public String getStatus() { return status; }
|
||||
public void setStatus(String status) { this.status = status; }
|
||||
}
|
||||
|
||||
/**
|
||||
* Problème de conformité
|
||||
*/
|
||||
class ComplianceIssue {
|
||||
private String code;
|
||||
private String description;
|
||||
private String severity;
|
||||
|
||||
public ComplianceIssue(String code, String description, String severity) {
|
||||
this.code = code;
|
||||
this.description = description;
|
||||
this.severity = severity;
|
||||
}
|
||||
|
||||
// Getters
|
||||
public String getCode() { return code; }
|
||||
public String getDescription() { return description; }
|
||||
public String getSeverity() { return severity; }
|
||||
}
|
||||
|
||||
/**
|
||||
* Échéance fiscale
|
||||
*/
|
||||
class TaxDeadline {
|
||||
private String type;
|
||||
private String description;
|
||||
private LocalDate dueDate;
|
||||
private String priority;
|
||||
|
||||
public TaxDeadline(String type, String description, LocalDate dueDate, String priority) {
|
||||
this.type = type;
|
||||
this.description = description;
|
||||
this.dueDate = dueDate;
|
||||
this.priority = priority;
|
||||
}
|
||||
|
||||
// Getters
|
||||
public String getType() { return type; }
|
||||
public String getDescription() { return description; }
|
||||
public LocalDate getDueDate() { return dueDate; }
|
||||
public String getPriority() { return priority; }
|
||||
}
|
||||
197
src/main/java/dev/lions/erp/core/Company.java
Normal file
197
src/main/java/dev/lions/erp/core/Company.java
Normal file
@@ -0,0 +1,197 @@
|
||||
package dev.lions.erp.core;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* Entité Entreprise - Multi-tenant
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "companies")
|
||||
public class Company {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false, unique = true)
|
||||
private String tenantId; // Identifiant unique pour multi-tenant
|
||||
|
||||
@Column(nullable = false)
|
||||
private String name;
|
||||
|
||||
private String legalName;
|
||||
private String registrationNumber; // Numéro RCCM
|
||||
private String taxNumber; // Numéro contribuable DGI
|
||||
private String cnpsNumber; // Numéro CNPS
|
||||
|
||||
// Adresse
|
||||
private String address;
|
||||
private String city;
|
||||
private String postalCode;
|
||||
private String country = "Côte d'Ivoire";
|
||||
|
||||
// Contact
|
||||
private String phone;
|
||||
private String email;
|
||||
private String website;
|
||||
|
||||
// Informations métier
|
||||
private String sector;
|
||||
private String activity;
|
||||
private Integer employeeCount;
|
||||
private String currency = "FCFA";
|
||||
|
||||
// Configuration
|
||||
@Enumerated(EnumType.STRING)
|
||||
private CompanyStatus status = CompanyStatus.ACTIVE;
|
||||
|
||||
@ElementCollection
|
||||
@Enumerated(EnumType.STRING)
|
||||
@CollectionTable(name = "company_modules")
|
||||
private List<ModuleType> enabledModules = new ArrayList<>();
|
||||
|
||||
// Dates
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
private LocalDateTime updatedAt;
|
||||
private LocalDateTime lastLoginAt;
|
||||
|
||||
// Constructeurs
|
||||
public Company() {
|
||||
this.createdAt = LocalDateTime.now();
|
||||
this.updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
public Company(String name, String tenantId) {
|
||||
this();
|
||||
this.name = name;
|
||||
this.tenantId = tenantId;
|
||||
}
|
||||
|
||||
// Méthodes métier
|
||||
public boolean hasModule(ModuleType moduleType) {
|
||||
return enabledModules.contains(moduleType);
|
||||
}
|
||||
|
||||
public void enableModule(ModuleType moduleType) {
|
||||
if (!enabledModules.contains(moduleType)) {
|
||||
enabledModules.add(moduleType);
|
||||
this.updatedAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
|
||||
public void disableModule(ModuleType moduleType) {
|
||||
if (enabledModules.remove(moduleType)) {
|
||||
this.updatedAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isActive() {
|
||||
return status == CompanyStatus.ACTIVE;
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
public void preUpdate() {
|
||||
this.updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
// Getters et Setters
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public String getTenantId() { return tenantId; }
|
||||
public void setTenantId(String tenantId) { this.tenantId = tenantId; }
|
||||
|
||||
public String getName() { return name; }
|
||||
public void setName(String name) { this.name = name; }
|
||||
|
||||
public String getLegalName() { return legalName; }
|
||||
public void setLegalName(String legalName) { this.legalName = legalName; }
|
||||
|
||||
public String getRegistrationNumber() { return registrationNumber; }
|
||||
public void setRegistrationNumber(String registrationNumber) { this.registrationNumber = registrationNumber; }
|
||||
|
||||
public String getTaxNumber() { return taxNumber; }
|
||||
public void setTaxNumber(String taxNumber) { this.taxNumber = taxNumber; }
|
||||
|
||||
public String getCnpsNumber() { return cnpsNumber; }
|
||||
public void setCnpsNumber(String cnpsNumber) { this.cnpsNumber = cnpsNumber; }
|
||||
|
||||
public String getAddress() { return address; }
|
||||
public void setAddress(String address) { this.address = address; }
|
||||
|
||||
public String getCity() { return city; }
|
||||
public void setCity(String city) { this.city = city; }
|
||||
|
||||
public String getPostalCode() { return postalCode; }
|
||||
public void setPostalCode(String postalCode) { this.postalCode = postalCode; }
|
||||
|
||||
public String getCountry() { return country; }
|
||||
public void setCountry(String country) { this.country = country; }
|
||||
|
||||
public String getPhone() { return phone; }
|
||||
public void setPhone(String phone) { this.phone = phone; }
|
||||
|
||||
public String getEmail() { return email; }
|
||||
public void setEmail(String email) { this.email = email; }
|
||||
|
||||
public String getWebsite() { return website; }
|
||||
public void setWebsite(String website) { this.website = website; }
|
||||
|
||||
public String getSector() { return sector; }
|
||||
public void setSector(String sector) { this.sector = sector; }
|
||||
|
||||
public String getActivity() { return activity; }
|
||||
public void setActivity(String activity) { this.activity = activity; }
|
||||
|
||||
public Integer getEmployeeCount() { return employeeCount; }
|
||||
public void setEmployeeCount(Integer employeeCount) { this.employeeCount = employeeCount; }
|
||||
|
||||
public String getCurrency() { return currency; }
|
||||
public void setCurrency(String currency) { this.currency = currency; }
|
||||
|
||||
public CompanyStatus getStatus() { return status; }
|
||||
public void setStatus(CompanyStatus status) { this.status = status; }
|
||||
|
||||
public List<ModuleType> getEnabledModules() { return enabledModules; }
|
||||
public void setEnabledModules(List<ModuleType> enabledModules) { this.enabledModules = enabledModules; }
|
||||
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
|
||||
public LocalDateTime getUpdatedAt() { return updatedAt; }
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
|
||||
|
||||
public LocalDateTime getLastLoginAt() { return lastLoginAt; }
|
||||
public void setLastLoginAt(LocalDateTime lastLoginAt) { this.lastLoginAt = lastLoginAt; }
|
||||
}
|
||||
|
||||
/**
|
||||
* Statut de l'entreprise
|
||||
*/
|
||||
enum CompanyStatus {
|
||||
ACTIVE, // Active
|
||||
SUSPENDED, // Suspendue
|
||||
TRIAL, // En période d'essai
|
||||
EXPIRED // Expirée
|
||||
}
|
||||
|
||||
/**
|
||||
* Types de modules ERP
|
||||
*/
|
||||
enum ModuleType {
|
||||
CRM, // Gestion commerciale
|
||||
STOCK, // Gestion des stocks
|
||||
ACCOUNTING, // Comptabilité
|
||||
HR, // Ressources humaines
|
||||
PROJECT, // Gestion de projets
|
||||
PURCHASE, // Achats
|
||||
SALES, // Ventes
|
||||
INVENTORY, // Inventaire
|
||||
REPORTING, // Rapports
|
||||
DASHBOARD // Tableaux de bord
|
||||
}
|
||||
338
src/main/java/dev/lions/erp/core/TenantService.java
Normal file
338
src/main/java/dev/lions/erp/core/TenantService.java
Normal file
@@ -0,0 +1,338 @@
|
||||
package dev.lions.erp.core;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.transaction.Transactional;
|
||||
import java.util.UUID;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Service de gestion multi-tenant
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class TenantService {
|
||||
|
||||
@Inject
|
||||
EntityManager em;
|
||||
|
||||
/**
|
||||
* Crée une nouvelle entreprise (tenant)
|
||||
*/
|
||||
@Transactional
|
||||
public Company createCompany(String name, String email, String contactName) {
|
||||
// Génération d'un tenant ID unique
|
||||
String tenantId = generateTenantId();
|
||||
|
||||
// Création de l'entreprise
|
||||
Company company = new Company(name, tenantId);
|
||||
company.setEmail(email);
|
||||
|
||||
// Modules par défaut
|
||||
company.enableModule(ModuleType.CRM);
|
||||
company.enableModule(ModuleType.DASHBOARD);
|
||||
|
||||
em.persist(company);
|
||||
|
||||
// Création de l'utilisateur administrateur
|
||||
User admin = createAdminUser(contactName, email, tenantId);
|
||||
|
||||
return company;
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée l'utilisateur administrateur pour une entreprise
|
||||
*/
|
||||
@Transactional
|
||||
public User createAdminUser(String fullName, String email, String tenantId) {
|
||||
String[] names = fullName.split(" ", 2);
|
||||
String firstName = names[0];
|
||||
String lastName = names.length > 1 ? names[1] : "";
|
||||
|
||||
User admin = new User(email, firstName, lastName, tenantId);
|
||||
admin.addRole(UserRole.ADMIN);
|
||||
admin.setPosition("Administrateur");
|
||||
admin.setEmailVerified(true);
|
||||
|
||||
// Permissions complètes
|
||||
admin.addPermission(Permission.USER_MANAGEMENT);
|
||||
admin.addPermission(Permission.COMPANY_SETTINGS);
|
||||
admin.addPermission(Permission.CRM_READ);
|
||||
admin.addPermission(Permission.CRM_WRITE);
|
||||
admin.addPermission(Permission.REPORTS_VIEW);
|
||||
|
||||
// Mot de passe temporaire (à changer au premier login)
|
||||
admin.setPasswordHash(hashPassword("TempPass123!"));
|
||||
|
||||
em.persist(admin);
|
||||
|
||||
return admin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère une entreprise par tenant ID
|
||||
*/
|
||||
public Optional<Company> getCompanyByTenantId(String tenantId) {
|
||||
try {
|
||||
Company company = em.createQuery(
|
||||
"SELECT c FROM Company c WHERE c.tenantId = :tenantId", Company.class)
|
||||
.setParameter("tenantId", tenantId)
|
||||
.getSingleResult();
|
||||
return Optional.of(company);
|
||||
} catch (Exception e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère un utilisateur par email et tenant
|
||||
*/
|
||||
public Optional<User> getUserByEmailAndTenant(String email, String tenantId) {
|
||||
try {
|
||||
User user = em.createQuery(
|
||||
"SELECT u FROM User u WHERE u.email = :email AND u.tenantId = :tenantId", User.class)
|
||||
.setParameter("email", email)
|
||||
.setParameter("tenantId", tenantId)
|
||||
.getSingleResult();
|
||||
return Optional.of(user);
|
||||
} catch (Exception e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Liste les utilisateurs d'une entreprise
|
||||
*/
|
||||
public List<User> getUsersByTenant(String tenantId) {
|
||||
return em.createQuery(
|
||||
"SELECT u FROM User u WHERE u.tenantId = :tenantId ORDER BY u.firstName, u.lastName", User.class)
|
||||
.setParameter("tenantId", tenantId)
|
||||
.getResultList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Active un module pour une entreprise
|
||||
*/
|
||||
@Transactional
|
||||
public void enableModule(String tenantId, ModuleType moduleType) {
|
||||
Company company = getCompanyByTenantId(tenantId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Entreprise non trouvée"));
|
||||
|
||||
company.enableModule(moduleType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Désactive un module pour une entreprise
|
||||
*/
|
||||
@Transactional
|
||||
public void disableModule(String tenantId, ModuleType moduleType) {
|
||||
Company company = getCompanyByTenantId(tenantId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Entreprise non trouvée"));
|
||||
|
||||
company.disableModule(moduleType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si une entreprise a accès à un module
|
||||
*/
|
||||
public boolean hasModuleAccess(String tenantId, ModuleType moduleType) {
|
||||
return getCompanyByTenantId(tenantId)
|
||||
.map(company -> company.hasModule(moduleType))
|
||||
.orElse(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour les informations d'une entreprise
|
||||
*/
|
||||
@Transactional
|
||||
public Company updateCompany(String tenantId, CompanyUpdateDTO updateData) {
|
||||
Company company = getCompanyByTenantId(tenantId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Entreprise non trouvée"));
|
||||
|
||||
if (updateData.getName() != null) {
|
||||
company.setName(updateData.getName());
|
||||
}
|
||||
if (updateData.getAddress() != null) {
|
||||
company.setAddress(updateData.getAddress());
|
||||
}
|
||||
if (updateData.getPhone() != null) {
|
||||
company.setPhone(updateData.getPhone());
|
||||
}
|
||||
if (updateData.getEmail() != null) {
|
||||
company.setEmail(updateData.getEmail());
|
||||
}
|
||||
if (updateData.getSector() != null) {
|
||||
company.setSector(updateData.getSector());
|
||||
}
|
||||
if (updateData.getEmployeeCount() != null) {
|
||||
company.setEmployeeCount(updateData.getEmployeeCount());
|
||||
}
|
||||
|
||||
return company;
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un nouvel utilisateur dans une entreprise
|
||||
*/
|
||||
@Transactional
|
||||
public User createUser(String tenantId, UserCreateDTO userData) {
|
||||
// Vérification que l'entreprise existe
|
||||
getCompanyByTenantId(tenantId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Entreprise non trouvée"));
|
||||
|
||||
// Vérification que l'email n'existe pas déjà
|
||||
if (getUserByEmailAndTenant(userData.getEmail(), tenantId).isPresent()) {
|
||||
throw new IllegalArgumentException("Un utilisateur avec cet email existe déjà");
|
||||
}
|
||||
|
||||
User user = new User(userData.getEmail(), userData.getFirstName(),
|
||||
userData.getLastName(), tenantId);
|
||||
user.setPhone(userData.getPhone());
|
||||
user.setPosition(userData.getPosition());
|
||||
user.setDepartment(userData.getDepartment());
|
||||
|
||||
// Rôle par défaut
|
||||
user.addRole(UserRole.USER);
|
||||
|
||||
// Permissions de base
|
||||
user.addPermission(Permission.CRM_READ);
|
||||
user.addPermission(Permission.REPORTS_VIEW);
|
||||
|
||||
// Mot de passe temporaire
|
||||
user.setPasswordHash(hashPassword("TempPass123!"));
|
||||
|
||||
em.persist(user);
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime une entreprise et tous ses utilisateurs
|
||||
*/
|
||||
@Transactional
|
||||
public void deleteCompany(String tenantId) {
|
||||
// Suppression des utilisateurs
|
||||
em.createQuery("DELETE FROM User u WHERE u.tenantId = :tenantId")
|
||||
.setParameter("tenantId", tenantId)
|
||||
.executeUpdate();
|
||||
|
||||
// Suppression de l'entreprise
|
||||
em.createQuery("DELETE FROM Company c WHERE c.tenantId = :tenantId")
|
||||
.setParameter("tenantId", tenantId)
|
||||
.executeUpdate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Statistiques d'une entreprise
|
||||
*/
|
||||
public TenantStats getTenantStats(String tenantId) {
|
||||
Company company = getCompanyByTenantId(tenantId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Entreprise non trouvée"));
|
||||
|
||||
Long userCount = em.createQuery(
|
||||
"SELECT COUNT(u) FROM User u WHERE u.tenantId = :tenantId", Long.class)
|
||||
.setParameter("tenantId", tenantId)
|
||||
.getSingleResult();
|
||||
|
||||
Long activeUserCount = em.createQuery(
|
||||
"SELECT COUNT(u) FROM User u WHERE u.tenantId = :tenantId AND u.status = :status", Long.class)
|
||||
.setParameter("tenantId", tenantId)
|
||||
.setParameter("status", UserStatus.ACTIVE)
|
||||
.getSingleResult();
|
||||
|
||||
return new TenantStats(company, userCount, activeUserCount);
|
||||
}
|
||||
|
||||
// Méthodes utilitaires
|
||||
private String generateTenantId() {
|
||||
return "tenant_" + UUID.randomUUID().toString().replace("-", "").substring(0, 12);
|
||||
}
|
||||
|
||||
private String hashPassword(String password) {
|
||||
// Implémentation simple - en production, utiliser BCrypt ou Argon2
|
||||
return "hashed_" + password;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DTO pour mise à jour entreprise
|
||||
*/
|
||||
class CompanyUpdateDTO {
|
||||
private String name;
|
||||
private String address;
|
||||
private String phone;
|
||||
private String email;
|
||||
private String sector;
|
||||
private Integer employeeCount;
|
||||
|
||||
// Getters et Setters
|
||||
public String getName() { return name; }
|
||||
public void setName(String name) { this.name = name; }
|
||||
|
||||
public String getAddress() { return address; }
|
||||
public void setAddress(String address) { this.address = address; }
|
||||
|
||||
public String getPhone() { return phone; }
|
||||
public void setPhone(String phone) { this.phone = phone; }
|
||||
|
||||
public String getEmail() { return email; }
|
||||
public void setEmail(String email) { this.email = email; }
|
||||
|
||||
public String getSector() { return sector; }
|
||||
public void setSector(String sector) { this.sector = sector; }
|
||||
|
||||
public Integer getEmployeeCount() { return employeeCount; }
|
||||
public void setEmployeeCount(Integer employeeCount) { this.employeeCount = employeeCount; }
|
||||
}
|
||||
|
||||
/**
|
||||
* DTO pour création utilisateur
|
||||
*/
|
||||
class UserCreateDTO {
|
||||
private String email;
|
||||
private String firstName;
|
||||
private String lastName;
|
||||
private String phone;
|
||||
private String position;
|
||||
private String department;
|
||||
|
||||
// Getters et Setters
|
||||
public String getEmail() { return email; }
|
||||
public void setEmail(String email) { this.email = email; }
|
||||
|
||||
public String getFirstName() { return firstName; }
|
||||
public void setFirstName(String firstName) { this.firstName = firstName; }
|
||||
|
||||
public String getLastName() { return lastName; }
|
||||
public void setLastName(String lastName) { this.lastName = lastName; }
|
||||
|
||||
public String getPhone() { return phone; }
|
||||
public void setPhone(String phone) { this.phone = phone; }
|
||||
|
||||
public String getPosition() { return position; }
|
||||
public void setPosition(String position) { this.position = position; }
|
||||
|
||||
public String getDepartment() { return department; }
|
||||
public void setDepartment(String department) { this.department = department; }
|
||||
}
|
||||
|
||||
/**
|
||||
* Statistiques tenant
|
||||
*/
|
||||
class TenantStats {
|
||||
private Company company;
|
||||
private Long totalUsers;
|
||||
private Long activeUsers;
|
||||
|
||||
public TenantStats(Company company, Long totalUsers, Long activeUsers) {
|
||||
this.company = company;
|
||||
this.totalUsers = totalUsers;
|
||||
this.activeUsers = activeUsers;
|
||||
}
|
||||
|
||||
// Getters
|
||||
public Company getCompany() { return company; }
|
||||
public Long getTotalUsers() { return totalUsers; }
|
||||
public Long getActiveUsers() { return activeUsers; }
|
||||
}
|
||||
257
src/main/java/dev/lions/erp/core/User.java
Normal file
257
src/main/java/dev/lions/erp/core/User.java
Normal file
@@ -0,0 +1,257 @@
|
||||
package dev.lions.erp.core;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Set;
|
||||
import java.util.HashSet;
|
||||
|
||||
/**
|
||||
* Entité Utilisateur - Multi-tenant
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "users", uniqueConstraints = {
|
||||
@UniqueConstraint(columnNames = {"email", "tenantId"})
|
||||
})
|
||||
public class User {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String tenantId; // Lien vers l'entreprise
|
||||
|
||||
@Column(nullable = false)
|
||||
private String email;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String firstName;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String lastName;
|
||||
|
||||
private String phone;
|
||||
private String position; // Poste dans l'entreprise
|
||||
private String department;
|
||||
|
||||
// Authentification
|
||||
@Column(nullable = false)
|
||||
private String passwordHash;
|
||||
|
||||
private String resetToken;
|
||||
private LocalDateTime resetTokenExpiry;
|
||||
|
||||
// Permissions
|
||||
@ElementCollection
|
||||
@Enumerated(EnumType.STRING)
|
||||
@CollectionTable(name = "user_roles")
|
||||
private Set<UserRole> roles = new HashSet<>();
|
||||
|
||||
@ElementCollection
|
||||
@Enumerated(EnumType.STRING)
|
||||
@CollectionTable(name = "user_permissions")
|
||||
private Set<Permission> permissions = new HashSet<>();
|
||||
|
||||
// Statut
|
||||
@Enumerated(EnumType.STRING)
|
||||
private UserStatus status = UserStatus.ACTIVE;
|
||||
|
||||
private Boolean emailVerified = false;
|
||||
private String emailVerificationToken;
|
||||
|
||||
// Dates
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
private LocalDateTime updatedAt;
|
||||
private LocalDateTime lastLoginAt;
|
||||
private LocalDateTime lastActivityAt;
|
||||
|
||||
// Préférences
|
||||
private String language = "fr";
|
||||
private String timezone = "Africa/Abidjan";
|
||||
private String theme = "light";
|
||||
|
||||
// Constructeurs
|
||||
public User() {
|
||||
this.createdAt = LocalDateTime.now();
|
||||
this.updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
public User(String email, String firstName, String lastName, String tenantId) {
|
||||
this();
|
||||
this.email = email;
|
||||
this.firstName = firstName;
|
||||
this.lastName = lastName;
|
||||
this.tenantId = tenantId;
|
||||
}
|
||||
|
||||
// Méthodes métier
|
||||
public String getFullName() {
|
||||
return firstName + " " + lastName;
|
||||
}
|
||||
|
||||
public boolean hasRole(UserRole role) {
|
||||
return roles.contains(role);
|
||||
}
|
||||
|
||||
public boolean hasPermission(Permission permission) {
|
||||
return permissions.contains(permission) || hasAdminRole();
|
||||
}
|
||||
|
||||
public boolean hasAdminRole() {
|
||||
return roles.contains(UserRole.ADMIN) || roles.contains(UserRole.SUPER_ADMIN);
|
||||
}
|
||||
|
||||
public void addRole(UserRole role) {
|
||||
roles.add(role);
|
||||
this.updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
public void removeRole(UserRole role) {
|
||||
roles.remove(role);
|
||||
this.updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
public void addPermission(Permission permission) {
|
||||
permissions.add(permission);
|
||||
this.updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
public void removePermission(Permission permission) {
|
||||
permissions.remove(permission);
|
||||
this.updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
public boolean isActive() {
|
||||
return status == UserStatus.ACTIVE;
|
||||
}
|
||||
|
||||
public void updateLastActivity() {
|
||||
this.lastActivityAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
public void updateLastLogin() {
|
||||
this.lastLoginAt = LocalDateTime.now();
|
||||
this.lastActivityAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
public void preUpdate() {
|
||||
this.updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
// Getters et Setters
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public String getTenantId() { return tenantId; }
|
||||
public void setTenantId(String tenantId) { this.tenantId = tenantId; }
|
||||
|
||||
public String getEmail() { return email; }
|
||||
public void setEmail(String email) { this.email = email; }
|
||||
|
||||
public String getFirstName() { return firstName; }
|
||||
public void setFirstName(String firstName) { this.firstName = firstName; }
|
||||
|
||||
public String getLastName() { return lastName; }
|
||||
public void setLastName(String lastName) { this.lastName = lastName; }
|
||||
|
||||
public String getPhone() { return phone; }
|
||||
public void setPhone(String phone) { this.phone = phone; }
|
||||
|
||||
public String getPosition() { return position; }
|
||||
public void setPosition(String position) { this.position = position; }
|
||||
|
||||
public String getDepartment() { return department; }
|
||||
public void setDepartment(String department) { this.department = department; }
|
||||
|
||||
public String getPasswordHash() { return passwordHash; }
|
||||
public void setPasswordHash(String passwordHash) { this.passwordHash = passwordHash; }
|
||||
|
||||
public String getResetToken() { return resetToken; }
|
||||
public void setResetToken(String resetToken) { this.resetToken = resetToken; }
|
||||
|
||||
public LocalDateTime getResetTokenExpiry() { return resetTokenExpiry; }
|
||||
public void setResetTokenExpiry(LocalDateTime resetTokenExpiry) { this.resetTokenExpiry = resetTokenExpiry; }
|
||||
|
||||
public Set<UserRole> getRoles() { return roles; }
|
||||
public void setRoles(Set<UserRole> roles) { this.roles = roles; }
|
||||
|
||||
public Set<Permission> getPermissions() { return permissions; }
|
||||
public void setPermissions(Set<Permission> permissions) { this.permissions = permissions; }
|
||||
|
||||
public UserStatus getStatus() { return status; }
|
||||
public void setStatus(UserStatus status) { this.status = status; }
|
||||
|
||||
public Boolean getEmailVerified() { return emailVerified; }
|
||||
public void setEmailVerified(Boolean emailVerified) { this.emailVerified = emailVerified; }
|
||||
|
||||
public String getEmailVerificationToken() { return emailVerificationToken; }
|
||||
public void setEmailVerificationToken(String emailVerificationToken) { this.emailVerificationToken = emailVerificationToken; }
|
||||
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
|
||||
public LocalDateTime getUpdatedAt() { return updatedAt; }
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
|
||||
|
||||
public LocalDateTime getLastLoginAt() { return lastLoginAt; }
|
||||
public void setLastLoginAt(LocalDateTime lastLoginAt) { this.lastLoginAt = lastLoginAt; }
|
||||
|
||||
public LocalDateTime getLastActivityAt() { return lastActivityAt; }
|
||||
public void setLastActivityAt(LocalDateTime lastActivityAt) { this.lastActivityAt = lastActivityAt; }
|
||||
|
||||
public String getLanguage() { return language; }
|
||||
public void setLanguage(String language) { this.language = language; }
|
||||
|
||||
public String getTimezone() { return timezone; }
|
||||
public void setTimezone(String timezone) { this.timezone = timezone; }
|
||||
|
||||
public String getTheme() { return theme; }
|
||||
public void setTheme(String theme) { this.theme = theme; }
|
||||
}
|
||||
|
||||
/**
|
||||
* Rôles utilisateur
|
||||
*/
|
||||
enum UserRole {
|
||||
SUPER_ADMIN, // Super administrateur Lions Dev
|
||||
ADMIN, // Administrateur entreprise
|
||||
MANAGER, // Manager/Responsable
|
||||
USER, // Utilisateur standard
|
||||
VIEWER // Consultation uniquement
|
||||
}
|
||||
|
||||
/**
|
||||
* Permissions spécifiques
|
||||
*/
|
||||
enum Permission {
|
||||
// CRM
|
||||
CRM_READ, CRM_WRITE, CRM_DELETE,
|
||||
|
||||
// Stock
|
||||
STOCK_READ, STOCK_WRITE, STOCK_DELETE,
|
||||
|
||||
// Comptabilité
|
||||
ACCOUNTING_READ, ACCOUNTING_WRITE, ACCOUNTING_DELETE,
|
||||
|
||||
// RH
|
||||
HR_READ, HR_WRITE, HR_DELETE,
|
||||
|
||||
// Administration
|
||||
USER_MANAGEMENT, COMPANY_SETTINGS, SYSTEM_CONFIG,
|
||||
|
||||
// Rapports
|
||||
REPORTS_VIEW, REPORTS_EXPORT, REPORTS_CREATE
|
||||
}
|
||||
|
||||
/**
|
||||
* Statut utilisateur
|
||||
*/
|
||||
enum UserStatus {
|
||||
ACTIVE, // Actif
|
||||
INACTIVE, // Inactif
|
||||
SUSPENDED, // Suspendu
|
||||
PENDING_VERIFICATION // En attente de vérification
|
||||
}
|
||||
11
src/main/java/dev/lions/quote/ComplexityLevel.java
Normal file
11
src/main/java/dev/lions/quote/ComplexityLevel.java
Normal file
@@ -0,0 +1,11 @@
|
||||
package dev.lions.quote;
|
||||
|
||||
/**
|
||||
* Niveau de complexité d'un module
|
||||
*/
|
||||
public enum ComplexityLevel {
|
||||
BASIC, // Basique (-20%)
|
||||
STANDARD, // Standard (prix de base)
|
||||
ADVANCED, // Avancé (+30%)
|
||||
ENTERPRISE // Enterprise (+60%)
|
||||
}
|
||||
158
src/main/java/dev/lions/quote/ModuleCatalog.java
Normal file
158
src/main/java/dev/lions/quote/ModuleCatalog.java
Normal file
@@ -0,0 +1,158 @@
|
||||
package dev.lions.quote;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* Catalogue des modules disponibles avec tarification
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "module_catalog")
|
||||
public class ModuleCatalog {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false, unique = true)
|
||||
private String moduleCode;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String moduleName;
|
||||
|
||||
@Column(length = 2000)
|
||||
private String description;
|
||||
|
||||
@Column(length = 3000)
|
||||
private String features; // Fonctionnalités principales
|
||||
|
||||
// Tarification par niveau
|
||||
private Double basicPrice; // Prix niveau basique
|
||||
private Double standardPrice; // Prix niveau standard
|
||||
private Double advancedPrice; // Prix niveau avancé
|
||||
private Double enterprisePrice; // Prix niveau enterprise
|
||||
|
||||
// Détails techniques
|
||||
@Column(length = 2000)
|
||||
private String technicalRequirements;
|
||||
|
||||
private Integer baseImplementationDays;
|
||||
private Integer maxUsers;
|
||||
private String supportLevel;
|
||||
|
||||
// Prérequis et dépendances
|
||||
@ElementCollection
|
||||
@CollectionTable(name = "module_prerequisites")
|
||||
private List<String> prerequisites = new ArrayList<>();
|
||||
|
||||
@ElementCollection
|
||||
@CollectionTable(name = "module_integrations")
|
||||
private List<String> integrations = new ArrayList<>();
|
||||
|
||||
// Métadonnées
|
||||
private String category; // commercial, stock, comptabilite, rh, infrastructure
|
||||
private Integer displayOrder;
|
||||
private Boolean active = true;
|
||||
private Boolean popular = false;
|
||||
|
||||
// Constructeurs
|
||||
public ModuleCatalog() {}
|
||||
|
||||
public ModuleCatalog(String moduleCode, String moduleName, String category) {
|
||||
this.moduleCode = moduleCode;
|
||||
this.moduleName = moduleName;
|
||||
this.category = category;
|
||||
}
|
||||
|
||||
// Méthodes métier
|
||||
public Double getPriceForComplexity(ComplexityLevel complexity) {
|
||||
return switch (complexity) {
|
||||
case BASIC -> basicPrice;
|
||||
case STANDARD -> standardPrice;
|
||||
case ADVANCED -> advancedPrice;
|
||||
case ENTERPRISE -> enterprisePrice;
|
||||
};
|
||||
}
|
||||
|
||||
public ComplexityLevel getRecommendedComplexity(Double auditScore, Integer employeeCount) {
|
||||
// Logique de recommandation basée sur l'audit et la taille
|
||||
if (auditScore < 30 || employeeCount <= 5) {
|
||||
return ComplexityLevel.BASIC;
|
||||
} else if (auditScore < 60 || employeeCount <= 20) {
|
||||
return ComplexityLevel.STANDARD;
|
||||
} else if (auditScore < 80 || employeeCount <= 50) {
|
||||
return ComplexityLevel.ADVANCED;
|
||||
} else {
|
||||
return ComplexityLevel.ENTERPRISE;
|
||||
}
|
||||
}
|
||||
|
||||
public Integer getEstimatedImplementationDays(ComplexityLevel complexity) {
|
||||
double multiplier = switch (complexity) {
|
||||
case BASIC -> 0.7;
|
||||
case STANDARD -> 1.0;
|
||||
case ADVANCED -> 1.4;
|
||||
case ENTERPRISE -> 2.0;
|
||||
};
|
||||
return (int) Math.ceil(baseImplementationDays * multiplier);
|
||||
}
|
||||
|
||||
// Getters et Setters
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public String getModuleCode() { return moduleCode; }
|
||||
public void setModuleCode(String moduleCode) { this.moduleCode = moduleCode; }
|
||||
|
||||
public String getModuleName() { return moduleName; }
|
||||
public void setModuleName(String moduleName) { this.moduleName = moduleName; }
|
||||
|
||||
public String getDescription() { return description; }
|
||||
public void setDescription(String description) { this.description = description; }
|
||||
|
||||
public String getFeatures() { return features; }
|
||||
public void setFeatures(String features) { this.features = features; }
|
||||
|
||||
public Double getBasicPrice() { return basicPrice; }
|
||||
public void setBasicPrice(Double basicPrice) { this.basicPrice = basicPrice; }
|
||||
|
||||
public Double getStandardPrice() { return standardPrice; }
|
||||
public void setStandardPrice(Double standardPrice) { this.standardPrice = standardPrice; }
|
||||
|
||||
public Double getAdvancedPrice() { return advancedPrice; }
|
||||
public void setAdvancedPrice(Double advancedPrice) { this.advancedPrice = advancedPrice; }
|
||||
|
||||
public Double getEnterprisePrice() { return enterprisePrice; }
|
||||
public void setEnterprisePrice(Double enterprisePrice) { this.enterprisePrice = enterprisePrice; }
|
||||
|
||||
public String getTechnicalRequirements() { return technicalRequirements; }
|
||||
public void setTechnicalRequirements(String technicalRequirements) { this.technicalRequirements = technicalRequirements; }
|
||||
|
||||
public Integer getBaseImplementationDays() { return baseImplementationDays; }
|
||||
public void setBaseImplementationDays(Integer baseImplementationDays) { this.baseImplementationDays = baseImplementationDays; }
|
||||
|
||||
public Integer getMaxUsers() { return maxUsers; }
|
||||
public void setMaxUsers(Integer maxUsers) { this.maxUsers = maxUsers; }
|
||||
|
||||
public String getSupportLevel() { return supportLevel; }
|
||||
public void setSupportLevel(String supportLevel) { this.supportLevel = supportLevel; }
|
||||
|
||||
public List<String> getPrerequisites() { return prerequisites; }
|
||||
public void setPrerequisites(List<String> prerequisites) { this.prerequisites = prerequisites; }
|
||||
|
||||
public List<String> getIntegrations() { return integrations; }
|
||||
public void setIntegrations(List<String> integrations) { this.integrations = integrations; }
|
||||
|
||||
public String getCategory() { return category; }
|
||||
public void setCategory(String category) { this.category = category; }
|
||||
|
||||
public Integer getDisplayOrder() { return displayOrder; }
|
||||
public void setDisplayOrder(Integer displayOrder) { this.displayOrder = displayOrder; }
|
||||
|
||||
public Boolean getActive() { return active; }
|
||||
public void setActive(Boolean active) { this.active = active; }
|
||||
|
||||
public Boolean getPopular() { return popular; }
|
||||
public void setPopular(Boolean popular) { this.popular = popular; }
|
||||
}
|
||||
245
src/main/java/dev/lions/quote/Quote.java
Normal file
245
src/main/java/dev/lions/quote/Quote.java
Normal file
@@ -0,0 +1,245 @@
|
||||
package dev.lions.quote;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* Devis personnalisé généré pour une PME
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "quotes")
|
||||
public class Quote {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false, unique = true)
|
||||
private String quoteNumber; // QUO-2024-001
|
||||
|
||||
// Informations client
|
||||
@Column(nullable = false)
|
||||
private String companyName;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String contactName;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String email;
|
||||
|
||||
private String phone;
|
||||
private String address;
|
||||
private String sector;
|
||||
private Integer employeeCount;
|
||||
|
||||
// Référence audit
|
||||
private Long auditId;
|
||||
private Double auditScore;
|
||||
|
||||
// Modules sélectionnés
|
||||
@OneToMany(mappedBy = "quote", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
|
||||
private List<QuoteModule> modules = new ArrayList<>();
|
||||
|
||||
// Tarification
|
||||
private Double subtotalHT; // Sous-total HT
|
||||
private Double discountPercentage = 0.0; // Remise %
|
||||
private Double discountAmount = 0.0; // Montant remise
|
||||
private Double totalHT; // Total HT après remise
|
||||
private Double vatRate = 18.0; // TVA 18% Côte d'Ivoire
|
||||
private Double vatAmount; // Montant TVA
|
||||
private Double totalTTC; // Total TTC
|
||||
|
||||
// Services additionnels
|
||||
private Double formationHours = 0.0;
|
||||
private Double formationRate = 15000.0; // 15K FCFA/heure
|
||||
private Double supportMonths = 0.0;
|
||||
private Double supportRate = 25000.0; // 25K FCFA/mois
|
||||
|
||||
// Conditions
|
||||
private Integer validityDays = 30; // Validité 30 jours
|
||||
private String paymentTerms = "50% à la commande, 50% à la livraison";
|
||||
private String deliveryTerms = "6-8 semaines après signature";
|
||||
|
||||
// Statut
|
||||
@Enumerated(EnumType.STRING)
|
||||
private QuoteStatus status = QuoteStatus.DRAFT;
|
||||
|
||||
// Dates
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
private LocalDateTime sentAt;
|
||||
private LocalDateTime viewedAt;
|
||||
private LocalDateTime acceptedAt;
|
||||
private LocalDateTime expiredAt;
|
||||
|
||||
// Suivi commercial
|
||||
private String salesNotes;
|
||||
private String clientFeedback;
|
||||
|
||||
// Constructeurs
|
||||
public Quote() {
|
||||
this.createdAt = LocalDateTime.now();
|
||||
this.expiredAt = LocalDateTime.now().plusDays(validityDays);
|
||||
}
|
||||
|
||||
public Quote(String companyName, String contactName, String email) {
|
||||
this();
|
||||
this.companyName = companyName;
|
||||
this.contactName = contactName;
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
// Méthodes métier
|
||||
public void calculateTotals() {
|
||||
// Calcul sous-total modules
|
||||
this.subtotalHT = modules.stream()
|
||||
.mapToDouble(m -> m.getUnitPrice() * m.getQuantity())
|
||||
.sum();
|
||||
|
||||
// Ajout formation et support
|
||||
this.subtotalHT += (formationHours * formationRate);
|
||||
this.subtotalHT += (supportMonths * supportRate);
|
||||
|
||||
// Calcul remise
|
||||
this.discountAmount = subtotalHT * (discountPercentage / 100);
|
||||
this.totalHT = subtotalHT - discountAmount;
|
||||
|
||||
// Calcul TVA
|
||||
this.vatAmount = totalHT * (vatRate / 100);
|
||||
this.totalTTC = totalHT + vatAmount;
|
||||
}
|
||||
|
||||
public void generateQuoteNumber() {
|
||||
if (this.quoteNumber == null) {
|
||||
int year = LocalDateTime.now().getYear();
|
||||
// Le numéro sera généré par le service avec séquence
|
||||
this.quoteNumber = String.format("QUO-%d-XXX", year);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isExpired() {
|
||||
return LocalDateTime.now().isAfter(expiredAt) && status == QuoteStatus.SENT;
|
||||
}
|
||||
|
||||
public boolean isValid() {
|
||||
return LocalDateTime.now().isBefore(expiredAt) && status == QuoteStatus.SENT;
|
||||
}
|
||||
|
||||
// Getters et Setters
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public String getQuoteNumber() { return quoteNumber; }
|
||||
public void setQuoteNumber(String quoteNumber) { this.quoteNumber = quoteNumber; }
|
||||
|
||||
public String getCompanyName() { return companyName; }
|
||||
public void setCompanyName(String companyName) { this.companyName = companyName; }
|
||||
|
||||
public String getContactName() { return contactName; }
|
||||
public void setContactName(String contactName) { this.contactName = contactName; }
|
||||
|
||||
public String getEmail() { return email; }
|
||||
public void setEmail(String email) { this.email = email; }
|
||||
|
||||
public String getPhone() { return phone; }
|
||||
public void setPhone(String phone) { this.phone = phone; }
|
||||
|
||||
public String getAddress() { return address; }
|
||||
public void setAddress(String address) { this.address = address; }
|
||||
|
||||
public String getSector() { return sector; }
|
||||
public void setSector(String sector) { this.sector = sector; }
|
||||
|
||||
public Integer getEmployeeCount() { return employeeCount; }
|
||||
public void setEmployeeCount(Integer employeeCount) { this.employeeCount = employeeCount; }
|
||||
|
||||
public Long getAuditId() { return auditId; }
|
||||
public void setAuditId(Long auditId) { this.auditId = auditId; }
|
||||
|
||||
public Double getAuditScore() { return auditScore; }
|
||||
public void setAuditScore(Double auditScore) { this.auditScore = auditScore; }
|
||||
|
||||
public List<QuoteModule> getModules() { return modules; }
|
||||
public void setModules(List<QuoteModule> modules) { this.modules = modules; }
|
||||
|
||||
public Double getSubtotalHT() { return subtotalHT; }
|
||||
public void setSubtotalHT(Double subtotalHT) { this.subtotalHT = subtotalHT; }
|
||||
|
||||
public Double getDiscountPercentage() { return discountPercentage; }
|
||||
public void setDiscountPercentage(Double discountPercentage) { this.discountPercentage = discountPercentage; }
|
||||
|
||||
public Double getDiscountAmount() { return discountAmount; }
|
||||
public void setDiscountAmount(Double discountAmount) { this.discountAmount = discountAmount; }
|
||||
|
||||
public Double getTotalHT() { return totalHT; }
|
||||
public void setTotalHT(Double totalHT) { this.totalHT = totalHT; }
|
||||
|
||||
public Double getVatRate() { return vatRate; }
|
||||
public void setVatRate(Double vatRate) { this.vatRate = vatRate; }
|
||||
|
||||
public Double getVatAmount() { return vatAmount; }
|
||||
public void setVatAmount(Double vatAmount) { this.vatAmount = vatAmount; }
|
||||
|
||||
public Double getTotalTTC() { return totalTTC; }
|
||||
public void setTotalTTC(Double totalTTC) { this.totalTTC = totalTTC; }
|
||||
|
||||
public Double getFormationHours() { return formationHours; }
|
||||
public void setFormationHours(Double formationHours) { this.formationHours = formationHours; }
|
||||
|
||||
public Double getFormationRate() { return formationRate; }
|
||||
public void setFormationRate(Double formationRate) { this.formationRate = formationRate; }
|
||||
|
||||
public Double getSupportMonths() { return supportMonths; }
|
||||
public void setSupportMonths(Double supportMonths) { this.supportMonths = supportMonths; }
|
||||
|
||||
public Double getSupportRate() { return supportRate; }
|
||||
public void setSupportRate(Double supportRate) { this.supportRate = supportRate; }
|
||||
|
||||
public Integer getValidityDays() { return validityDays; }
|
||||
public void setValidityDays(Integer validityDays) { this.validityDays = validityDays; }
|
||||
|
||||
public String getPaymentTerms() { return paymentTerms; }
|
||||
public void setPaymentTerms(String paymentTerms) { this.paymentTerms = paymentTerms; }
|
||||
|
||||
public String getDeliveryTerms() { return deliveryTerms; }
|
||||
public void setDeliveryTerms(String deliveryTerms) { this.deliveryTerms = deliveryTerms; }
|
||||
|
||||
public QuoteStatus getStatus() { return status; }
|
||||
public void setStatus(QuoteStatus status) { this.status = status; }
|
||||
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
|
||||
public LocalDateTime getSentAt() { return sentAt; }
|
||||
public void setSentAt(LocalDateTime sentAt) { this.sentAt = sentAt; }
|
||||
|
||||
public LocalDateTime getViewedAt() { return viewedAt; }
|
||||
public void setViewedAt(LocalDateTime viewedAt) { this.viewedAt = viewedAt; }
|
||||
|
||||
public LocalDateTime getAcceptedAt() { return acceptedAt; }
|
||||
public void setAcceptedAt(LocalDateTime acceptedAt) { this.acceptedAt = acceptedAt; }
|
||||
|
||||
public LocalDateTime getExpiredAt() { return expiredAt; }
|
||||
public void setExpiredAt(LocalDateTime expiredAt) { this.expiredAt = expiredAt; }
|
||||
|
||||
public String getSalesNotes() { return salesNotes; }
|
||||
public void setSalesNotes(String salesNotes) { this.salesNotes = salesNotes; }
|
||||
|
||||
public String getClientFeedback() { return clientFeedback; }
|
||||
public void setClientFeedback(String clientFeedback) { this.clientFeedback = clientFeedback; }
|
||||
}
|
||||
|
||||
/**
|
||||
* Statut du devis
|
||||
*/
|
||||
enum QuoteStatus {
|
||||
DRAFT, // Brouillon
|
||||
SENT, // Envoyé
|
||||
VIEWED, // Consulté par le client
|
||||
ACCEPTED, // Accepté
|
||||
REJECTED, // Refusé
|
||||
EXPIRED // Expiré
|
||||
}
|
||||
42
src/main/java/dev/lions/quote/QuoteCustomizationDTO.java
Normal file
42
src/main/java/dev/lions/quote/QuoteCustomizationDTO.java
Normal file
@@ -0,0 +1,42 @@
|
||||
package dev.lions.quote;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* DTO pour la personnalisation de devis
|
||||
*/
|
||||
public class QuoteCustomizationDTO {
|
||||
|
||||
private List<QuoteModuleDTO> modules;
|
||||
private Double formationHours;
|
||||
private Double supportMonths;
|
||||
private String paymentTerms;
|
||||
private String deliveryTerms;
|
||||
private Double discountPercentage;
|
||||
private String discountReason;
|
||||
|
||||
// Constructeurs
|
||||
public QuoteCustomizationDTO() {}
|
||||
|
||||
// Getters et Setters
|
||||
public List<QuoteModuleDTO> getModules() { return modules; }
|
||||
public void setModules(List<QuoteModuleDTO> modules) { this.modules = modules; }
|
||||
|
||||
public Double getFormationHours() { return formationHours; }
|
||||
public void setFormationHours(Double formationHours) { this.formationHours = formationHours; }
|
||||
|
||||
public Double getSupportMonths() { return supportMonths; }
|
||||
public void setSupportMonths(Double supportMonths) { this.supportMonths = supportMonths; }
|
||||
|
||||
public String getPaymentTerms() { return paymentTerms; }
|
||||
public void setPaymentTerms(String paymentTerms) { this.paymentTerms = paymentTerms; }
|
||||
|
||||
public String getDeliveryTerms() { return deliveryTerms; }
|
||||
public void setDeliveryTerms(String deliveryTerms) { this.deliveryTerms = deliveryTerms; }
|
||||
|
||||
public Double getDiscountPercentage() { return discountPercentage; }
|
||||
public void setDiscountPercentage(Double discountPercentage) { this.discountPercentage = discountPercentage; }
|
||||
|
||||
public String getDiscountReason() { return discountReason; }
|
||||
public void setDiscountReason(String discountReason) { this.discountReason = discountReason; }
|
||||
}
|
||||
181
src/main/java/dev/lions/quote/QuoteDTO.java
Normal file
181
src/main/java/dev/lions/quote/QuoteDTO.java
Normal file
@@ -0,0 +1,181 @@
|
||||
package dev.lions.quote;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* DTOs pour les devis
|
||||
*/
|
||||
public class QuoteDTO {
|
||||
|
||||
private Long id;
|
||||
private String quoteNumber;
|
||||
private String companyName;
|
||||
private String contactName;
|
||||
private String email;
|
||||
private String phone;
|
||||
private String sector;
|
||||
private Integer employeeCount;
|
||||
private Long auditId;
|
||||
private Double auditScore;
|
||||
|
||||
private List<QuoteModuleDTO> modules = new ArrayList<>();
|
||||
|
||||
private Double subtotalHT;
|
||||
private Double discountPercentage;
|
||||
private Double discountAmount;
|
||||
private Double totalHT;
|
||||
private Double vatRate;
|
||||
private Double vatAmount;
|
||||
private Double totalTTC;
|
||||
|
||||
private Double formationHours;
|
||||
private Double formationRate;
|
||||
private Double supportMonths;
|
||||
private Double supportRate;
|
||||
|
||||
private String paymentTerms;
|
||||
private String deliveryTerms;
|
||||
private Integer validityDays;
|
||||
|
||||
private QuoteStatus status;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime sentAt;
|
||||
private LocalDateTime expiredAt;
|
||||
|
||||
// Constructeurs
|
||||
public QuoteDTO() {}
|
||||
|
||||
public QuoteDTO(Quote quote) {
|
||||
this.id = quote.getId();
|
||||
this.quoteNumber = quote.getQuoteNumber();
|
||||
this.companyName = quote.getCompanyName();
|
||||
this.contactName = quote.getContactName();
|
||||
this.email = quote.getEmail();
|
||||
this.phone = quote.getPhone();
|
||||
this.sector = quote.getSector();
|
||||
this.employeeCount = quote.getEmployeeCount();
|
||||
this.auditId = quote.getAuditId();
|
||||
this.auditScore = quote.getAuditScore();
|
||||
|
||||
this.subtotalHT = quote.getSubtotalHT();
|
||||
this.discountPercentage = quote.getDiscountPercentage();
|
||||
this.discountAmount = quote.getDiscountAmount();
|
||||
this.totalHT = quote.getTotalHT();
|
||||
this.vatRate = quote.getVatRate();
|
||||
this.vatAmount = quote.getVatAmount();
|
||||
this.totalTTC = quote.getTotalTTC();
|
||||
|
||||
this.formationHours = quote.getFormationHours();
|
||||
this.formationRate = quote.getFormationRate();
|
||||
this.supportMonths = quote.getSupportMonths();
|
||||
this.supportRate = quote.getSupportRate();
|
||||
|
||||
this.paymentTerms = quote.getPaymentTerms();
|
||||
this.deliveryTerms = quote.getDeliveryTerms();
|
||||
this.validityDays = quote.getValidityDays();
|
||||
|
||||
this.status = quote.getStatus();
|
||||
this.createdAt = quote.getCreatedAt();
|
||||
this.sentAt = quote.getSentAt();
|
||||
this.expiredAt = quote.getExpiredAt();
|
||||
|
||||
// Conversion des modules
|
||||
if (quote.getModules() != null) {
|
||||
for (QuoteModule module : quote.getModules()) {
|
||||
this.modules.add(new QuoteModuleDTO(module));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Getters et Setters
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public String getQuoteNumber() { return quoteNumber; }
|
||||
public void setQuoteNumber(String quoteNumber) { this.quoteNumber = quoteNumber; }
|
||||
|
||||
public String getCompanyName() { return companyName; }
|
||||
public void setCompanyName(String companyName) { this.companyName = companyName; }
|
||||
|
||||
public String getContactName() { return contactName; }
|
||||
public void setContactName(String contactName) { this.contactName = contactName; }
|
||||
|
||||
public String getEmail() { return email; }
|
||||
public void setEmail(String email) { this.email = email; }
|
||||
|
||||
public String getPhone() { return phone; }
|
||||
public void setPhone(String phone) { this.phone = phone; }
|
||||
|
||||
public String getSector() { return sector; }
|
||||
public void setSector(String sector) { this.sector = sector; }
|
||||
|
||||
public Integer getEmployeeCount() { return employeeCount; }
|
||||
public void setEmployeeCount(Integer employeeCount) { this.employeeCount = employeeCount; }
|
||||
|
||||
public Long getAuditId() { return auditId; }
|
||||
public void setAuditId(Long auditId) { this.auditId = auditId; }
|
||||
|
||||
public Double getAuditScore() { return auditScore; }
|
||||
public void setAuditScore(Double auditScore) { this.auditScore = auditScore; }
|
||||
|
||||
public List<QuoteModuleDTO> getModules() { return modules; }
|
||||
public void setModules(List<QuoteModuleDTO> modules) { this.modules = modules; }
|
||||
|
||||
public Double getSubtotalHT() { return subtotalHT; }
|
||||
public void setSubtotalHT(Double subtotalHT) { this.subtotalHT = subtotalHT; }
|
||||
|
||||
public Double getDiscountPercentage() { return discountPercentage; }
|
||||
public void setDiscountPercentage(Double discountPercentage) { this.discountPercentage = discountPercentage; }
|
||||
|
||||
public Double getDiscountAmount() { return discountAmount; }
|
||||
public void setDiscountAmount(Double discountAmount) { this.discountAmount = discountAmount; }
|
||||
|
||||
public Double getTotalHT() { return totalHT; }
|
||||
public void setTotalHT(Double totalHT) { this.totalHT = totalHT; }
|
||||
|
||||
public Double getVatRate() { return vatRate; }
|
||||
public void setVatRate(Double vatRate) { this.vatRate = vatRate; }
|
||||
|
||||
public Double getVatAmount() { return vatAmount; }
|
||||
public void setVatAmount(Double vatAmount) { this.vatAmount = vatAmount; }
|
||||
|
||||
public Double getTotalTTC() { return totalTTC; }
|
||||
public void setTotalTTC(Double totalTTC) { this.totalTTC = totalTTC; }
|
||||
|
||||
public Double getFormationHours() { return formationHours; }
|
||||
public void setFormationHours(Double formationHours) { this.formationHours = formationHours; }
|
||||
|
||||
public Double getFormationRate() { return formationRate; }
|
||||
public void setFormationRate(Double formationRate) { this.formationRate = formationRate; }
|
||||
|
||||
public Double getSupportMonths() { return supportMonths; }
|
||||
public void setSupportMonths(Double supportMonths) { this.supportMonths = supportMonths; }
|
||||
|
||||
public Double getSupportRate() { return supportRate; }
|
||||
public void setSupportRate(Double supportRate) { this.supportRate = supportRate; }
|
||||
|
||||
public String getPaymentTerms() { return paymentTerms; }
|
||||
public void setPaymentTerms(String paymentTerms) { this.paymentTerms = paymentTerms; }
|
||||
|
||||
public String getDeliveryTerms() { return deliveryTerms; }
|
||||
public void setDeliveryTerms(String deliveryTerms) { this.deliveryTerms = deliveryTerms; }
|
||||
|
||||
public Integer getValidityDays() { return validityDays; }
|
||||
public void setValidityDays(Integer validityDays) { this.validityDays = validityDays; }
|
||||
|
||||
public QuoteStatus getStatus() { return status; }
|
||||
public void setStatus(QuoteStatus status) { this.status = status; }
|
||||
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
|
||||
public LocalDateTime getSentAt() { return sentAt; }
|
||||
public void setSentAt(LocalDateTime sentAt) { this.sentAt = sentAt; }
|
||||
|
||||
public LocalDateTime getExpiredAt() { return expiredAt; }
|
||||
public void setExpiredAt(LocalDateTime expiredAt) { this.expiredAt = expiredAt; }
|
||||
}
|
||||
|
||||
|
||||
115
src/main/java/dev/lions/quote/QuoteModule.java
Normal file
115
src/main/java/dev/lions/quote/QuoteModule.java
Normal file
@@ -0,0 +1,115 @@
|
||||
package dev.lions.quote;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
/**
|
||||
* Module/ligne d'un devis
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "quote_modules")
|
||||
public class QuoteModule {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "quote_id", nullable = false)
|
||||
private Quote quote;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String moduleCode; // CRM, STOCK, COMPTA, RH, INFRA
|
||||
|
||||
@Column(nullable = false)
|
||||
private String moduleName;
|
||||
|
||||
@Column(length = 1000)
|
||||
private String description;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Double unitPrice; // Prix unitaire HT
|
||||
|
||||
@Column(nullable = false)
|
||||
private Integer quantity = 1;
|
||||
|
||||
private String unit = "licence"; // licence, heure, mois, etc.
|
||||
|
||||
// Détails techniques
|
||||
@Column(length = 2000)
|
||||
private String technicalSpecs;
|
||||
|
||||
@Column(length = 1000)
|
||||
private String deliverables;
|
||||
|
||||
private Integer implementationDays; // Jours d'implémentation
|
||||
|
||||
// Niveau de complexité basé sur l'audit
|
||||
@Enumerated(EnumType.STRING)
|
||||
private ComplexityLevel complexity = ComplexityLevel.STANDARD;
|
||||
|
||||
// Constructeurs
|
||||
public QuoteModule() {}
|
||||
|
||||
public QuoteModule(String moduleCode, String moduleName, Double unitPrice) {
|
||||
this.moduleCode = moduleCode;
|
||||
this.moduleName = moduleName;
|
||||
this.unitPrice = unitPrice;
|
||||
}
|
||||
|
||||
// Méthodes métier
|
||||
public Double getTotalPrice() {
|
||||
return unitPrice * quantity;
|
||||
}
|
||||
|
||||
public Double getComplexityMultiplier() {
|
||||
return switch (complexity) {
|
||||
case BASIC -> 0.8;
|
||||
case STANDARD -> 1.0;
|
||||
case ADVANCED -> 1.3;
|
||||
case ENTERPRISE -> 1.6;
|
||||
};
|
||||
}
|
||||
|
||||
public Double getAdjustedPrice() {
|
||||
return unitPrice * getComplexityMultiplier();
|
||||
}
|
||||
|
||||
// Getters et Setters
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public Quote getQuote() { return quote; }
|
||||
public void setQuote(Quote quote) { this.quote = quote; }
|
||||
|
||||
public String getModuleCode() { return moduleCode; }
|
||||
public void setModuleCode(String moduleCode) { this.moduleCode = moduleCode; }
|
||||
|
||||
public String getModuleName() { return moduleName; }
|
||||
public void setModuleName(String moduleName) { this.moduleName = moduleName; }
|
||||
|
||||
public String getDescription() { return description; }
|
||||
public void setDescription(String description) { this.description = description; }
|
||||
|
||||
public Double getUnitPrice() { return unitPrice; }
|
||||
public void setUnitPrice(Double unitPrice) { this.unitPrice = unitPrice; }
|
||||
|
||||
public Integer getQuantity() { return quantity; }
|
||||
public void setQuantity(Integer quantity) { this.quantity = quantity; }
|
||||
|
||||
public String getUnit() { return unit; }
|
||||
public void setUnit(String unit) { this.unit = unit; }
|
||||
|
||||
public String getTechnicalSpecs() { return technicalSpecs; }
|
||||
public void setTechnicalSpecs(String technicalSpecs) { this.technicalSpecs = technicalSpecs; }
|
||||
|
||||
public String getDeliverables() { return deliverables; }
|
||||
public void setDeliverables(String deliverables) { this.deliverables = deliverables; }
|
||||
|
||||
public Integer getImplementationDays() { return implementationDays; }
|
||||
public void setImplementationDays(Integer implementationDays) { this.implementationDays = implementationDays; }
|
||||
|
||||
public ComplexityLevel getComplexity() { return complexity; }
|
||||
public void setComplexity(ComplexityLevel complexity) { this.complexity = complexity; }
|
||||
}
|
||||
|
||||
|
||||
70
src/main/java/dev/lions/quote/QuoteModuleDTO.java
Normal file
70
src/main/java/dev/lions/quote/QuoteModuleDTO.java
Normal file
@@ -0,0 +1,70 @@
|
||||
package dev.lions.quote;
|
||||
|
||||
/**
|
||||
* DTO pour les modules de devis
|
||||
*/
|
||||
public class QuoteModuleDTO {
|
||||
|
||||
private Long id;
|
||||
private String moduleCode;
|
||||
private String moduleName;
|
||||
private String description;
|
||||
private Double unitPrice;
|
||||
private Integer quantity;
|
||||
private String unit;
|
||||
private String technicalSpecs;
|
||||
private String deliverables;
|
||||
private Integer implementationDays;
|
||||
private ComplexityLevel complexity;
|
||||
|
||||
// Constructeurs
|
||||
public QuoteModuleDTO() {}
|
||||
|
||||
public QuoteModuleDTO(QuoteModule module) {
|
||||
this.id = module.getId();
|
||||
this.moduleCode = module.getModuleCode();
|
||||
this.moduleName = module.getModuleName();
|
||||
this.description = module.getDescription();
|
||||
this.unitPrice = module.getUnitPrice();
|
||||
this.quantity = module.getQuantity();
|
||||
this.unit = module.getUnit();
|
||||
this.technicalSpecs = module.getTechnicalSpecs();
|
||||
this.deliverables = module.getDeliverables();
|
||||
this.implementationDays = module.getImplementationDays();
|
||||
this.complexity = module.getComplexity();
|
||||
}
|
||||
|
||||
// Getters et Setters
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public String getModuleCode() { return moduleCode; }
|
||||
public void setModuleCode(String moduleCode) { this.moduleCode = moduleCode; }
|
||||
|
||||
public String getModuleName() { return moduleName; }
|
||||
public void setModuleName(String moduleName) { this.moduleName = moduleName; }
|
||||
|
||||
public String getDescription() { return description; }
|
||||
public void setDescription(String description) { this.description = description; }
|
||||
|
||||
public Double getUnitPrice() { return unitPrice; }
|
||||
public void setUnitPrice(Double unitPrice) { this.unitPrice = unitPrice; }
|
||||
|
||||
public Integer getQuantity() { return quantity; }
|
||||
public void setQuantity(Integer quantity) { this.quantity = quantity; }
|
||||
|
||||
public String getUnit() { return unit; }
|
||||
public void setUnit(String unit) { this.unit = unit; }
|
||||
|
||||
public String getTechnicalSpecs() { return technicalSpecs; }
|
||||
public void setTechnicalSpecs(String technicalSpecs) { this.technicalSpecs = technicalSpecs; }
|
||||
|
||||
public String getDeliverables() { return deliverables; }
|
||||
public void setDeliverables(String deliverables) { this.deliverables = deliverables; }
|
||||
|
||||
public Integer getImplementationDays() { return implementationDays; }
|
||||
public void setImplementationDays(Integer implementationDays) { this.implementationDays = implementationDays; }
|
||||
|
||||
public ComplexityLevel getComplexity() { return complexity; }
|
||||
public void setComplexity(ComplexityLevel complexity) { this.complexity = complexity; }
|
||||
}
|
||||
421
src/main/java/dev/lions/quote/QuoteReportService.java
Normal file
421
src/main/java/dev/lions/quote/QuoteReportService.java
Normal file
@@ -0,0 +1,421 @@
|
||||
package dev.lions.quote;
|
||||
|
||||
import com.itextpdf.text.*;
|
||||
import com.itextpdf.text.pdf.*;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import io.quarkus.mailer.Mail;
|
||||
import io.quarkus.mailer.Mailer;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.text.NumberFormat;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Service de génération de rapports PDF pour les devis
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class QuoteReportService {
|
||||
|
||||
@Inject
|
||||
Mailer mailer;
|
||||
|
||||
private static final Font TITLE_FONT = new Font(Font.FontFamily.HELVETICA, 20, Font.BOLD, BaseColor.DARK_GRAY);
|
||||
private static final Font HEADER_FONT = new Font(Font.FontFamily.HELVETICA, 14, Font.BOLD, BaseColor.BLACK);
|
||||
private static final Font NORMAL_FONT = new Font(Font.FontFamily.HELVETICA, 11, Font.NORMAL, BaseColor.BLACK);
|
||||
private static final Font SMALL_FONT = new Font(Font.FontFamily.HELVETICA, 9, Font.NORMAL, BaseColor.GRAY);
|
||||
private static final Font PRICE_FONT = new Font(Font.FontFamily.HELVETICA, 12, Font.BOLD, BaseColor.BLUE);
|
||||
|
||||
private final NumberFormat currencyFormat = NumberFormat.getInstance(Locale.FRANCE);
|
||||
|
||||
/**
|
||||
* Génère le PDF du devis
|
||||
*/
|
||||
public byte[] generateQuotePDF(Quote quote) throws Exception {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
Document document = new Document(PageSize.A4, 50, 50, 50, 50);
|
||||
PdfWriter writer = PdfWriter.getInstance(document, baos);
|
||||
|
||||
document.open();
|
||||
|
||||
// En-tête Lions Dev
|
||||
addHeader(document, quote);
|
||||
|
||||
// Informations client
|
||||
addClientInfo(document, quote);
|
||||
|
||||
// Détail des modules
|
||||
addModulesDetail(document, quote);
|
||||
|
||||
// Services additionnels
|
||||
addAdditionalServices(document, quote);
|
||||
|
||||
// Récapitulatif financier
|
||||
addFinancialSummary(document, quote);
|
||||
|
||||
// Conditions commerciales
|
||||
addCommercialTerms(document, quote);
|
||||
|
||||
// Pied de page
|
||||
addFooter(document);
|
||||
|
||||
document.close();
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
private void addHeader(Document document, Quote quote) throws DocumentException {
|
||||
// Logo et titre Lions Dev
|
||||
PdfPTable headerTable = new PdfPTable(2);
|
||||
headerTable.setWidthPercentage(100);
|
||||
headerTable.setWidths(new float[]{2, 1});
|
||||
|
||||
// Colonne gauche - Lions Dev
|
||||
PdfPCell leftCell = new PdfPCell();
|
||||
leftCell.setBorder(Rectangle.NO_BORDER);
|
||||
|
||||
Paragraph title = new Paragraph("LIONS DEV", TITLE_FONT);
|
||||
title.setSpacingAfter(5);
|
||||
leftCell.addElement(title);
|
||||
|
||||
Paragraph subtitle = new Paragraph("Solutions Digitales Innovantes", HEADER_FONT);
|
||||
subtitle.setSpacingAfter(10);
|
||||
leftCell.addElement(subtitle);
|
||||
|
||||
Paragraph address = new Paragraph("Abidjan, Côte d'Ivoire\n+225 01 01 75 95 25\ncontact@lions.dev", NORMAL_FONT);
|
||||
leftCell.addElement(address);
|
||||
|
||||
headerTable.addCell(leftCell);
|
||||
|
||||
// Colonne droite - Numéro devis
|
||||
PdfPCell rightCell = new PdfPCell();
|
||||
rightCell.setBorder(Rectangle.NO_BORDER);
|
||||
rightCell.setHorizontalAlignment(Element.ALIGN_RIGHT);
|
||||
|
||||
Paragraph quoteTitle = new Paragraph("DEVIS", new Font(Font.FontFamily.HELVETICA, 16, Font.BOLD));
|
||||
quoteTitle.setAlignment(Element.ALIGN_RIGHT);
|
||||
rightCell.addElement(quoteTitle);
|
||||
|
||||
Paragraph quoteNumber = new Paragraph(quote.getQuoteNumber(), HEADER_FONT);
|
||||
quoteNumber.setAlignment(Element.ALIGN_RIGHT);
|
||||
rightCell.addElement(quoteNumber);
|
||||
|
||||
Paragraph quoteDate = new Paragraph("Date: " + quote.getCreatedAt().format(DateTimeFormatter.ofPattern("dd/MM/yyyy")), NORMAL_FONT);
|
||||
quoteDate.setAlignment(Element.ALIGN_RIGHT);
|
||||
rightCell.addElement(quoteDate);
|
||||
|
||||
Paragraph validUntil = new Paragraph("Valide jusqu'au: " + quote.getExpiredAt().format(DateTimeFormatter.ofPattern("dd/MM/yyyy")), NORMAL_FONT);
|
||||
validUntil.setAlignment(Element.ALIGN_RIGHT);
|
||||
rightCell.addElement(validUntil);
|
||||
|
||||
headerTable.addCell(rightCell);
|
||||
|
||||
document.add(headerTable);
|
||||
document.add(Chunk.NEWLINE);
|
||||
|
||||
// Ligne de séparation
|
||||
com.itextpdf.text.pdf.draw.LineSeparator line = new com.itextpdf.text.pdf.draw.LineSeparator();
|
||||
document.add(new Chunk(line));
|
||||
document.add(Chunk.NEWLINE);
|
||||
}
|
||||
|
||||
private void addClientInfo(Document document, Quote quote) throws DocumentException {
|
||||
Paragraph section = new Paragraph("INFORMATIONS CLIENT", HEADER_FONT);
|
||||
section.setSpacingBefore(10);
|
||||
section.setSpacingAfter(10);
|
||||
document.add(section);
|
||||
|
||||
PdfPTable table = new PdfPTable(2);
|
||||
table.setWidthPercentage(100);
|
||||
table.setSpacingAfter(15);
|
||||
|
||||
addTableRow(table, "Entreprise:", quote.getCompanyName());
|
||||
addTableRow(table, "Contact:", quote.getContactName());
|
||||
addTableRow(table, "Email:", quote.getEmail());
|
||||
addTableRow(table, "Téléphone:", quote.getPhone() != null ? quote.getPhone() : "-");
|
||||
addTableRow(table, "Secteur:", quote.getSector() != null ? quote.getSector() : "-");
|
||||
addTableRow(table, "Employés:", quote.getEmployeeCount() != null ? quote.getEmployeeCount() + " personnes" : "-");
|
||||
|
||||
if (quote.getAuditScore() != null) {
|
||||
addTableRow(table, "Score audit:", String.format("%.1f%%", quote.getAuditScore()));
|
||||
}
|
||||
|
||||
document.add(table);
|
||||
}
|
||||
|
||||
private void addModulesDetail(Document document, Quote quote) throws DocumentException {
|
||||
Paragraph section = new Paragraph("DÉTAIL DE LA SOLUTION", HEADER_FONT);
|
||||
section.setSpacingBefore(15);
|
||||
section.setSpacingAfter(10);
|
||||
document.add(section);
|
||||
|
||||
if (quote.getModules().isEmpty()) {
|
||||
Paragraph noModules = new Paragraph("Aucun module sélectionné", NORMAL_FONT);
|
||||
document.add(noModules);
|
||||
return;
|
||||
}
|
||||
|
||||
PdfPTable table = new PdfPTable(5);
|
||||
table.setWidthPercentage(100);
|
||||
table.setWidths(new float[]{3, 1, 1, 1, 1.5f});
|
||||
|
||||
// En-têtes
|
||||
addTableHeader(table, "Module / Description");
|
||||
addTableHeader(table, "Qté");
|
||||
addTableHeader(table, "Prix Unit.");
|
||||
addTableHeader(table, "Niveau");
|
||||
addTableHeader(table, "Total");
|
||||
|
||||
double subtotal = 0;
|
||||
|
||||
for (QuoteModule module : quote.getModules()) {
|
||||
// Nom du module
|
||||
PdfPCell nameCell = new PdfPCell(new Phrase(module.getModuleName(), NORMAL_FONT));
|
||||
nameCell.setVerticalAlignment(Element.ALIGN_TOP);
|
||||
table.addCell(nameCell);
|
||||
|
||||
// Quantité
|
||||
table.addCell(new PdfPCell(new Phrase(module.getQuantity().toString(), NORMAL_FONT)));
|
||||
|
||||
// Prix unitaire
|
||||
table.addCell(new PdfPCell(new Phrase(formatCurrency(module.getUnitPrice()), NORMAL_FONT)));
|
||||
|
||||
// Niveau de complexité
|
||||
String complexityText = switch (module.getComplexity()) {
|
||||
case BASIC -> "Basique";
|
||||
case STANDARD -> "Standard";
|
||||
case ADVANCED -> "Avancé";
|
||||
case ENTERPRISE -> "Enterprise";
|
||||
};
|
||||
table.addCell(new PdfPCell(new Phrase(complexityText, NORMAL_FONT)));
|
||||
|
||||
// Total
|
||||
double moduleTotal = module.getUnitPrice() * module.getQuantity();
|
||||
table.addCell(new PdfPCell(new Phrase(formatCurrency(moduleTotal), PRICE_FONT)));
|
||||
subtotal += moduleTotal;
|
||||
|
||||
// Description détaillée (ligne suivante)
|
||||
if (module.getDescription() != null && !module.getDescription().isEmpty()) {
|
||||
PdfPCell descCell = new PdfPCell(new Phrase(module.getDescription(), SMALL_FONT));
|
||||
descCell.setColspan(5);
|
||||
descCell.setPaddingTop(5);
|
||||
descCell.setPaddingBottom(10);
|
||||
table.addCell(descCell);
|
||||
}
|
||||
}
|
||||
|
||||
document.add(table);
|
||||
}
|
||||
|
||||
private void addAdditionalServices(Document document, Quote quote) throws DocumentException {
|
||||
if (quote.getFormationHours() > 0 || quote.getSupportMonths() > 0) {
|
||||
Paragraph section = new Paragraph("SERVICES ADDITIONNELS", HEADER_FONT);
|
||||
section.setSpacingBefore(15);
|
||||
section.setSpacingAfter(10);
|
||||
document.add(section);
|
||||
|
||||
PdfPTable table = new PdfPTable(4);
|
||||
table.setWidthPercentage(100);
|
||||
table.setWidths(new float[]{3, 1, 1, 1.5f});
|
||||
|
||||
// En-têtes
|
||||
addTableHeader(table, "Service");
|
||||
addTableHeader(table, "Quantité");
|
||||
addTableHeader(table, "Prix Unit.");
|
||||
addTableHeader(table, "Total");
|
||||
|
||||
// Formation
|
||||
if (quote.getFormationHours() > 0) {
|
||||
table.addCell(new PdfPCell(new Phrase("Formation utilisateurs", NORMAL_FONT)));
|
||||
table.addCell(new PdfPCell(new Phrase(quote.getFormationHours() + " heures", NORMAL_FONT)));
|
||||
table.addCell(new PdfPCell(new Phrase(formatCurrency(quote.getFormationRate()), NORMAL_FONT)));
|
||||
table.addCell(new PdfPCell(new Phrase(formatCurrency(quote.getFormationHours() * quote.getFormationRate()), PRICE_FONT)));
|
||||
}
|
||||
|
||||
// Support
|
||||
if (quote.getSupportMonths() > 0) {
|
||||
table.addCell(new PdfPCell(new Phrase("Support technique", NORMAL_FONT)));
|
||||
table.addCell(new PdfPCell(new Phrase(quote.getSupportMonths() + " mois", NORMAL_FONT)));
|
||||
table.addCell(new PdfPCell(new Phrase(formatCurrency(quote.getSupportRate()), NORMAL_FONT)));
|
||||
table.addCell(new PdfPCell(new Phrase(formatCurrency(quote.getSupportMonths() * quote.getSupportRate()), PRICE_FONT)));
|
||||
}
|
||||
|
||||
document.add(table);
|
||||
}
|
||||
}
|
||||
|
||||
private void addFinancialSummary(Document document, Quote quote) throws DocumentException {
|
||||
Paragraph section = new Paragraph("RÉCAPITULATIF FINANCIER", HEADER_FONT);
|
||||
section.setSpacingBefore(15);
|
||||
section.setSpacingAfter(10);
|
||||
document.add(section);
|
||||
|
||||
PdfPTable table = new PdfPTable(2);
|
||||
table.setWidthPercentage(60);
|
||||
table.setHorizontalAlignment(Element.ALIGN_RIGHT);
|
||||
|
||||
// Sous-total
|
||||
addFinancialRow(table, "Sous-total HT:", formatCurrency(quote.getSubtotalHT()));
|
||||
|
||||
// Remise
|
||||
if (quote.getDiscountPercentage() > 0) {
|
||||
addFinancialRow(table, String.format("Remise (%.1f%%):", quote.getDiscountPercentage()),
|
||||
"-" + formatCurrency(quote.getDiscountAmount()));
|
||||
}
|
||||
|
||||
// Total HT
|
||||
addFinancialRow(table, "Total HT:", formatCurrency(quote.getTotalHT()));
|
||||
|
||||
// TVA
|
||||
addFinancialRow(table, String.format("TVA (%.0f%%):", quote.getVatRate()),
|
||||
formatCurrency(quote.getVatAmount()));
|
||||
|
||||
// Total TTC
|
||||
PdfPCell labelCell = new PdfPCell(new Phrase("TOTAL TTC:", new Font(Font.FontFamily.HELVETICA, 12, Font.BOLD)));
|
||||
labelCell.setHorizontalAlignment(Element.ALIGN_RIGHT);
|
||||
labelCell.setBorder(Rectangle.TOP);
|
||||
table.addCell(labelCell);
|
||||
|
||||
PdfPCell valueCell = new PdfPCell(new Phrase(formatCurrency(quote.getTotalTTC()),
|
||||
new Font(Font.FontFamily.HELVETICA, 14, Font.BOLD, BaseColor.BLUE)));
|
||||
valueCell.setHorizontalAlignment(Element.ALIGN_RIGHT);
|
||||
valueCell.setBorder(Rectangle.TOP);
|
||||
table.addCell(valueCell);
|
||||
|
||||
document.add(table);
|
||||
}
|
||||
|
||||
private void addCommercialTerms(Document document, Quote quote) throws DocumentException {
|
||||
Paragraph section = new Paragraph("CONDITIONS COMMERCIALES", HEADER_FONT);
|
||||
section.setSpacingBefore(20);
|
||||
section.setSpacingAfter(10);
|
||||
document.add(section);
|
||||
|
||||
// Conditions de paiement
|
||||
Paragraph payment = new Paragraph("Conditions de paiement: " + quote.getPaymentTerms(), NORMAL_FONT);
|
||||
payment.setSpacingAfter(5);
|
||||
document.add(payment);
|
||||
|
||||
// Délais de livraison
|
||||
Paragraph delivery = new Paragraph("Délais de livraison: " + quote.getDeliveryTerms(), NORMAL_FONT);
|
||||
delivery.setSpacingAfter(5);
|
||||
document.add(delivery);
|
||||
|
||||
// Validité
|
||||
Paragraph validity = new Paragraph("Validité du devis: " + quote.getValidityDays() + " jours", NORMAL_FONT);
|
||||
validity.setSpacingAfter(10);
|
||||
document.add(validity);
|
||||
|
||||
// Notes importantes
|
||||
Paragraph notes = new Paragraph("Notes importantes:", HEADER_FONT);
|
||||
notes.setSpacingAfter(5);
|
||||
document.add(notes);
|
||||
|
||||
List notesList = new List(List.UNORDERED);
|
||||
notesList.add(new ListItem("Prix exprimés en FCFA, hors taxes", SMALL_FONT));
|
||||
notesList.add(new ListItem("Formation incluse sur site client", SMALL_FONT));
|
||||
notesList.add(new ListItem("Support technique pendant la période indiquée", SMALL_FONT));
|
||||
notesList.add(new ListItem("Garantie logiciel 12 mois", SMALL_FONT));
|
||||
notesList.add(new ListItem("Mises à jour incluses pendant 6 mois", SMALL_FONT));
|
||||
|
||||
document.add(notesList);
|
||||
}
|
||||
|
||||
private void addFooter(Document document) throws DocumentException {
|
||||
Paragraph footer = new Paragraph("\nPour accepter ce devis, contactez-nous au +225 01 01 75 95 25\n" +
|
||||
"ou par email à contact@lions.dev\n\n" +
|
||||
"Lions Dev - Votre partenaire digital en Côte d'Ivoire", SMALL_FONT);
|
||||
footer.setAlignment(Element.ALIGN_CENTER);
|
||||
footer.setSpacingBefore(20);
|
||||
document.add(footer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Envoie le devis par email
|
||||
*/
|
||||
public void sendQuoteByEmail(Quote quote, String customMessage) {
|
||||
try {
|
||||
byte[] pdfBytes = generateQuotePDF(quote);
|
||||
|
||||
String emailContent = generateQuoteEmailContent(quote, customMessage);
|
||||
|
||||
Mail mail = Mail.withHtml(quote.getEmail(),
|
||||
"Votre Devis Personnalisé - " + quote.getQuoteNumber(),
|
||||
emailContent)
|
||||
.addAttachment("devis-" + quote.getQuoteNumber() + ".pdf",
|
||||
pdfBytes, "application/pdf");
|
||||
|
||||
mailer.send(mail);
|
||||
} catch (Exception e) {
|
||||
// Log l'erreur mais ne fait pas échouer le processus
|
||||
System.err.println("Erreur envoi email devis: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private String generateQuoteEmailContent(Quote quote, String customMessage) {
|
||||
return String.format("""
|
||||
<h2>Bonjour %s,</h2>
|
||||
|
||||
<p>Nous avons le plaisir de vous adresser votre devis personnalisé <strong>%s</strong>.</p>
|
||||
|
||||
%s
|
||||
|
||||
<h3>Récapitulatif:</h3>
|
||||
<ul>
|
||||
<li><strong>Montant total:</strong> %s FCFA TTC</li>
|
||||
<li><strong>Validité:</strong> %d jours</li>
|
||||
<li><strong>Délais:</strong> %s</li>
|
||||
</ul>
|
||||
|
||||
<p>Ce devis a été établi suite à votre audit de maturité digitale (score: %.1f%%).</p>
|
||||
|
||||
<p><strong>Pour accepter ce devis:</strong></p>
|
||||
<ul>
|
||||
<li>Répondez à cet email</li>
|
||||
<li>Appelez-nous au +225 01 01 75 95 25</li>
|
||||
<li>Ou planifiez un rendez-vous sur notre site</li>
|
||||
</ul>
|
||||
|
||||
<p>Notre équipe reste à votre disposition pour toute question ou personnalisation.</p>
|
||||
|
||||
<p>Cordialement,<br>
|
||||
L'équipe Lions Dev<br>
|
||||
+225 01 01 75 95 25<br>
|
||||
contact@lions.dev</p>
|
||||
""",
|
||||
quote.getContactName(),
|
||||
quote.getQuoteNumber(),
|
||||
customMessage != null ? "<p>" + customMessage + "</p>" : "",
|
||||
formatCurrency(quote.getTotalTTC()),
|
||||
quote.getValidityDays(),
|
||||
quote.getDeliveryTerms(),
|
||||
quote.getAuditScore() != null ? quote.getAuditScore() : 0.0);
|
||||
}
|
||||
|
||||
// Méthodes utilitaires
|
||||
private void addTableRow(PdfPTable table, String label, String value) {
|
||||
table.addCell(new PdfPCell(new Phrase(label, NORMAL_FONT)));
|
||||
table.addCell(new PdfPCell(new Phrase(value != null ? value : "-", NORMAL_FONT)));
|
||||
}
|
||||
|
||||
private void addTableHeader(PdfPTable table, String header) {
|
||||
PdfPCell cell = new PdfPCell(new Phrase(header, HEADER_FONT));
|
||||
cell.setBackgroundColor(BaseColor.LIGHT_GRAY);
|
||||
cell.setHorizontalAlignment(Element.ALIGN_CENTER);
|
||||
table.addCell(cell);
|
||||
}
|
||||
|
||||
private void addFinancialRow(PdfPTable table, String label, String value) {
|
||||
PdfPCell labelCell = new PdfPCell(new Phrase(label, NORMAL_FONT));
|
||||
labelCell.setHorizontalAlignment(Element.ALIGN_RIGHT);
|
||||
table.addCell(labelCell);
|
||||
|
||||
PdfPCell valueCell = new PdfPCell(new Phrase(value, PRICE_FONT));
|
||||
valueCell.setHorizontalAlignment(Element.ALIGN_RIGHT);
|
||||
table.addCell(valueCell);
|
||||
}
|
||||
|
||||
private String formatCurrency(Double amount) {
|
||||
if (amount == null) return "0";
|
||||
return String.format("%,.0f", amount) + " FCFA";
|
||||
}
|
||||
}
|
||||
296
src/main/java/dev/lions/quote/QuoteResource.java
Normal file
296
src/main/java/dev/lions/quote/QuoteResource.java
Normal file
@@ -0,0 +1,296 @@
|
||||
package dev.lions.quote;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.ws.rs.*;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* API REST pour la gestion des devis
|
||||
*/
|
||||
@Path("/api/quotes")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public class QuoteResource {
|
||||
|
||||
@Inject
|
||||
QuoteService quoteService;
|
||||
|
||||
@Inject
|
||||
QuoteReportService quoteReportService;
|
||||
|
||||
/**
|
||||
* Génère un devis automatique à partir d'un audit
|
||||
*/
|
||||
@POST
|
||||
@Path("/generate/{auditId}")
|
||||
public Response generateQuoteFromAudit(@PathParam("auditId") Long auditId) {
|
||||
try {
|
||||
Quote quote = quoteService.generateQuoteFromAudit(auditId);
|
||||
QuoteDTO quoteDTO = new QuoteDTO(quote);
|
||||
|
||||
return Response.ok(quoteDTO).build();
|
||||
} catch (IllegalArgumentException e) {
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(Map.of("error", e.getMessage()))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(Map.of("error", "Erreur lors de la génération du devis"))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère un devis par ID
|
||||
*/
|
||||
@GET
|
||||
@Path("/{quoteId}")
|
||||
public Response getQuote(@PathParam("quoteId") Long quoteId) {
|
||||
Quote quote = quoteService.getQuoteById(quoteId);
|
||||
if (quote == null) {
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity(Map.of("error", "Devis non trouvé"))
|
||||
.build();
|
||||
}
|
||||
|
||||
QuoteDTO quoteDTO = new QuoteDTO(quote);
|
||||
return Response.ok(quoteDTO).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Personnalise un devis existant
|
||||
*/
|
||||
@PUT
|
||||
@Path("/{quoteId}/customize")
|
||||
public Response customizeQuote(@PathParam("quoteId") Long quoteId,
|
||||
QuoteCustomizationDTO customization) {
|
||||
try {
|
||||
Quote quote = quoteService.customizeQuote(quoteId, customization);
|
||||
QuoteDTO quoteDTO = new QuoteDTO(quote);
|
||||
|
||||
return Response.ok(quoteDTO).build();
|
||||
} catch (IllegalArgumentException e) {
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(Map.of("error", e.getMessage()))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(Map.of("error", "Erreur lors de la personnalisation"))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applique une remise à un devis
|
||||
*/
|
||||
@PUT
|
||||
@Path("/{quoteId}/discount")
|
||||
public Response applyDiscount(@PathParam("quoteId") Long quoteId,
|
||||
Map<String, Object> discountData) {
|
||||
try {
|
||||
Double percentage = ((Number) discountData.get("percentage")).doubleValue();
|
||||
String reason = (String) discountData.get("reason");
|
||||
|
||||
quoteService.applyDiscount(quoteId, percentage, reason);
|
||||
|
||||
Quote quote = quoteService.getQuoteById(quoteId);
|
||||
QuoteDTO quoteDTO = new QuoteDTO(quote);
|
||||
|
||||
return Response.ok(quoteDTO).build();
|
||||
} catch (Exception e) {
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(Map.of("error", "Erreur lors de l'application de la remise"))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour le statut d'un devis
|
||||
*/
|
||||
@PUT
|
||||
@Path("/{quoteId}/status")
|
||||
public Response updateStatus(@PathParam("quoteId") Long quoteId,
|
||||
Map<String, String> statusData) {
|
||||
try {
|
||||
QuoteStatus status = QuoteStatus.valueOf(statusData.get("status"));
|
||||
quoteService.updateQuoteStatus(quoteId, status);
|
||||
|
||||
return Response.ok(Map.of("message", "Statut mis à jour")).build();
|
||||
} catch (IllegalArgumentException e) {
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(Map.of("error", "Statut invalide"))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(Map.of("error", "Erreur lors de la mise à jour"))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère la liste des devis en attente
|
||||
*/
|
||||
@GET
|
||||
@Path("/pending")
|
||||
public Response getPendingQuotes() {
|
||||
List<Quote> quotes = quoteService.getPendingQuotes();
|
||||
List<QuoteDTO> quoteDTOs = quotes.stream()
|
||||
.map(QuoteDTO::new)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return Response.ok(quoteDTOs).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère le catalogue des modules
|
||||
*/
|
||||
@GET
|
||||
@Path("/catalog")
|
||||
public Response getModuleCatalog() {
|
||||
List<ModuleCatalog> catalog = quoteService.getActiveCatalog();
|
||||
return Response.ok(catalog).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère le PDF du devis
|
||||
*/
|
||||
@GET
|
||||
@Path("/{quoteId}/pdf")
|
||||
@Produces("application/pdf")
|
||||
public Response generateQuotePDF(@PathParam("quoteId") Long quoteId) {
|
||||
try {
|
||||
Quote quote = quoteService.getQuoteById(quoteId);
|
||||
if (quote == null) {
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity(Map.of("error", "Devis non trouvé"))
|
||||
.build();
|
||||
}
|
||||
|
||||
byte[] pdfBytes = quoteReportService.generateQuotePDF(quote);
|
||||
|
||||
return Response.ok(pdfBytes)
|
||||
.header("Content-Disposition",
|
||||
"attachment; filename=\"devis-" + quote.getQuoteNumber() + ".pdf\"")
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(Map.of("error", "Erreur lors de la génération du PDF"))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Envoie le devis par email
|
||||
*/
|
||||
@POST
|
||||
@Path("/{quoteId}/send")
|
||||
public Response sendQuote(@PathParam("quoteId") Long quoteId,
|
||||
Map<String, String> emailData) {
|
||||
try {
|
||||
Quote quote = quoteService.getQuoteById(quoteId);
|
||||
if (quote == null) {
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity(Map.of("error", "Devis non trouvé"))
|
||||
.build();
|
||||
}
|
||||
|
||||
String message = emailData.get("message");
|
||||
quoteReportService.sendQuoteByEmail(quote, message);
|
||||
|
||||
// Mise à jour du statut
|
||||
quoteService.updateQuoteStatus(quoteId, QuoteStatus.SENT);
|
||||
|
||||
return Response.ok(Map.of("message", "Devis envoyé avec succès")).build();
|
||||
} catch (Exception e) {
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(Map.of("error", "Erreur lors de l'envoi"))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Statistiques des devis
|
||||
*/
|
||||
@GET
|
||||
@Path("/statistics")
|
||||
public Response getStatistics() {
|
||||
Map<String, Object> stats = quoteService.getQuoteStatistics();
|
||||
return Response.ok(stats).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Recherche de devis
|
||||
*/
|
||||
@GET
|
||||
@Path("/search")
|
||||
public Response searchQuotes(@QueryParam("company") String company,
|
||||
@QueryParam("status") String status,
|
||||
@QueryParam("from") String fromDate,
|
||||
@QueryParam("to") String toDate) {
|
||||
// Implémentation de la recherche
|
||||
// Pour l'instant, retourne tous les devis en attente
|
||||
List<Quote> quotes = quoteService.getPendingQuotes();
|
||||
List<QuoteDTO> quoteDTOs = quotes.stream()
|
||||
.map(QuoteDTO::new)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return Response.ok(quoteDTOs).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Acceptation d'un devis par le client (lien public)
|
||||
*/
|
||||
@POST
|
||||
@Path("/{quoteId}/accept")
|
||||
public Response acceptQuote(@PathParam("quoteId") Long quoteId,
|
||||
Map<String, String> acceptanceData) {
|
||||
try {
|
||||
Quote quote = quoteService.getQuoteById(quoteId);
|
||||
if (quote == null || !quote.isValid()) {
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(Map.of("error", "Devis non valide ou expiré"))
|
||||
.build();
|
||||
}
|
||||
|
||||
// Mise à jour du statut et feedback client
|
||||
quoteService.updateQuoteStatus(quoteId, QuoteStatus.ACCEPTED);
|
||||
|
||||
String feedback = acceptanceData.get("feedback");
|
||||
if (feedback != null) {
|
||||
quote.setClientFeedback(feedback);
|
||||
}
|
||||
|
||||
return Response.ok(Map.of("message", "Devis accepté avec succès")).build();
|
||||
} catch (Exception e) {
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(Map.of("error", "Erreur lors de l'acceptation"))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Consultation d'un devis par le client (lien public)
|
||||
*/
|
||||
@GET
|
||||
@Path("/{quoteId}/view")
|
||||
public Response viewQuote(@PathParam("quoteId") Long quoteId) {
|
||||
Quote quote = quoteService.getQuoteById(quoteId);
|
||||
if (quote == null) {
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity(Map.of("error", "Devis non trouvé"))
|
||||
.build();
|
||||
}
|
||||
|
||||
// Marquer comme consulté
|
||||
if (quote.getStatus() == QuoteStatus.SENT) {
|
||||
quoteService.updateQuoteStatus(quoteId, QuoteStatus.VIEWED);
|
||||
}
|
||||
|
||||
QuoteDTO quoteDTO = new QuoteDTO(quote);
|
||||
return Response.ok(quoteDTO).build();
|
||||
}
|
||||
}
|
||||
356
src/main/java/dev/lions/quote/QuoteService.java
Normal file
356
src/main/java/dev/lions/quote/QuoteService.java
Normal file
@@ -0,0 +1,356 @@
|
||||
package dev.lions.quote;
|
||||
|
||||
import dev.lions.audit.AuditResponse;
|
||||
import dev.lions.audit.AuditService;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.transaction.Transactional;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Service de gestion des devis personnalisés
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class QuoteService {
|
||||
|
||||
@Inject
|
||||
EntityManager em;
|
||||
|
||||
@Inject
|
||||
AuditService auditService;
|
||||
|
||||
/**
|
||||
* Génère un devis automatique basé sur un audit
|
||||
*/
|
||||
@Transactional
|
||||
public Quote generateQuoteFromAudit(Long auditId) {
|
||||
AuditResponse audit = auditService.getAuditById(auditId);
|
||||
if (audit == null) {
|
||||
throw new IllegalArgumentException("Audit non trouvé");
|
||||
}
|
||||
|
||||
Quote quote = new Quote();
|
||||
quote.setCompanyName(audit.getCompanyName());
|
||||
quote.setContactName(audit.getContactName());
|
||||
quote.setEmail(audit.getEmail());
|
||||
quote.setPhone(audit.getPhone());
|
||||
quote.setSector(audit.getSector());
|
||||
quote.setEmployeeCount(audit.getEmployeeCount());
|
||||
quote.setAuditId(auditId);
|
||||
quote.setAuditScore(audit.getMaturityPercentage());
|
||||
|
||||
// Génération du numéro de devis
|
||||
quote.setQuoteNumber(generateQuoteNumber());
|
||||
|
||||
// Sélection automatique des modules selon l'audit
|
||||
List<QuoteModule> recommendedModules = recommendModulesFromAudit(audit);
|
||||
|
||||
for (QuoteModule module : recommendedModules) {
|
||||
module.setQuote(quote);
|
||||
quote.getModules().add(module);
|
||||
}
|
||||
|
||||
// Calcul des services additionnels
|
||||
calculateAdditionalServices(quote, audit);
|
||||
|
||||
// Calcul des totaux
|
||||
quote.calculateTotals();
|
||||
|
||||
// Sauvegarde
|
||||
em.persist(quote);
|
||||
|
||||
return quote;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recommande les modules selon les résultats d'audit
|
||||
*/
|
||||
private List<QuoteModule> recommendModulesFromAudit(AuditResponse audit) {
|
||||
List<QuoteModule> modules = new ArrayList<>();
|
||||
Map<String, Integer> categoryScores = audit.getCategoryScores();
|
||||
|
||||
// Récupération du catalogue
|
||||
List<ModuleCatalog> catalog = getActiveCatalog();
|
||||
|
||||
for (Map.Entry<String, Integer> entry : categoryScores.entrySet()) {
|
||||
String category = entry.getKey();
|
||||
Integer score = entry.getValue();
|
||||
|
||||
// Si le score est faible, recommander le module
|
||||
if (score < 60) { // Seuil configurable
|
||||
ModuleCatalog catalogModule = findCatalogModule(catalog, category);
|
||||
if (catalogModule != null) {
|
||||
QuoteModule module = createModuleFromCatalog(catalogModule, audit);
|
||||
modules.add(module);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return modules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un module de devis à partir du catalogue
|
||||
*/
|
||||
private QuoteModule createModuleFromCatalog(ModuleCatalog catalogModule, AuditResponse audit) {
|
||||
QuoteModule module = new QuoteModule();
|
||||
module.setModuleCode(catalogModule.getModuleCode());
|
||||
module.setModuleName(catalogModule.getModuleName());
|
||||
module.setDescription(catalogModule.getDescription());
|
||||
module.setTechnicalSpecs(catalogModule.getTechnicalRequirements());
|
||||
|
||||
// Détermination du niveau de complexité
|
||||
ComplexityLevel complexity = catalogModule.getRecommendedComplexity(
|
||||
audit.getMaturityPercentage(),
|
||||
audit.getEmployeeCount()
|
||||
);
|
||||
module.setComplexity(complexity);
|
||||
|
||||
// Prix selon la complexité
|
||||
module.setUnitPrice(catalogModule.getPriceForComplexity(complexity));
|
||||
module.setImplementationDays(catalogModule.getEstimatedImplementationDays(complexity));
|
||||
|
||||
// Génération des livrables
|
||||
module.setDeliverables(generateDeliverables(catalogModule, complexity));
|
||||
|
||||
return module;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule les services additionnels (formation, support)
|
||||
*/
|
||||
private void calculateAdditionalServices(Quote quote, AuditResponse audit) {
|
||||
// Formation : basée sur le nombre d'employés et la complexité
|
||||
int employeeCount = audit.getEmployeeCount() != null ? audit.getEmployeeCount() : 5;
|
||||
double formationHours = Math.max(8, employeeCount * 2); // Min 8h, 2h par employé
|
||||
|
||||
// Ajustement selon le score d'audit (plus le score est bas, plus de formation)
|
||||
if (audit.getMaturityPercentage() < 30) {
|
||||
formationHours *= 1.5;
|
||||
} else if (audit.getMaturityPercentage() < 60) {
|
||||
formationHours *= 1.2;
|
||||
}
|
||||
|
||||
quote.setFormationHours(formationHours);
|
||||
|
||||
// Support : 3 mois minimum, plus selon la complexité
|
||||
double supportMonths = 3.0;
|
||||
if (quote.getModules().size() > 2) {
|
||||
supportMonths = 6.0;
|
||||
}
|
||||
if (quote.getModules().size() > 4) {
|
||||
supportMonths = 12.0;
|
||||
}
|
||||
|
||||
quote.setSupportMonths(supportMonths);
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère un numéro de devis unique
|
||||
*/
|
||||
private String generateQuoteNumber() {
|
||||
int year = LocalDateTime.now().getYear();
|
||||
|
||||
// Récupération du dernier numéro de l'année
|
||||
String lastNumber = em.createQuery(
|
||||
"SELECT q.quoteNumber FROM Quote q WHERE q.quoteNumber LIKE :pattern ORDER BY q.quoteNumber DESC",
|
||||
String.class)
|
||||
.setParameter("pattern", "QUO-" + year + "-%")
|
||||
.setMaxResults(1)
|
||||
.getResultStream()
|
||||
.findFirst()
|
||||
.orElse("QUO-" + year + "-000");
|
||||
|
||||
// Extraction et incrémentation du numéro
|
||||
String[] parts = lastNumber.split("-");
|
||||
int nextNumber = Integer.parseInt(parts[2]) + 1;
|
||||
|
||||
return String.format("QUO-%d-%03d", year, nextNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère le catalogue actif
|
||||
*/
|
||||
public List<ModuleCatalog> getActiveCatalog() {
|
||||
return em.createQuery(
|
||||
"SELECT m FROM ModuleCatalog m WHERE m.active = true ORDER BY m.category, m.displayOrder",
|
||||
ModuleCatalog.class
|
||||
).getResultList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Trouve un module du catalogue par catégorie
|
||||
*/
|
||||
private ModuleCatalog findCatalogModule(List<ModuleCatalog> catalog, String category) {
|
||||
return catalog.stream()
|
||||
.filter(m -> category.equals(m.getCategory()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère les livrables selon le module et la complexité
|
||||
*/
|
||||
private String generateDeliverables(ModuleCatalog catalogModule, ComplexityLevel complexity) {
|
||||
StringBuilder deliverables = new StringBuilder();
|
||||
|
||||
deliverables.append("• Installation et configuration du module ").append(catalogModule.getModuleName()).append("\n");
|
||||
deliverables.append("• Paramétrage selon vos processus métier\n");
|
||||
deliverables.append("• Formation des utilisateurs\n");
|
||||
deliverables.append("• Documentation utilisateur\n");
|
||||
|
||||
if (complexity == ComplexityLevel.ADVANCED || complexity == ComplexityLevel.ENTERPRISE) {
|
||||
deliverables.append("• Intégrations avec systèmes existants\n");
|
||||
deliverables.append("• Rapports personnalisés\n");
|
||||
}
|
||||
|
||||
if (complexity == ComplexityLevel.ENTERPRISE) {
|
||||
deliverables.append("• Workflow avancés\n");
|
||||
deliverables.append("• API personnalisées\n");
|
||||
deliverables.append("• Support prioritaire\n");
|
||||
}
|
||||
|
||||
deliverables.append("• Garantie 12 mois\n");
|
||||
|
||||
return deliverables.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère un devis par ID
|
||||
*/
|
||||
public Quote getQuoteById(Long quoteId) {
|
||||
return em.find(Quote.class, quoteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour le statut d'un devis
|
||||
*/
|
||||
@Transactional
|
||||
public void updateQuoteStatus(Long quoteId, QuoteStatus status) {
|
||||
Quote quote = em.find(Quote.class, quoteId);
|
||||
if (quote != null) {
|
||||
quote.setStatus(status);
|
||||
|
||||
switch (status) {
|
||||
case SENT -> quote.setSentAt(LocalDateTime.now());
|
||||
case VIEWED -> quote.setViewedAt(LocalDateTime.now());
|
||||
case ACCEPTED -> quote.setAcceptedAt(LocalDateTime.now());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applique une remise à un devis
|
||||
*/
|
||||
@Transactional
|
||||
public void applyDiscount(Long quoteId, Double discountPercentage, String reason) {
|
||||
Quote quote = em.find(Quote.class, quoteId);
|
||||
if (quote != null) {
|
||||
quote.setDiscountPercentage(discountPercentage);
|
||||
quote.calculateTotals();
|
||||
|
||||
String note = String.format("Remise appliquée: %.1f%% - Raison: %s",
|
||||
discountPercentage, reason);
|
||||
quote.setSalesNotes(quote.getSalesNotes() != null ?
|
||||
quote.getSalesNotes() + "\n" + note : note);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les devis en attente
|
||||
*/
|
||||
public List<Quote> getPendingQuotes() {
|
||||
return em.createQuery(
|
||||
"SELECT q FROM Quote q WHERE q.status IN :statuses ORDER BY q.createdAt DESC",
|
||||
Quote.class)
|
||||
.setParameter("statuses", Arrays.asList(QuoteStatus.SENT, QuoteStatus.VIEWED))
|
||||
.getResultList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Statistiques des devis
|
||||
*/
|
||||
public Map<String, Object> getQuoteStatistics() {
|
||||
Map<String, Object> stats = new HashMap<>();
|
||||
|
||||
// Nombre total de devis
|
||||
Long totalQuotes = em.createQuery("SELECT COUNT(q) FROM Quote q", Long.class)
|
||||
.getSingleResult();
|
||||
stats.put("totalQuotes", totalQuotes);
|
||||
|
||||
// Taux de conversion
|
||||
Long acceptedQuotes = em.createQuery(
|
||||
"SELECT COUNT(q) FROM Quote q WHERE q.status = :status", Long.class)
|
||||
.setParameter("status", QuoteStatus.ACCEPTED)
|
||||
.getSingleResult();
|
||||
|
||||
double conversionRate = totalQuotes > 0 ? (double) acceptedQuotes / totalQuotes * 100 : 0;
|
||||
stats.put("conversionRate", conversionRate);
|
||||
|
||||
// Montant total des devis acceptés
|
||||
Double totalRevenue = em.createQuery(
|
||||
"SELECT SUM(q.totalTTC) FROM Quote q WHERE q.status = :status", Double.class)
|
||||
.setParameter("status", QuoteStatus.ACCEPTED)
|
||||
.getSingleResult();
|
||||
stats.put("totalRevenue", totalRevenue != null ? totalRevenue : 0.0);
|
||||
|
||||
// Ticket moyen
|
||||
double averageTicket = acceptedQuotes > 0 ? totalRevenue / acceptedQuotes : 0;
|
||||
stats.put("averageTicket", averageTicket);
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Personnalise un devis existant
|
||||
*/
|
||||
@Transactional
|
||||
public Quote customizeQuote(Long quoteId, QuoteCustomizationDTO customization) {
|
||||
Quote quote = em.find(Quote.class, quoteId);
|
||||
if (quote == null) {
|
||||
throw new IllegalArgumentException("Devis non trouvé");
|
||||
}
|
||||
|
||||
// Mise à jour des modules
|
||||
if (customization.getModules() != null) {
|
||||
quote.getModules().clear();
|
||||
|
||||
for (QuoteModuleDTO moduleDTO : customization.getModules()) {
|
||||
QuoteModule module = new QuoteModule();
|
||||
module.setQuote(quote);
|
||||
module.setModuleCode(moduleDTO.getModuleCode());
|
||||
module.setModuleName(moduleDTO.getModuleName());
|
||||
module.setDescription(moduleDTO.getDescription());
|
||||
module.setUnitPrice(moduleDTO.getUnitPrice());
|
||||
module.setQuantity(moduleDTO.getQuantity());
|
||||
module.setComplexity(moduleDTO.getComplexity());
|
||||
|
||||
quote.getModules().add(module);
|
||||
}
|
||||
}
|
||||
|
||||
// Mise à jour des services
|
||||
if (customization.getFormationHours() != null) {
|
||||
quote.setFormationHours(customization.getFormationHours());
|
||||
}
|
||||
if (customization.getSupportMonths() != null) {
|
||||
quote.setSupportMonths(customization.getSupportMonths());
|
||||
}
|
||||
|
||||
// Mise à jour des conditions
|
||||
if (customization.getPaymentTerms() != null) {
|
||||
quote.setPaymentTerms(customization.getPaymentTerms());
|
||||
}
|
||||
if (customization.getDeliveryTerms() != null) {
|
||||
quote.setDeliveryTerms(customization.getDeliveryTerms());
|
||||
}
|
||||
|
||||
// Recalcul des totaux
|
||||
quote.calculateTotals();
|
||||
|
||||
return quote;
|
||||
}
|
||||
}
|
||||
256
src/main/java/dev/lions/roi/ROICalculator.java
Normal file
256
src/main/java/dev/lions/roi/ROICalculator.java
Normal file
@@ -0,0 +1,256 @@
|
||||
package dev.lions.roi;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import java.util.Map;
|
||||
import java.util.HashMap;
|
||||
|
||||
/**
|
||||
* Calculateur de ROI pour démontrer la valeur de la digitalisation
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class ROICalculator {
|
||||
|
||||
/**
|
||||
* Calcule le ROI basé sur les gains de productivité
|
||||
*/
|
||||
public ROIResult calculateROI(ROIInput input) {
|
||||
ROIResult result = new ROIResult();
|
||||
|
||||
// Calculs des gains annuels
|
||||
double productivityGains = calculateProductivityGains(input);
|
||||
double errorReduction = calculateErrorReduction(input);
|
||||
double timesSavings = calculateTimeSavings(input);
|
||||
double complianceGains = calculateComplianceGains(input);
|
||||
|
||||
double totalAnnualGains = productivityGains + errorReduction + timesSavings + complianceGains;
|
||||
|
||||
// Coûts
|
||||
double implementationCost = input.getInvestmentAmount();
|
||||
double annualMaintenanceCost = implementationCost * 0.15; // 15% par an
|
||||
|
||||
// ROI sur 3 ans
|
||||
double totalGains3Years = totalAnnualGains * 3;
|
||||
double totalCosts3Years = implementationCost + (annualMaintenanceCost * 3);
|
||||
|
||||
double roi3Years = ((totalGains3Years - totalCosts3Years) / totalCosts3Years) * 100;
|
||||
|
||||
// Période de retour sur investissement
|
||||
double paybackPeriod = implementationCost / totalAnnualGains;
|
||||
|
||||
// Remplissage du résultat
|
||||
result.setAnnualProductivityGains(productivityGains);
|
||||
result.setAnnualErrorReduction(errorReduction);
|
||||
result.setAnnualTimeSavings(timesSavings);
|
||||
result.setAnnualComplianceGains(complianceGains);
|
||||
result.setTotalAnnualGains(totalAnnualGains);
|
||||
result.setImplementationCost(implementationCost);
|
||||
result.setAnnualMaintenanceCost(annualMaintenanceCost);
|
||||
result.setRoi3Years(roi3Years);
|
||||
result.setPaybackPeriodMonths(paybackPeriod * 12);
|
||||
result.setNetPresentValue3Years(totalGains3Years - totalCosts3Years);
|
||||
|
||||
// Détails par catégorie
|
||||
result.setGainsByCategory(Map.of(
|
||||
"Productivité", productivityGains,
|
||||
"Réduction erreurs", errorReduction,
|
||||
"Gain de temps", timesSavings,
|
||||
"Conformité", complianceGains
|
||||
));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private double calculateProductivityGains(ROIInput input) {
|
||||
// Gain de productivité basé sur l'automatisation
|
||||
double baseProductivity = input.getEmployeeCount() * input.getAverageSalary() * 0.12; // 12% de gain
|
||||
|
||||
// Ajustement selon les modules
|
||||
double multiplier = 1.0;
|
||||
if (input.getSelectedModules().contains("CRM")) multiplier += 0.15;
|
||||
if (input.getSelectedModules().contains("STOCK")) multiplier += 0.10;
|
||||
if (input.getSelectedModules().contains("COMPTA")) multiplier += 0.08;
|
||||
if (input.getSelectedModules().contains("RH")) multiplier += 0.05;
|
||||
|
||||
return baseProductivity * multiplier;
|
||||
}
|
||||
|
||||
private double calculateErrorReduction(ROIInput input) {
|
||||
// Réduction des erreurs et reprises
|
||||
double currentErrorCost = input.getTurnover() * 0.02; // 2% du CA en erreurs
|
||||
double reductionRate = 0.70; // 70% de réduction des erreurs
|
||||
|
||||
return currentErrorCost * reductionRate;
|
||||
}
|
||||
|
||||
private double calculateTimeSavings(ROIInput input) {
|
||||
// Gain de temps sur les tâches administratives
|
||||
double adminTimeHours = input.getEmployeeCount() * 2 * 250; // 2h/jour/employé, 250 jours/an
|
||||
double hourlyRate = input.getAverageSalary() / (8 * 250); // Taux horaire
|
||||
double timeSavingRate = 0.40; // 40% de gain de temps
|
||||
|
||||
return adminTimeHours * hourlyRate * timeSavingRate;
|
||||
}
|
||||
|
||||
private double calculateComplianceGains(ROIInput input) {
|
||||
// Évitement des pénalités et amendes
|
||||
double potentialFines = 500000; // 500K FCFA de pénalités potentielles par an
|
||||
double complianceImprovement = 0.80; // 80% d'amélioration de la conformité
|
||||
|
||||
return potentialFines * complianceImprovement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère des recommandations basées sur le ROI
|
||||
*/
|
||||
public String generateRecommendations(ROIResult result) {
|
||||
StringBuilder recommendations = new StringBuilder();
|
||||
|
||||
if (result.getRoi3Years() > 200) {
|
||||
recommendations.append("🚀 ROI EXCELLENT (>200%) : Investissement hautement recommandé !\n\n");
|
||||
} else if (result.getRoi3Years() > 100) {
|
||||
recommendations.append("✅ ROI TRÈS BON (>100%) : Investissement très rentable.\n\n");
|
||||
} else if (result.getRoi3Years() > 50) {
|
||||
recommendations.append("👍 ROI CORRECT (>50%) : Investissement rentable à moyen terme.\n\n");
|
||||
} else {
|
||||
recommendations.append("⚠️ ROI FAIBLE (<50%) : Revoir la configuration ou étaler l'investissement.\n\n");
|
||||
}
|
||||
|
||||
recommendations.append("POINTS CLÉS :\n");
|
||||
recommendations.append(String.format("• Retour sur investissement en %.1f mois\n", result.getPaybackPeriodMonths()));
|
||||
recommendations.append(String.format("• Gains annuels : %,.0f FCFA\n", result.getTotalAnnualGains()));
|
||||
recommendations.append(String.format("• Bénéfice net sur 3 ans : %,.0f FCFA\n", result.getNetPresentValue3Years()));
|
||||
|
||||
recommendations.append("\nPRIORITÉS D'IMPLÉMENTATION :\n");
|
||||
|
||||
// Recommandations par gain le plus élevé
|
||||
result.getGainsByCategory().entrySet().stream()
|
||||
.sorted(Map.Entry.<String, Double>comparingByValue().reversed())
|
||||
.forEach(entry -> {
|
||||
recommendations.append(String.format("• %s : %,.0f FCFA/an\n",
|
||||
entry.getKey(), entry.getValue()));
|
||||
});
|
||||
|
||||
return recommendations.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule le ROI pour différents scénarios
|
||||
*/
|
||||
public Map<String, ROIResult> calculateScenarios(ROIInput baseInput) {
|
||||
Map<String, ROIResult> scenarios = new HashMap<>();
|
||||
|
||||
// Scénario conservateur (gains -30%)
|
||||
ROIInput conservative = baseInput.copy();
|
||||
conservative.setAverageSalary(baseInput.getAverageSalary() * 0.7);
|
||||
scenarios.put("Conservateur", calculateROI(conservative));
|
||||
|
||||
// Scénario réaliste (base)
|
||||
scenarios.put("Réaliste", calculateROI(baseInput));
|
||||
|
||||
// Scénario optimiste (gains +50%)
|
||||
ROIInput optimistic = baseInput.copy();
|
||||
optimistic.setAverageSalary(baseInput.getAverageSalary() * 1.5);
|
||||
scenarios.put("Optimiste", calculateROI(optimistic));
|
||||
|
||||
return scenarios;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Données d'entrée pour le calcul ROI
|
||||
*/
|
||||
class ROIInput {
|
||||
private int employeeCount;
|
||||
private double averageSalary; // Salaire moyen annuel
|
||||
private double turnover; // CA annuel
|
||||
private double investmentAmount; // Montant investissement
|
||||
private java.util.List<String> selectedModules;
|
||||
private String sector;
|
||||
|
||||
// Constructeurs
|
||||
public ROIInput() {}
|
||||
|
||||
public ROIInput copy() {
|
||||
ROIInput copy = new ROIInput();
|
||||
copy.employeeCount = this.employeeCount;
|
||||
copy.averageSalary = this.averageSalary;
|
||||
copy.turnover = this.turnover;
|
||||
copy.investmentAmount = this.investmentAmount;
|
||||
copy.selectedModules = new java.util.ArrayList<>(this.selectedModules);
|
||||
copy.sector = this.sector;
|
||||
return copy;
|
||||
}
|
||||
|
||||
// Getters et Setters
|
||||
public int getEmployeeCount() { return employeeCount; }
|
||||
public void setEmployeeCount(int employeeCount) { this.employeeCount = employeeCount; }
|
||||
|
||||
public double getAverageSalary() { return averageSalary; }
|
||||
public void setAverageSalary(double averageSalary) { this.averageSalary = averageSalary; }
|
||||
|
||||
public double getTurnover() { return turnover; }
|
||||
public void setTurnover(double turnover) { this.turnover = turnover; }
|
||||
|
||||
public double getInvestmentAmount() { return investmentAmount; }
|
||||
public void setInvestmentAmount(double investmentAmount) { this.investmentAmount = investmentAmount; }
|
||||
|
||||
public java.util.List<String> getSelectedModules() { return selectedModules; }
|
||||
public void setSelectedModules(java.util.List<String> selectedModules) { this.selectedModules = selectedModules; }
|
||||
|
||||
public String getSector() { return sector; }
|
||||
public void setSector(String sector) { this.sector = sector; }
|
||||
}
|
||||
|
||||
/**
|
||||
* Résultat du calcul ROI
|
||||
*/
|
||||
class ROIResult {
|
||||
private double annualProductivityGains;
|
||||
private double annualErrorReduction;
|
||||
private double annualTimeSavings;
|
||||
private double annualComplianceGains;
|
||||
private double totalAnnualGains;
|
||||
private double implementationCost;
|
||||
private double annualMaintenanceCost;
|
||||
private double roi3Years;
|
||||
private double paybackPeriodMonths;
|
||||
private double netPresentValue3Years;
|
||||
private Map<String, Double> gainsByCategory;
|
||||
|
||||
// Constructeurs
|
||||
public ROIResult() {}
|
||||
|
||||
// Getters et Setters
|
||||
public double getAnnualProductivityGains() { return annualProductivityGains; }
|
||||
public void setAnnualProductivityGains(double annualProductivityGains) { this.annualProductivityGains = annualProductivityGains; }
|
||||
|
||||
public double getAnnualErrorReduction() { return annualErrorReduction; }
|
||||
public void setAnnualErrorReduction(double annualErrorReduction) { this.annualErrorReduction = annualErrorReduction; }
|
||||
|
||||
public double getAnnualTimeSavings() { return annualTimeSavings; }
|
||||
public void setAnnualTimeSavings(double annualTimeSavings) { this.annualTimeSavings = annualTimeSavings; }
|
||||
|
||||
public double getAnnualComplianceGains() { return annualComplianceGains; }
|
||||
public void setAnnualComplianceGains(double annualComplianceGains) { this.annualComplianceGains = annualComplianceGains; }
|
||||
|
||||
public double getTotalAnnualGains() { return totalAnnualGains; }
|
||||
public void setTotalAnnualGains(double totalAnnualGains) { this.totalAnnualGains = totalAnnualGains; }
|
||||
|
||||
public double getImplementationCost() { return implementationCost; }
|
||||
public void setImplementationCost(double implementationCost) { this.implementationCost = implementationCost; }
|
||||
|
||||
public double getAnnualMaintenanceCost() { return annualMaintenanceCost; }
|
||||
public void setAnnualMaintenanceCost(double annualMaintenanceCost) { this.annualMaintenanceCost = annualMaintenanceCost; }
|
||||
|
||||
public double getRoi3Years() { return roi3Years; }
|
||||
public void setRoi3Years(double roi3Years) { this.roi3Years = roi3Years; }
|
||||
|
||||
public double getPaybackPeriodMonths() { return paybackPeriodMonths; }
|
||||
public void setPaybackPeriodMonths(double paybackPeriodMonths) { this.paybackPeriodMonths = paybackPeriodMonths; }
|
||||
|
||||
public double getNetPresentValue3Years() { return netPresentValue3Years; }
|
||||
public void setNetPresentValue3Years(double netPresentValue3Years) { this.netPresentValue3Years = netPresentValue3Years; }
|
||||
|
||||
public Map<String, Double> getGainsByCategory() { return gainsByCategory; }
|
||||
public void setGainsByCategory(Map<String, Double> gainsByCategory) { this.gainsByCategory = gainsByCategory; }
|
||||
}
|
||||
170
src/main/java/dev/lions/roi/ROIResource.java
Normal file
170
src/main/java/dev/lions/roi/ROIResource.java
Normal file
@@ -0,0 +1,170 @@
|
||||
package dev.lions.roi;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.ws.rs.*;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import java.util.Map;
|
||||
import java.util.List;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* API REST pour le calculateur ROI
|
||||
*/
|
||||
@Path("/api/roi")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public class ROIResource {
|
||||
|
||||
@Inject
|
||||
ROICalculator roiCalculator;
|
||||
|
||||
/**
|
||||
* Calcule le ROI pour une configuration donnée
|
||||
*/
|
||||
@POST
|
||||
@Path("/calculate")
|
||||
public Response calculateROI(ROIInput input) {
|
||||
try {
|
||||
// Validation des données
|
||||
if (input.getEmployeeCount() <= 0 || input.getInvestmentAmount() <= 0) {
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(Map.of("error", "Données invalides"))
|
||||
.build();
|
||||
}
|
||||
|
||||
ROIResult result = roiCalculator.calculateROI(input);
|
||||
String recommendations = roiCalculator.generateRecommendations(result);
|
||||
|
||||
Map<String, Object> response = Map.of(
|
||||
"result", result,
|
||||
"recommendations", recommendations
|
||||
);
|
||||
|
||||
return Response.ok(response).build();
|
||||
} catch (Exception e) {
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(Map.of("error", "Erreur lors du calcul ROI"))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule plusieurs scénarios ROI
|
||||
*/
|
||||
@POST
|
||||
@Path("/scenarios")
|
||||
public Response calculateScenarios(ROIInput input) {
|
||||
try {
|
||||
Map<String, ROIResult> scenarios = roiCalculator.calculateScenarios(input);
|
||||
|
||||
return Response.ok(scenarios).build();
|
||||
} catch (Exception e) {
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(Map.of("error", "Erreur lors du calcul des scénarios"))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule le ROI rapide basé sur un audit
|
||||
*/
|
||||
@GET
|
||||
@Path("/quick/{auditId}")
|
||||
public Response quickROI(@PathParam("auditId") Long auditId) {
|
||||
try {
|
||||
// Récupération des données d'audit (simulation)
|
||||
ROIInput input = new ROIInput();
|
||||
input.setEmployeeCount(10); // Valeur par défaut
|
||||
input.setAverageSalary(2400000); // 200K FCFA/mois
|
||||
input.setTurnover(50000000); // 50M FCFA/an
|
||||
input.setInvestmentAmount(500000); // 500K FCFA
|
||||
input.setSelectedModules(Arrays.asList("CRM", "COMPTA"));
|
||||
input.setSector("commerce");
|
||||
|
||||
ROIResult result = roiCalculator.calculateROI(input);
|
||||
String recommendations = roiCalculator.generateRecommendations(result);
|
||||
|
||||
Map<String, Object> response = Map.of(
|
||||
"result", result,
|
||||
"recommendations", recommendations,
|
||||
"input", input
|
||||
);
|
||||
|
||||
return Response.ok(response).build();
|
||||
} catch (Exception e) {
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(Map.of("error", "Erreur lors du calcul ROI rapide"))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne les paramètres par défaut selon le secteur
|
||||
*/
|
||||
@GET
|
||||
@Path("/defaults/{sector}")
|
||||
public Response getDefaults(@PathParam("sector") String sector) {
|
||||
ROIInput defaults = new ROIInput();
|
||||
|
||||
switch (sector.toLowerCase()) {
|
||||
case "commerce":
|
||||
defaults.setAverageSalary(2400000); // 200K/mois
|
||||
defaults.setSelectedModules(Arrays.asList("CRM", "STOCK", "COMPTA"));
|
||||
break;
|
||||
case "services":
|
||||
defaults.setAverageSalary(3000000); // 250K/mois
|
||||
defaults.setSelectedModules(Arrays.asList("CRM", "RH", "COMPTA"));
|
||||
break;
|
||||
case "industrie":
|
||||
defaults.setAverageSalary(3600000); // 300K/mois
|
||||
defaults.setSelectedModules(Arrays.asList("STOCK", "COMPTA", "RH"));
|
||||
break;
|
||||
default:
|
||||
defaults.setAverageSalary(2400000);
|
||||
defaults.setSelectedModules(Arrays.asList("CRM", "COMPTA"));
|
||||
}
|
||||
|
||||
defaults.setSector(sector);
|
||||
|
||||
return Response.ok(defaults).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère un rapport ROI détaillé
|
||||
*/
|
||||
@POST
|
||||
@Path("/report")
|
||||
public Response generateReport(ROIInput input) {
|
||||
try {
|
||||
ROIResult result = roiCalculator.calculateROI(input);
|
||||
Map<String, ROIResult> scenarios = roiCalculator.calculateScenarios(input);
|
||||
String recommendations = roiCalculator.generateRecommendations(result);
|
||||
|
||||
Map<String, Object> report = Map.of(
|
||||
"input", input,
|
||||
"baseResult", result,
|
||||
"scenarios", scenarios,
|
||||
"recommendations", recommendations,
|
||||
"summary", generateSummary(result)
|
||||
);
|
||||
|
||||
return Response.ok(report).build();
|
||||
} catch (Exception e) {
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(Map.of("error", "Erreur lors de la génération du rapport"))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Object> generateSummary(ROIResult result) {
|
||||
return Map.of(
|
||||
"roiPercentage", Math.round(result.getRoi3Years()),
|
||||
"paybackMonths", Math.round(result.getPaybackPeriodMonths()),
|
||||
"annualSavings", Math.round(result.getTotalAnnualGains()),
|
||||
"netBenefit", Math.round(result.getNetPresentValue3Years()),
|
||||
"recommendation", result.getRoi3Years() > 100 ? "RECOMMANDÉ" :
|
||||
result.getRoi3Years() > 50 ? "ACCEPTABLE" : "À REVOIR"
|
||||
);
|
||||
}
|
||||
}
|
||||
637
src/main/resources/META-INF/resources/audit.html
Normal file
637
src/main/resources/META-INF/resources/audit.html
Normal file
@@ -0,0 +1,637 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Audit Gratuit de Maturité Digitale - Lions Dev</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<style>
|
||||
.audit-container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.audit-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.audit-progress {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background: #e0e0e0;
|
||||
border-radius: 4px;
|
||||
margin: 20px 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.audit-progress-bar {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #4CAF50, #45a049);
|
||||
transition: width 0.3s ease;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.audit-section {
|
||||
margin-bottom: 30px;
|
||||
padding: 20px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.audit-section.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 1.5em;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.section-icon {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin-right: 10px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.question {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
background: #f9f9f9;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.question-text {
|
||||
font-weight: 500;
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.question-options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
background: white;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.option:hover {
|
||||
border-color: #667eea;
|
||||
background: #f0f4ff;
|
||||
}
|
||||
|
||||
.option.selected {
|
||||
border-color: #667eea;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.option input[type="radio"] {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.navigation {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #5a6fd8;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.company-info {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-weight: 500;
|
||||
margin-bottom: 5px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select {
|
||||
padding: 10px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group select:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.results-container {
|
||||
display: none;
|
||||
text-align: center;
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.score-display {
|
||||
font-size: 3em;
|
||||
font-weight: bold;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.score-excellent { color: #4CAF50; }
|
||||
.score-good { color: #2196F3; }
|
||||
.score-average { color: #FF9800; }
|
||||
.score-poor { color: #F44336; }
|
||||
|
||||
.recommendations {
|
||||
text-align: left;
|
||||
background: #f9f9f9;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.budget-estimation {
|
||||
background: #e3f2fd;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.company-info {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.audit-container {
|
||||
margin: 10px;
|
||||
padding: 15px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="audit-container">
|
||||
<!-- En-tête -->
|
||||
<div class="audit-header">
|
||||
<h1>🦁 Audit Gratuit de Maturité Digitale</h1>
|
||||
<p>Évaluez le niveau de digitalisation de votre PME en 10 minutes</p>
|
||||
<p><strong>100% Gratuit • Rapport PDF personnalisé • Recommandations d'experts</strong></p>
|
||||
</div>
|
||||
|
||||
<!-- Barre de progression -->
|
||||
<div class="audit-progress">
|
||||
<div class="audit-progress-bar" id="progressBar" style="width: 0%"></div>
|
||||
</div>
|
||||
<div style="text-align: center; margin-bottom: 20px;">
|
||||
<span id="progressText">Étape 1 sur 6</span>
|
||||
</div>
|
||||
|
||||
<!-- Formulaire d'audit -->
|
||||
<form id="auditForm">
|
||||
<!-- Section 0: Informations entreprise -->
|
||||
<div class="audit-section active" data-section="0">
|
||||
<div class="section-title">
|
||||
<div class="section-icon" style="background: #4CAF50; color: white;">🏢</div>
|
||||
Informations sur votre entreprise
|
||||
</div>
|
||||
|
||||
<div class="company-info">
|
||||
<div class="form-group">
|
||||
<label for="companyName">Nom de l'entreprise *</label>
|
||||
<input type="text" id="companyName" name="companyName" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="contactName">Votre nom *</label>
|
||||
<input type="text" id="contactName" name="contactName" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="email">Email professionnel *</label>
|
||||
<input type="email" id="email" name="email" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="phone">Téléphone</label>
|
||||
<input type="tel" id="phone" name="phone">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="sector">Secteur d'activité</label>
|
||||
<select id="sector" name="sector">
|
||||
<option value="">Sélectionnez...</option>
|
||||
<option value="commerce">Commerce/Distribution</option>
|
||||
<option value="services">Services</option>
|
||||
<option value="industrie">Industrie/Manufacturing</option>
|
||||
<option value="agriculture">Agriculture/Agro-alimentaire</option>
|
||||
<option value="btp">BTP/Construction</option>
|
||||
<option value="transport">Transport/Logistique</option>
|
||||
<option value="sante">Santé</option>
|
||||
<option value="education">Éducation/Formation</option>
|
||||
<option value="autre">Autre</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="employeeCount">Nombre d'employés</label>
|
||||
<select id="employeeCount" name="employeeCount">
|
||||
<option value="">Sélectionnez...</option>
|
||||
<option value="1">1-5 employés</option>
|
||||
<option value="6">6-10 employés</option>
|
||||
<option value="11">11-20 employés</option>
|
||||
<option value="21">21-50 employés</option>
|
||||
<option value="51">51-100 employés</option>
|
||||
<option value="101">Plus de 100 employés</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="turnover">Chiffre d'affaires annuel</label>
|
||||
<select id="turnover" name="turnover">
|
||||
<option value="">Sélectionnez...</option>
|
||||
<option value="< 10M">Moins de 10M FCFA</option>
|
||||
<option value="10-50M">10-50M FCFA</option>
|
||||
<option value="50-100M">50-100M FCFA</option>
|
||||
<option value="100-500M">100-500M FCFA</option>
|
||||
<option value="500M-1B">500M-1B FCFA</option>
|
||||
<option value="> 1B">Plus de 1B FCFA</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Les sections de questions seront générées dynamiquement -->
|
||||
<div id="questionsContainer"></div>
|
||||
|
||||
<!-- Section résultats -->
|
||||
<div class="results-container" id="resultsContainer">
|
||||
<h2>🎉 Votre Audit est Terminé !</h2>
|
||||
<div class="score-display" id="scoreDisplay">---%</div>
|
||||
<div id="maturityLevel"></div>
|
||||
|
||||
<div class="recommendations" id="recommendationsDisplay">
|
||||
<!-- Recommandations générées -->
|
||||
</div>
|
||||
|
||||
<div class="budget-estimation" id="budgetDisplay">
|
||||
<!-- Estimation budgétaire -->
|
||||
</div>
|
||||
|
||||
<p><strong>📧 Votre rapport détaillé vous a été envoyé par email</strong></p>
|
||||
<p>Notre équipe vous contactera dans les 24h pour planifier un rendez-vous diagnostic gratuit.</p>
|
||||
|
||||
<button type="button" class="btn btn-primary" onclick="requestMeeting()">
|
||||
📅 Demander un Rendez-vous Maintenant
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Navigation -->
|
||||
<div class="navigation">
|
||||
<button type="button" class="btn btn-secondary" id="prevBtn" onclick="previousSection()" style="display: none;">
|
||||
← Précédent
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" id="nextBtn" onclick="nextSection()">
|
||||
Suivant →
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Variables globales
|
||||
let currentSection = 0;
|
||||
let questions = {};
|
||||
let answers = {};
|
||||
let totalSections = 0;
|
||||
|
||||
// Initialisation
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadQuestions();
|
||||
});
|
||||
|
||||
// Chargement des questions depuis l'API
|
||||
async function loadQuestions() {
|
||||
try {
|
||||
const response = await fetch('/api/audit/questions');
|
||||
questions = await response.json();
|
||||
generateQuestionSections();
|
||||
updateProgress();
|
||||
} catch (error) {
|
||||
console.error('Erreur chargement questions:', error);
|
||||
alert('Erreur lors du chargement du questionnaire. Veuillez rafraîchir la page.');
|
||||
}
|
||||
}
|
||||
|
||||
// Génération des sections de questions
|
||||
function generateQuestionSections() {
|
||||
const container = document.getElementById('questionsContainer');
|
||||
const categories = Object.keys(questions);
|
||||
totalSections = categories.length + 1; // +1 pour les infos entreprise
|
||||
|
||||
categories.forEach((category, index) => {
|
||||
const section = createQuestionSection(category, questions[category], index + 1);
|
||||
container.appendChild(section);
|
||||
});
|
||||
}
|
||||
|
||||
// Création d'une section de questions
|
||||
function createQuestionSection(category, categoryQuestions, sectionIndex) {
|
||||
const section = document.createElement('div');
|
||||
section.className = 'audit-section';
|
||||
section.setAttribute('data-section', sectionIndex);
|
||||
|
||||
const categoryIcons = {
|
||||
'commercial': '💼',
|
||||
'stock': '📦',
|
||||
'comptabilite': '💰',
|
||||
'rh': '👥',
|
||||
'infrastructure': '🖥️'
|
||||
};
|
||||
|
||||
const categoryNames = {
|
||||
'commercial': 'Gestion Commerciale',
|
||||
'stock': 'Gestion des Stocks',
|
||||
'comptabilite': 'Comptabilité',
|
||||
'rh': 'Ressources Humaines',
|
||||
'infrastructure': 'Infrastructure IT'
|
||||
};
|
||||
|
||||
section.innerHTML = `
|
||||
<div class="section-title">
|
||||
<div class="section-icon" style="background: #667eea; color: white;">
|
||||
${categoryIcons[category] || '📋'}
|
||||
</div>
|
||||
${categoryNames[category] || category}
|
||||
</div>
|
||||
`;
|
||||
|
||||
categoryQuestions.forEach(question => {
|
||||
const questionDiv = createQuestionElement(question);
|
||||
section.appendChild(questionDiv);
|
||||
});
|
||||
|
||||
return section;
|
||||
}
|
||||
|
||||
// Création d'un élément question
|
||||
function createQuestionElement(question) {
|
||||
const questionDiv = document.createElement('div');
|
||||
questionDiv.className = 'question';
|
||||
|
||||
const optionsHtml = question.options.map((option, index) => `
|
||||
<div class="option" onclick="selectOption(${question.id}, ${index}, this)">
|
||||
<input type="radio" name="question_${question.id}" value="${index}" style="display: none;">
|
||||
${option}
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
questionDiv.innerHTML = `
|
||||
<div class="question-text">${question.question}</div>
|
||||
<div class="question-options">
|
||||
${optionsHtml}
|
||||
</div>
|
||||
`;
|
||||
|
||||
return questionDiv;
|
||||
}
|
||||
|
||||
// Sélection d'une option
|
||||
function selectOption(questionId, optionIndex, element) {
|
||||
// Désélectionner les autres options de cette question
|
||||
const questionDiv = element.closest('.question');
|
||||
questionDiv.querySelectorAll('.option').forEach(opt => opt.classList.remove('selected'));
|
||||
|
||||
// Sélectionner l'option cliquée
|
||||
element.classList.add('selected');
|
||||
element.querySelector('input').checked = true;
|
||||
|
||||
// Enregistrer la réponse
|
||||
answers[questionId] = optionIndex;
|
||||
}
|
||||
|
||||
// Navigation
|
||||
function nextSection() {
|
||||
if (currentSection === 0) {
|
||||
// Validation des informations entreprise
|
||||
if (!validateCompanyInfo()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentSection < totalSections) {
|
||||
document.querySelector(`[data-section="${currentSection}"]`).classList.remove('active');
|
||||
currentSection++;
|
||||
|
||||
if (currentSection < totalSections) {
|
||||
document.querySelector(`[data-section="${currentSection}"]`).classList.add('active');
|
||||
updateProgress();
|
||||
updateNavigation();
|
||||
} else {
|
||||
// Soumission de l'audit
|
||||
submitAudit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function previousSection() {
|
||||
if (currentSection > 0) {
|
||||
document.querySelector(`[data-section="${currentSection}"]`).classList.remove('active');
|
||||
currentSection--;
|
||||
document.querySelector(`[data-section="${currentSection}"]`).classList.add('active');
|
||||
updateProgress();
|
||||
updateNavigation();
|
||||
}
|
||||
}
|
||||
|
||||
// Mise à jour de la barre de progression
|
||||
function updateProgress() {
|
||||
const progress = (currentSection / totalSections) * 100;
|
||||
document.getElementById('progressBar').style.width = progress + '%';
|
||||
document.getElementById('progressText').textContent = `Étape ${currentSection + 1} sur ${totalSections + 1}`;
|
||||
}
|
||||
|
||||
// Mise à jour de la navigation
|
||||
function updateNavigation() {
|
||||
const prevBtn = document.getElementById('prevBtn');
|
||||
const nextBtn = document.getElementById('nextBtn');
|
||||
|
||||
prevBtn.style.display = currentSection > 0 ? 'block' : 'none';
|
||||
nextBtn.textContent = currentSection === totalSections - 1 ? 'Terminer l\'Audit' : 'Suivant →';
|
||||
}
|
||||
|
||||
// Validation des informations entreprise
|
||||
function validateCompanyInfo() {
|
||||
const companyName = document.getElementById('companyName').value.trim();
|
||||
const contactName = document.getElementById('contactName').value.trim();
|
||||
const email = document.getElementById('email').value.trim();
|
||||
|
||||
if (!companyName || !contactName || !email) {
|
||||
alert('Veuillez remplir tous les champs obligatoires (*)');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isValidEmail(email)) {
|
||||
alert('Veuillez saisir un email valide');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Validation email
|
||||
function isValidEmail(email) {
|
||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
||||
}
|
||||
|
||||
// Soumission de l'audit
|
||||
async function submitAudit() {
|
||||
try {
|
||||
// Affichage du loading
|
||||
document.getElementById('nextBtn').textContent = 'Traitement en cours...';
|
||||
document.getElementById('nextBtn').disabled = true;
|
||||
|
||||
const submission = {
|
||||
companyName: document.getElementById('companyName').value,
|
||||
contactName: document.getElementById('contactName').value,
|
||||
email: document.getElementById('email').value,
|
||||
phone: document.getElementById('phone').value,
|
||||
sector: document.getElementById('sector').value,
|
||||
employeeCount: parseInt(document.getElementById('employeeCount').value) || null,
|
||||
turnover: document.getElementById('turnover').value,
|
||||
answers: answers
|
||||
};
|
||||
|
||||
const response = await fetch('/api/audit/submit', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(submission)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
displayResults(result);
|
||||
} else {
|
||||
const error = await response.json();
|
||||
alert('Erreur: ' + (error.error || 'Erreur lors de la soumission'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur soumission:', error);
|
||||
alert('Erreur lors de la soumission. Veuillez réessayer.');
|
||||
} finally {
|
||||
document.getElementById('nextBtn').disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Affichage des résultats
|
||||
function displayResults(result) {
|
||||
// Masquer le formulaire
|
||||
document.querySelector('.navigation').style.display = 'none';
|
||||
document.querySelector(`[data-section="${currentSection}"]`).classList.remove('active');
|
||||
|
||||
// Afficher les résultats
|
||||
const resultsContainer = document.getElementById('resultsContainer');
|
||||
resultsContainer.style.display = 'block';
|
||||
|
||||
// Score
|
||||
const scoreDisplay = document.getElementById('scoreDisplay');
|
||||
scoreDisplay.textContent = result.maturityPercentage.toFixed(1) + '%';
|
||||
|
||||
// Couleur du score
|
||||
if (result.maturityPercentage >= 80) {
|
||||
scoreDisplay.className = 'score-display score-excellent';
|
||||
document.getElementById('maturityLevel').textContent = '🏆 Niveau Expert';
|
||||
} else if (result.maturityPercentage >= 60) {
|
||||
scoreDisplay.className = 'score-display score-good';
|
||||
document.getElementById('maturityLevel').textContent = '✅ Niveau Avancé';
|
||||
} else if (result.maturityPercentage >= 30) {
|
||||
scoreDisplay.className = 'score-display score-average';
|
||||
document.getElementById('maturityLevel').textContent = '⚠️ Niveau Intermédiaire';
|
||||
} else {
|
||||
scoreDisplay.className = 'score-display score-poor';
|
||||
document.getElementById('maturityLevel').textContent = '🚨 Niveau Débutant';
|
||||
}
|
||||
|
||||
// Recommandations
|
||||
document.getElementById('recommendationsDisplay').innerHTML = `
|
||||
<h3>📋 Recommandations Personnalisées</h3>
|
||||
<p>${result.recommendations.replace(/\n/g, '<br>')}</p>
|
||||
<p><strong>Actions prioritaires:</strong> ${result.priorityActions}</p>
|
||||
`;
|
||||
|
||||
// Budget
|
||||
if (result.estimatedBudgetMin && result.estimatedBudgetMax) {
|
||||
document.getElementById('budgetDisplay').innerHTML = `
|
||||
<h3>💰 Estimation Budgétaire</h3>
|
||||
<p>Investissement estimé pour votre digitalisation:</p>
|
||||
<p><strong>${result.estimatedBudgetMin.toLocaleString()} - ${result.estimatedBudgetMax.toLocaleString()} FCFA</strong></p>
|
||||
<p><small>* Estimation basée sur votre audit. Devis personnalisé après analyse détaillée.</small></p>
|
||||
`;
|
||||
}
|
||||
|
||||
// Mise à jour de la progression
|
||||
document.getElementById('progressBar').style.width = '100%';
|
||||
document.getElementById('progressText').textContent = 'Audit Terminé ✅';
|
||||
|
||||
// Scroll vers les résultats
|
||||
resultsContainer.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
|
||||
// Demande de rendez-vous
|
||||
function requestMeeting() {
|
||||
// Redirection vers formulaire de RDV ou ouverture modal
|
||||
window.location.href = '/contact?source=audit';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
419
src/main/resources/META-INF/resources/pme.html
Normal file
419
src/main/resources/META-INF/resources/pme.html
Normal file
@@ -0,0 +1,419 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Lions Dev - Solutions ERP pour PME Ivoiriennes</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="icon" href="favicon.ico">
|
||||
<style>
|
||||
/* PME-specific styles */
|
||||
.hero-pme {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 80px 0;
|
||||
text-align: center;
|
||||
}
|
||||
.hero-pme h1 {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.hero-pme .highlight {
|
||||
color: #ffd700;
|
||||
}
|
||||
.hero-stats {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 3rem;
|
||||
margin: 2rem 0;
|
||||
}
|
||||
.stat {
|
||||
text-align: center;
|
||||
}
|
||||
.stat-number {
|
||||
font-size: 2.5rem;
|
||||
font-weight: bold;
|
||||
color: #ffd700;
|
||||
}
|
||||
.stat-label {
|
||||
font-size: 1rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.solutions-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 2rem;
|
||||
margin: 3rem 0;
|
||||
}
|
||||
.solution-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
|
||||
transition: transform 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
.solution-card:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
.solution-card.popular::before {
|
||||
content: "Plus populaire";
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
right: 20px;
|
||||
background: #ff6b6b;
|
||||
color: white;
|
||||
padding: 5px 15px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.solution-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.solution-features {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
.solution-features li {
|
||||
padding: 0.5rem 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
.solution-features li:before {
|
||||
content: "✓";
|
||||
color: #28a745;
|
||||
font-weight: bold;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
.solution-price {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
color: #667eea;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 12px 24px;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 6px;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
.btn:hover {
|
||||
background: #5a6fd8;
|
||||
}
|
||||
.btn-primary {
|
||||
background: #667eea;
|
||||
}
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
}
|
||||
.btn-large {
|
||||
padding: 16px 32px;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 2rem;
|
||||
}
|
||||
.problems-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 2rem;
|
||||
margin: 3rem 0;
|
||||
}
|
||||
.problem-card {
|
||||
background: #f8f9fa;
|
||||
padding: 2rem;
|
||||
border-radius: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
.problem-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.process-steps {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 2rem;
|
||||
margin: 3rem 0;
|
||||
}
|
||||
.step {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
.step-number {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
margin: 0 auto 1rem;
|
||||
}
|
||||
.cta-section {
|
||||
background: #f8f9fa;
|
||||
padding: 4rem 0;
|
||||
text-align: center;
|
||||
}
|
||||
.navbar {
|
||||
background: white;
|
||||
padding: 1rem 0;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
.nav-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.nav-menu {
|
||||
display: flex;
|
||||
list-style: none;
|
||||
gap: 2rem;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.nav-menu a {
|
||||
text-decoration: none;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
.logo {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
color: #667eea;
|
||||
text-decoration: none;
|
||||
}
|
||||
.footer {
|
||||
background: #333;
|
||||
color: white;
|
||||
padding: 3rem 0 1rem;
|
||||
}
|
||||
.footer-content {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 2rem;
|
||||
}
|
||||
.footer-bottom {
|
||||
text-align: center;
|
||||
margin-top: 2rem;
|
||||
padding-top: 2rem;
|
||||
border-top: 1px solid #555;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar">
|
||||
<div class="container">
|
||||
<div class="nav-content">
|
||||
<a href="/" class="logo">🦁 Lions Dev</a>
|
||||
<ul class="nav-menu">
|
||||
<li><a href="#solutions">Solutions</a></li>
|
||||
<li><a href="audit.html">Audit Gratuit</a></li>
|
||||
<li><a href="roi-calculator.html">ROI</a></li>
|
||||
<li><a href="#contact">Contact</a></li>
|
||||
</ul>
|
||||
<a href="audit.html" class="btn btn-primary">Audit Gratuit</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main>
|
||||
<!-- Hero Section -->
|
||||
<section class="hero-pme">
|
||||
<div class="container">
|
||||
<h1>
|
||||
Digitalisez votre PME avec des solutions ERP
|
||||
<span class="highlight">100% adaptées à la Côte d'Ivoire</span>
|
||||
</h1>
|
||||
<p style="font-size: 1.2rem; margin: 1rem 0;">
|
||||
Gestion intégrée • Conformité fiscale • ROI garanti • Support local
|
||||
</p>
|
||||
<div class="hero-stats">
|
||||
<div class="stat">
|
||||
<div class="stat-number">50+</div>
|
||||
<div class="stat-label">PME accompagnées</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-number">300%</div>
|
||||
<div class="stat-label">ROI moyen</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-number">6 mois</div>
|
||||
<div class="stat-label">Retour sur investissement</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 2rem;">
|
||||
<a href="audit.html" class="btn btn-large" style="background: #ffd700; color: #333; margin-right: 1rem;">
|
||||
🎯 Audit Gratuit de Maturité Digitale
|
||||
</a>
|
||||
<a href="roi-calculator.html" class="btn btn-large btn-secondary">
|
||||
💰 Calculer mon ROI
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Problems Section -->
|
||||
<section style="padding: 4rem 0;">
|
||||
<div class="container">
|
||||
<h2 style="text-align: center; margin-bottom: 3rem;">Les défis des PME ivoiriennes</h2>
|
||||
<div class="problems-grid">
|
||||
<div class="problem-card">
|
||||
<div class="problem-icon">📊</div>
|
||||
<h3>Gestion manuelle</h3>
|
||||
<p>Excel, papier, calculs manuels... Perte de temps et erreurs fréquentes</p>
|
||||
</div>
|
||||
<div class="problem-card">
|
||||
<div class="problem-icon">⚖️</div>
|
||||
<h3>Conformité fiscale</h3>
|
||||
<p>TVA, IS, CNPS... Déclarations complexes et risques de pénalités</p>
|
||||
</div>
|
||||
<div class="problem-card">
|
||||
<div class="problem-icon">📈</div>
|
||||
<h3>Manque de visibilité</h3>
|
||||
<p>Pas de tableaux de bord, décisions prises "au feeling"</p>
|
||||
</div>
|
||||
<div class="problem-card">
|
||||
<div class="problem-icon">💸</div>
|
||||
<h3>Coûts cachés</h3>
|
||||
<p>Erreurs, retards, temps perdu... Impact sur la rentabilité</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Solutions Section -->
|
||||
<section id="solutions" style="padding: 4rem 0; background: #f8f9fa;">
|
||||
<div class="container">
|
||||
<h2 style="text-align: center; margin-bottom: 3rem;">Nos Solutions ERP Modulaires</h2>
|
||||
<div class="solutions-grid">
|
||||
<div class="solution-card popular">
|
||||
<div class="solution-icon">🤝</div>
|
||||
<h3>CRM Commercial</h3>
|
||||
<p>Gestion complète de la relation client</p>
|
||||
<ul class="solution-features">
|
||||
<li>Pipeline de ventes</li>
|
||||
<li>Suivi des prospects</li>
|
||||
<li>Facturation intégrée</li>
|
||||
<li>Rapports commerciaux</li>
|
||||
</ul>
|
||||
<div class="solution-price">À partir de 150K FCFA</div>
|
||||
<a href="quote-configurator.html?module=CRM" class="btn">Configurer</a>
|
||||
</div>
|
||||
|
||||
<div class="solution-card">
|
||||
<div class="solution-icon">📦</div>
|
||||
<h3>Gestion des Stocks</h3>
|
||||
<p>Inventaire automatisé avec codes-barres</p>
|
||||
<ul class="solution-features">
|
||||
<li>Multi-entrepôts</li>
|
||||
<li>Codes-barres/QR codes</li>
|
||||
<li>Alertes stock minimum</li>
|
||||
<li>Valorisation FIFO/LIFO</li>
|
||||
</ul>
|
||||
<div class="solution-price">À partir de 120K FCFA</div>
|
||||
<a href="quote-configurator.html?module=STOCK" class="btn">Configurer</a>
|
||||
</div>
|
||||
|
||||
<div class="solution-card popular">
|
||||
<div class="solution-icon">💼</div>
|
||||
<h3>Comptabilité Intégrée</h3>
|
||||
<p>Conforme aux normes ivoiriennes</p>
|
||||
<ul class="solution-features">
|
||||
<li>Plan comptable SYSCOHADA</li>
|
||||
<li>Déclarations TVA/IS automatiques</li>
|
||||
<li>Intégration bancaire</li>
|
||||
<li>Rapports DGI</li>
|
||||
</ul>
|
||||
<div class="solution-price">À partir de 180K FCFA</div>
|
||||
<a href="quote-configurator.html?module=COMPTA" class="btn">Configurer</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Process Section -->
|
||||
<section style="padding: 4rem 0;">
|
||||
<div class="container">
|
||||
<h2 style="text-align: center; margin-bottom: 3rem;">Notre Approche en 4 Étapes</h2>
|
||||
<div class="process-steps">
|
||||
<div class="step">
|
||||
<div class="step-number">1</div>
|
||||
<h3>Audit Gratuit</h3>
|
||||
<p>Évaluation de votre maturité digitale en 15 minutes</p>
|
||||
<a href="audit.html" class="btn">Commencer l'audit</a>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-number">2</div>
|
||||
<h3>Devis Personnalisé</h3>
|
||||
<p>Configuration sur mesure selon vos besoins</p>
|
||||
<a href="quote-configurator.html" class="btn">Configurer</a>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-number">3</div>
|
||||
<h3>Implémentation</h3>
|
||||
<p>Déploiement et formation de vos équipes</p>
|
||||
</div>
|
||||
<div class="step">
|
||||
<div class="step-number">4</div>
|
||||
<h3>Support Continu</h3>
|
||||
<p>Accompagnement et évolutions</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CTA Section -->
|
||||
<section class="cta-section">
|
||||
<div class="container">
|
||||
<h2>Prêt à digitaliser votre PME ?</h2>
|
||||
<p>Rejoignez les 50+ entreprises ivoiriennes qui nous font confiance</p>
|
||||
<div style="margin-top: 2rem;">
|
||||
<a href="audit.html" class="btn btn-large" style="background: #667eea; margin-right: 1rem;">
|
||||
🎯 Audit Gratuit (15 min)
|
||||
</a>
|
||||
<a href="tel:+2250123456789" class="btn btn-large btn-secondary">
|
||||
📞 Appeler maintenant
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="footer-content">
|
||||
<div>
|
||||
<h4>Lions Dev</h4>
|
||||
<p>Solutions ERP pour PME ivoiriennes</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4>Solutions</h4>
|
||||
<ul style="list-style: none; padding: 0;">
|
||||
<li><a href="#solutions" style="color: #ccc;">CRM Commercial</a></li>
|
||||
<li><a href="#solutions" style="color: #ccc;">Gestion Stocks</a></li>
|
||||
<li><a href="#solutions" style="color: #ccc;">Comptabilité</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4>Contact</h4>
|
||||
<p>📍 Abidjan, Côte d'Ivoire</p>
|
||||
<p>📞 +225 01 23 45 67 89</p>
|
||||
<p>✉️ contact@lionsdev.ci</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-bottom">
|
||||
<p>© 2024 Lions Dev. Tous droits réservés.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
569
src/main/resources/META-INF/resources/quote-configurator.html
Normal file
569
src/main/resources/META-INF/resources/quote-configurator.html
Normal file
@@ -0,0 +1,569 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Configurateur de Devis - Lions Dev</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<style>
|
||||
.configurator-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.configurator-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
padding: 30px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.modules-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.module-card {
|
||||
background: white;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.module-card:hover {
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.15);
|
||||
}
|
||||
|
||||
.module-card.selected {
|
||||
border-color: #667eea;
|
||||
background: #f0f4ff;
|
||||
}
|
||||
|
||||
.module-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.module-title {
|
||||
font-size: 1.3em;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.module-price {
|
||||
font-size: 1.1em;
|
||||
font-weight: bold;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.module-description {
|
||||
color: #666;
|
||||
margin-bottom: 15px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.module-features {
|
||||
font-size: 0.9em;
|
||||
color: #555;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.complexity-selector {
|
||||
margin-top: 15px;
|
||||
padding: 10px;
|
||||
background: #f9f9f9;
|
||||
border-radius: 8px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.module-card.selected .complexity-selector {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.complexity-options {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.complexity-option {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.complexity-option:hover {
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.complexity-option.selected {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.services-section {
|
||||
background: white;
|
||||
padding: 25px;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 30px;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.services-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.service-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.service-input {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.quote-summary {
|
||||
background: white;
|
||||
padding: 25px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e0e0e0;
|
||||
position: sticky;
|
||||
top: 20px;
|
||||
}
|
||||
|
||||
.summary-line {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.summary-line.total {
|
||||
border-top: 2px solid #667eea;
|
||||
padding-top: 15px;
|
||||
margin-top: 15px;
|
||||
font-weight: bold;
|
||||
font-size: 1.2em;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
flex: 1;
|
||||
padding: 12px 20px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #5a6fd8;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #f8f9fa;
|
||||
color: #333;
|
||||
border: 2px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.popular-badge {
|
||||
background: #ff6b6b;
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.8em;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.modules-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.services-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.complexity-options {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="configurator-container">
|
||||
<!-- En-tête -->
|
||||
<div class="configurator-header">
|
||||
<h1>🛠️ Configurateur de Devis Personnalisé</h1>
|
||||
<p>Sélectionnez les modules qui correspondent à vos besoins</p>
|
||||
<p><strong>Devis gratuit • Personnalisation complète • Support inclus</strong></p>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 2fr 1fr; gap: 30px;">
|
||||
<!-- Configuration des modules -->
|
||||
<div>
|
||||
<h2>📦 Modules Disponibles</h2>
|
||||
<div class="modules-grid" id="modulesGrid">
|
||||
<!-- Les modules seront chargés dynamiquement -->
|
||||
</div>
|
||||
|
||||
<!-- Services additionnels -->
|
||||
<div class="services-section">
|
||||
<h3>🎓 Services Additionnels</h3>
|
||||
<div class="services-grid">
|
||||
<div class="service-item">
|
||||
<label>Formation (heures):</label>
|
||||
<input type="number" id="formationHours" class="service-input"
|
||||
value="8" min="0" max="100" onchange="updateSummary()">
|
||||
<span>15 000 FCFA/h</span>
|
||||
</div>
|
||||
<div class="service-item">
|
||||
<label>Support (mois):</label>
|
||||
<input type="number" id="supportMonths" class="service-input"
|
||||
value="3" min="0" max="24" onchange="updateSummary()">
|
||||
<span>25 000 FCFA/mois</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Résumé du devis -->
|
||||
<div>
|
||||
<div class="quote-summary">
|
||||
<h3>💰 Résumé du Devis</h3>
|
||||
|
||||
<div id="selectedModules">
|
||||
<!-- Modules sélectionnés -->
|
||||
</div>
|
||||
|
||||
<div class="summary-line">
|
||||
<span>Sous-total modules:</span>
|
||||
<span id="subtotalModules">0 FCFA</span>
|
||||
</div>
|
||||
|
||||
<div class="summary-line">
|
||||
<span>Formation:</span>
|
||||
<span id="formationCost">120 000 FCFA</span>
|
||||
</div>
|
||||
|
||||
<div class="summary-line">
|
||||
<span>Support:</span>
|
||||
<span id="supportCost">75 000 FCFA</span>
|
||||
</div>
|
||||
|
||||
<div class="summary-line">
|
||||
<span>Total HT:</span>
|
||||
<span id="totalHT">0 FCFA</span>
|
||||
</div>
|
||||
|
||||
<div class="summary-line">
|
||||
<span>TVA (18%):</span>
|
||||
<span id="vatAmount">0 FCFA</span>
|
||||
</div>
|
||||
|
||||
<div class="summary-line total">
|
||||
<span>TOTAL TTC:</span>
|
||||
<span id="totalTTC">0 FCFA</span>
|
||||
</div>
|
||||
|
||||
<div class="action-buttons">
|
||||
<button class="btn btn-secondary" onclick="resetConfiguration()">
|
||||
🔄 Réinitialiser
|
||||
</button>
|
||||
<button class="btn btn-primary" onclick="generateQuote()">
|
||||
📄 Générer Devis
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 15px; text-align: center;">
|
||||
<small style="color: #666;">
|
||||
💡 Devis personnalisé gratuit<br>
|
||||
📞 Conseil expert inclus<br>
|
||||
⚡ Réponse sous 24h
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Variables globales
|
||||
let catalog = [];
|
||||
let selectedModules = new Map();
|
||||
let auditId = null;
|
||||
|
||||
// Initialisation
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Récupérer l'ID d'audit depuis l'URL si présent
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
auditId = urlParams.get('audit');
|
||||
|
||||
loadModuleCatalog();
|
||||
});
|
||||
|
||||
// Chargement du catalogue
|
||||
async function loadModuleCatalog() {
|
||||
try {
|
||||
const response = await fetch('/api/quotes/catalog');
|
||||
catalog = await response.json();
|
||||
renderModules();
|
||||
} catch (error) {
|
||||
console.error('Erreur chargement catalogue:', error);
|
||||
alert('Erreur lors du chargement du catalogue');
|
||||
}
|
||||
}
|
||||
|
||||
// Affichage des modules
|
||||
function renderModules() {
|
||||
const grid = document.getElementById('modulesGrid');
|
||||
grid.innerHTML = '';
|
||||
|
||||
catalog.forEach(module => {
|
||||
const moduleCard = createModuleCard(module);
|
||||
grid.appendChild(moduleCard);
|
||||
});
|
||||
|
||||
updateSummary();
|
||||
}
|
||||
|
||||
// Création d'une carte module
|
||||
function createModuleCard(module) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'module-card';
|
||||
card.onclick = () => toggleModule(module.moduleCode);
|
||||
|
||||
const popularBadge = module.popular ? '<span class="popular-badge">Populaire</span>' : '';
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="module-header">
|
||||
<div class="module-title">${module.moduleName}${popularBadge}</div>
|
||||
<div class="module-price">À partir de ${formatCurrency(module.basicPrice)}</div>
|
||||
</div>
|
||||
<div class="module-description">${module.description}</div>
|
||||
<div class="module-features">${module.features}</div>
|
||||
|
||||
<div class="complexity-selector">
|
||||
<label><strong>Niveau de complexité:</strong></label>
|
||||
<div class="complexity-options">
|
||||
<div class="complexity-option" data-level="BASIC" onclick="selectComplexity('${module.moduleCode}', 'BASIC', ${module.basicPrice}, event)">
|
||||
Basique<br><small>${formatCurrency(module.basicPrice)}</small>
|
||||
</div>
|
||||
<div class="complexity-option selected" data-level="STANDARD" onclick="selectComplexity('${module.moduleCode}', 'STANDARD', ${module.standardPrice}, event)">
|
||||
Standard<br><small>${formatCurrency(module.standardPrice)}</small>
|
||||
</div>
|
||||
<div class="complexity-option" data-level="ADVANCED" onclick="selectComplexity('${module.moduleCode}', 'ADVANCED', ${module.advancedPrice}, event)">
|
||||
Avancé<br><small>${formatCurrency(module.advancedPrice)}</small>
|
||||
</div>
|
||||
<div class="complexity-option" data-level="ENTERPRISE" onclick="selectComplexity('${module.moduleCode}', 'ENTERPRISE', ${module.enterprisePrice}, event)">
|
||||
Enterprise<br><small>${formatCurrency(module.enterprisePrice)}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
// Sélection/désélection d'un module
|
||||
function toggleModule(moduleCode) {
|
||||
const card = event.currentTarget;
|
||||
|
||||
if (selectedModules.has(moduleCode)) {
|
||||
// Désélectionner
|
||||
selectedModules.delete(moduleCode);
|
||||
card.classList.remove('selected');
|
||||
} else {
|
||||
// Sélectionner avec niveau standard par défaut
|
||||
const module = catalog.find(m => m.moduleCode === moduleCode);
|
||||
selectedModules.set(moduleCode, {
|
||||
module: module,
|
||||
complexity: 'STANDARD',
|
||||
price: module.standardPrice
|
||||
});
|
||||
card.classList.add('selected');
|
||||
}
|
||||
|
||||
updateSummary();
|
||||
}
|
||||
|
||||
// Sélection du niveau de complexité
|
||||
function selectComplexity(moduleCode, level, price, event) {
|
||||
event.stopPropagation();
|
||||
|
||||
// Mise à jour visuelle
|
||||
const card = event.target.closest('.module-card');
|
||||
card.querySelectorAll('.complexity-option').forEach(opt => opt.classList.remove('selected'));
|
||||
event.target.classList.add('selected');
|
||||
|
||||
// Mise à jour des données
|
||||
if (selectedModules.has(moduleCode)) {
|
||||
const selection = selectedModules.get(moduleCode);
|
||||
selection.complexity = level;
|
||||
selection.price = price;
|
||||
updateSummary();
|
||||
}
|
||||
}
|
||||
|
||||
// Mise à jour du résumé
|
||||
function updateSummary() {
|
||||
const selectedDiv = document.getElementById('selectedModules');
|
||||
selectedDiv.innerHTML = '';
|
||||
|
||||
let subtotalModules = 0;
|
||||
|
||||
// Affichage des modules sélectionnés
|
||||
selectedModules.forEach((selection, moduleCode) => {
|
||||
const moduleDiv = document.createElement('div');
|
||||
moduleDiv.className = 'summary-line';
|
||||
moduleDiv.innerHTML = `
|
||||
<span>${selection.module.moduleName} (${selection.complexity})</span>
|
||||
<span>${formatCurrency(selection.price)}</span>
|
||||
`;
|
||||
selectedDiv.appendChild(moduleDiv);
|
||||
subtotalModules += selection.price;
|
||||
});
|
||||
|
||||
// Calculs
|
||||
const formationHours = parseFloat(document.getElementById('formationHours').value) || 0;
|
||||
const supportMonths = parseFloat(document.getElementById('supportMonths').value) || 0;
|
||||
|
||||
const formationCost = formationHours * 15000;
|
||||
const supportCost = supportMonths * 25000;
|
||||
const totalHT = subtotalModules + formationCost + supportCost;
|
||||
const vatAmount = totalHT * 0.18;
|
||||
const totalTTC = totalHT + vatAmount;
|
||||
|
||||
// Mise à jour de l'affichage
|
||||
document.getElementById('subtotalModules').textContent = formatCurrency(subtotalModules);
|
||||
document.getElementById('formationCost').textContent = formatCurrency(formationCost);
|
||||
document.getElementById('supportCost').textContent = formatCurrency(supportCost);
|
||||
document.getElementById('totalHT').textContent = formatCurrency(totalHT);
|
||||
document.getElementById('vatAmount').textContent = formatCurrency(vatAmount);
|
||||
document.getElementById('totalTTC').textContent = formatCurrency(totalTTC);
|
||||
}
|
||||
|
||||
// Réinitialisation
|
||||
function resetConfiguration() {
|
||||
selectedModules.clear();
|
||||
document.querySelectorAll('.module-card').forEach(card => {
|
||||
card.classList.remove('selected');
|
||||
});
|
||||
document.getElementById('formationHours').value = 8;
|
||||
document.getElementById('supportMonths').value = 3;
|
||||
updateSummary();
|
||||
}
|
||||
|
||||
// Génération du devis
|
||||
async function generateQuote() {
|
||||
if (selectedModules.size === 0) {
|
||||
alert('Veuillez sélectionner au moins un module');
|
||||
return;
|
||||
}
|
||||
|
||||
// Collecte des données
|
||||
const modules = Array.from(selectedModules.values()).map(selection => ({
|
||||
moduleCode: selection.module.moduleCode,
|
||||
moduleName: selection.module.moduleName,
|
||||
description: selection.module.description,
|
||||
unitPrice: selection.price,
|
||||
quantity: 1,
|
||||
complexity: selection.complexity
|
||||
}));
|
||||
|
||||
const customization = {
|
||||
modules: modules,
|
||||
formationHours: parseFloat(document.getElementById('formationHours').value) || 0,
|
||||
supportMonths: parseFloat(document.getElementById('supportMonths').value) || 0
|
||||
};
|
||||
|
||||
try {
|
||||
let response;
|
||||
|
||||
if (auditId) {
|
||||
// Génération depuis un audit
|
||||
response = await fetch(`/api/quotes/generate/${auditId}`, {
|
||||
method: 'POST'
|
||||
});
|
||||
} else {
|
||||
// Redirection vers formulaire client
|
||||
const params = new URLSearchParams({
|
||||
config: JSON.stringify(customization)
|
||||
});
|
||||
window.location.href = `/quote-form.html?${params}`;
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
const quote = await response.json();
|
||||
|
||||
// Personnalisation du devis
|
||||
const customizeResponse = await fetch(`/api/quotes/${quote.id}/customize`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(customization)
|
||||
});
|
||||
|
||||
if (customizeResponse.ok) {
|
||||
alert('Devis généré avec succès !');
|
||||
window.location.href = `/quote-view.html?id=${quote.id}`;
|
||||
}
|
||||
} else {
|
||||
alert('Erreur lors de la génération du devis');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur:', error);
|
||||
alert('Erreur lors de la génération du devis');
|
||||
}
|
||||
}
|
||||
|
||||
// Formatage des montants
|
||||
function formatCurrency(amount) {
|
||||
return new Intl.NumberFormat('fr-FR').format(amount) + ' FCFA';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
558
src/main/resources/META-INF/resources/roi-calculator.html
Normal file
558
src/main/resources/META-INF/resources/roi-calculator.html
Normal file
@@ -0,0 +1,558 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Calculateur ROI - Lions Dev</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<style>
|
||||
.roi-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.roi-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
padding: 30px;
|
||||
background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%);
|
||||
color: white;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.roi-form {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.1);
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select {
|
||||
padding: 12px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group select:focus {
|
||||
outline: none;
|
||||
border-color: #4CAF50;
|
||||
}
|
||||
|
||||
.modules-selection {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.modules-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.module-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 15px;
|
||||
background: #f9f9f9;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.module-checkbox:hover {
|
||||
border-color: #4CAF50;
|
||||
background: #f0fff0;
|
||||
}
|
||||
|
||||
.module-checkbox.selected {
|
||||
border-color: #4CAF50;
|
||||
background: #e8f5e8;
|
||||
}
|
||||
|
||||
.module-checkbox input {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.calculate-btn {
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.calculate-btn:hover {
|
||||
background: #45a049;
|
||||
}
|
||||
|
||||
.results-container {
|
||||
display: none;
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.roi-metrics {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #4CAF50;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 2em;
|
||||
font-weight: bold;
|
||||
color: #4CAF50;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
color: #666;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.gains-breakdown {
|
||||
margin: 30px 0;
|
||||
}
|
||||
|
||||
.gains-chart {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.gain-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #f9f9f9;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.gain-label {
|
||||
min-width: 150px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.gain-progress {
|
||||
flex: 1;
|
||||
height: 20px;
|
||||
background: #e0e0e0;
|
||||
border-radius: 10px;
|
||||
margin: 0 15px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.gain-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #4CAF50, #45a049);
|
||||
border-radius: 10px;
|
||||
transition: width 0.5s ease;
|
||||
}
|
||||
|
||||
.gain-amount {
|
||||
font-weight: bold;
|
||||
color: #4CAF50;
|
||||
min-width: 120px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.scenarios-section {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.scenarios-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 20px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.scenario-card {
|
||||
padding: 20px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.scenario-card.best {
|
||||
border-color: #4CAF50;
|
||||
background: #f0fff0;
|
||||
}
|
||||
|
||||
.recommendations {
|
||||
background: #e3f2fd;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.recommendations h3 {
|
||||
color: #1976d2;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.recommendations pre {
|
||||
white-space: pre-wrap;
|
||||
font-family: inherit;
|
||||
margin: 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.roi-metrics {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.scenarios-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="roi-container">
|
||||
<!-- En-tête -->
|
||||
<div class="roi-header">
|
||||
<h1>📊 Calculateur de Retour sur Investissement</h1>
|
||||
<p>Découvrez les bénéfices financiers de votre digitalisation</p>
|
||||
<p><strong>Calcul personnalisé • Scénarios multiples • Recommandations d'experts</strong></p>
|
||||
</div>
|
||||
|
||||
<!-- Formulaire -->
|
||||
<div class="roi-form">
|
||||
<h2>🏢 Informations sur votre entreprise</h2>
|
||||
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label for="sector">Secteur d'activité</label>
|
||||
<select id="sector" onchange="loadDefaults()">
|
||||
<option value="">Sélectionnez...</option>
|
||||
<option value="commerce">Commerce/Distribution</option>
|
||||
<option value="services">Services</option>
|
||||
<option value="industrie">Industrie/Manufacturing</option>
|
||||
<option value="agriculture">Agriculture/Agro-alimentaire</option>
|
||||
<option value="btp">BTP/Construction</option>
|
||||
<option value="transport">Transport/Logistique</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="employeeCount">Nombre d'employés</label>
|
||||
<input type="number" id="employeeCount" min="1" max="1000" value="10">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="averageSalary">Salaire moyen annuel (FCFA)</label>
|
||||
<input type="number" id="averageSalary" min="1000000" max="10000000" value="2400000" step="100000">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="turnover">Chiffre d'affaires annuel (FCFA)</label>
|
||||
<input type="number" id="turnover" min="1000000" max="1000000000" value="50000000" step="1000000">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="investmentAmount">Montant d'investissement (FCFA)</label>
|
||||
<input type="number" id="investmentAmount" min="100000" max="10000000" value="500000" step="50000">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modules-selection">
|
||||
<h3>📦 Modules à implémenter</h3>
|
||||
<div class="modules-grid">
|
||||
<div class="module-checkbox" onclick="toggleModule('CRM')">
|
||||
<input type="checkbox" id="module-CRM" value="CRM">
|
||||
<label for="module-CRM">💼 CRM Commercial</label>
|
||||
</div>
|
||||
<div class="module-checkbox" onclick="toggleModule('STOCK')">
|
||||
<input type="checkbox" id="module-STOCK" value="STOCK">
|
||||
<label for="module-STOCK">📦 Gestion Stock</label>
|
||||
</div>
|
||||
<div class="module-checkbox" onclick="toggleModule('COMPTA')">
|
||||
<input type="checkbox" id="module-COMPTA" value="COMPTA">
|
||||
<label for="module-COMPTA">💰 Comptabilité</label>
|
||||
</div>
|
||||
<div class="module-checkbox" onclick="toggleModule('RH')">
|
||||
<input type="checkbox" id="module-RH" value="RH">
|
||||
<label for="module-RH">👥 Ressources Humaines</label>
|
||||
</div>
|
||||
<div class="module-checkbox" onclick="toggleModule('INFRA')">
|
||||
<input type="checkbox" id="module-INFRA" value="INFRA">
|
||||
<label for="module-INFRA">🖥️ Infrastructure IT</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="calculate-btn" onclick="calculateROI()">
|
||||
🚀 Calculer le ROI
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Résultats -->
|
||||
<div class="results-container" id="resultsContainer">
|
||||
<h2>📈 Résultats du Calcul ROI</h2>
|
||||
|
||||
<!-- Métriques principales -->
|
||||
<div class="roi-metrics">
|
||||
<div class="metric-card">
|
||||
<div class="metric-value" id="roiPercentage">0%</div>
|
||||
<div class="metric-label">ROI sur 3 ans</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-value" id="paybackPeriod">0</div>
|
||||
<div class="metric-label">Retour investissement (mois)</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-value" id="annualGains">0</div>
|
||||
<div class="metric-label">Gains annuels (FCFA)</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-value" id="netBenefit">0</div>
|
||||
<div class="metric-label">Bénéfice net 3 ans (FCFA)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Répartition des gains -->
|
||||
<div class="gains-breakdown">
|
||||
<h3>💡 Répartition des Gains Annuels</h3>
|
||||
<div class="gains-chart" id="gainsChart">
|
||||
<!-- Généré dynamiquement -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scénarios -->
|
||||
<div class="scenarios-section">
|
||||
<h3>🎯 Analyse par Scénarios</h3>
|
||||
<div class="scenarios-grid" id="scenariosGrid">
|
||||
<!-- Généré dynamiquement -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recommandations -->
|
||||
<div class="recommendations" id="recommendationsSection">
|
||||
<h3>🎯 Recommandations Personnalisées</h3>
|
||||
<pre id="recommendationsText"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Variables globales
|
||||
let selectedModules = new Set();
|
||||
|
||||
// Chargement des valeurs par défaut selon le secteur
|
||||
async function loadDefaults() {
|
||||
const sector = document.getElementById('sector').value;
|
||||
if (!sector) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/roi/defaults/${sector}`);
|
||||
const defaults = await response.json();
|
||||
|
||||
document.getElementById('averageSalary').value = defaults.averageSalary;
|
||||
|
||||
// Sélection des modules recommandés
|
||||
selectedModules.clear();
|
||||
document.querySelectorAll('.module-checkbox').forEach(cb => cb.classList.remove('selected'));
|
||||
document.querySelectorAll('input[type="checkbox"]').forEach(cb => cb.checked = false);
|
||||
|
||||
defaults.selectedModules.forEach(moduleCode => {
|
||||
toggleModule(moduleCode, true);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Erreur chargement defaults:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Sélection/désélection des modules
|
||||
function toggleModule(moduleCode, force = false) {
|
||||
const checkbox = document.getElementById(`module-${moduleCode}`);
|
||||
const container = checkbox.closest('.module-checkbox');
|
||||
|
||||
if (force || !selectedModules.has(moduleCode)) {
|
||||
selectedModules.add(moduleCode);
|
||||
container.classList.add('selected');
|
||||
checkbox.checked = true;
|
||||
} else {
|
||||
selectedModules.delete(moduleCode);
|
||||
container.classList.remove('selected');
|
||||
checkbox.checked = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Calcul du ROI
|
||||
async function calculateROI() {
|
||||
if (selectedModules.size === 0) {
|
||||
alert('Veuillez sélectionner au moins un module');
|
||||
return;
|
||||
}
|
||||
|
||||
const input = {
|
||||
employeeCount: parseInt(document.getElementById('employeeCount').value),
|
||||
averageSalary: parseFloat(document.getElementById('averageSalary').value),
|
||||
turnover: parseFloat(document.getElementById('turnover').value),
|
||||
investmentAmount: parseFloat(document.getElementById('investmentAmount').value),
|
||||
selectedModules: Array.from(selectedModules),
|
||||
sector: document.getElementById('sector').value
|
||||
};
|
||||
|
||||
try {
|
||||
// Calcul du ROI principal
|
||||
const response = await fetch('/api/roi/calculate', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(input)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Calcul des scénarios
|
||||
const scenariosResponse = await fetch('/api/roi/scenarios', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(input)
|
||||
});
|
||||
|
||||
const scenarios = await scenariosResponse.json();
|
||||
|
||||
// Affichage des résultats
|
||||
displayResults(data.result, data.recommendations, scenarios);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur calcul ROI:', error);
|
||||
alert('Erreur lors du calcul du ROI');
|
||||
}
|
||||
}
|
||||
|
||||
// Affichage des résultats
|
||||
function displayResults(result, recommendations, scenarios) {
|
||||
// Affichage du conteneur
|
||||
document.getElementById('resultsContainer').style.display = 'block';
|
||||
|
||||
// Métriques principales
|
||||
document.getElementById('roiPercentage').textContent = Math.round(result.roi3Years) + '%';
|
||||
document.getElementById('paybackPeriod').textContent = Math.round(result.paybackPeriodMonths);
|
||||
document.getElementById('annualGains').textContent = formatCurrency(result.totalAnnualGains);
|
||||
document.getElementById('netBenefit').textContent = formatCurrency(result.netPresentValue3Years);
|
||||
|
||||
// Répartition des gains
|
||||
displayGainsBreakdown(result.gainsByCategory);
|
||||
|
||||
// Scénarios
|
||||
displayScenarios(scenarios);
|
||||
|
||||
// Recommandations
|
||||
document.getElementById('recommendationsText').textContent = recommendations;
|
||||
|
||||
// Scroll vers les résultats
|
||||
document.getElementById('resultsContainer').scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
|
||||
// Affichage de la répartition des gains
|
||||
function displayGainsBreakdown(gainsByCategory) {
|
||||
const chart = document.getElementById('gainsChart');
|
||||
chart.innerHTML = '';
|
||||
|
||||
const maxGain = Math.max(...Object.values(gainsByCategory));
|
||||
|
||||
Object.entries(gainsByCategory).forEach(([category, amount]) => {
|
||||
const percentage = (amount / maxGain) * 100;
|
||||
|
||||
const bar = document.createElement('div');
|
||||
bar.className = 'gain-bar';
|
||||
bar.innerHTML = `
|
||||
<div class="gain-label">${category}</div>
|
||||
<div class="gain-progress">
|
||||
<div class="gain-fill" style="width: ${percentage}%"></div>
|
||||
</div>
|
||||
<div class="gain-amount">${formatCurrency(amount)}</div>
|
||||
`;
|
||||
|
||||
chart.appendChild(bar);
|
||||
});
|
||||
}
|
||||
|
||||
// Affichage des scénarios
|
||||
function displayScenarios(scenarios) {
|
||||
const grid = document.getElementById('scenariosGrid');
|
||||
grid.innerHTML = '';
|
||||
|
||||
Object.entries(scenarios).forEach(([name, result]) => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'scenario-card';
|
||||
if (name === 'Réaliste') card.classList.add('best');
|
||||
|
||||
card.innerHTML = `
|
||||
<h4>${name}</h4>
|
||||
<div style="font-size: 1.5em; font-weight: bold; color: #4CAF50; margin: 10px 0;">
|
||||
${Math.round(result.roi3Years)}%
|
||||
</div>
|
||||
<div style="font-size: 0.9em; color: #666;">
|
||||
ROI sur 3 ans
|
||||
</div>
|
||||
<div style="margin-top: 10px; font-size: 0.8em;">
|
||||
Retour: ${Math.round(result.paybackPeriodMonths)} mois<br>
|
||||
Gains: ${formatCurrency(result.totalAnnualGains)}/an
|
||||
</div>
|
||||
`;
|
||||
|
||||
grid.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
// Formatage des montants
|
||||
function formatCurrency(amount) {
|
||||
return new Intl.NumberFormat('fr-FR').format(Math.round(amount)) + ' FCFA';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -24,7 +24,7 @@ app.default-language=fr
|
||||
# Configuration proxy et CORS
|
||||
quarkus.http.proxy.proxy-address-forwarding=true
|
||||
quarkus.http.proxy.allow-forwarded=true
|
||||
quarkus.http.cors=true
|
||||
quarkus.http.cors.enabled=true
|
||||
quarkus.http.cors.origins=${CORS_ORIGINS:http://localhost:8707}
|
||||
quarkus.http.cors.methods=GET,POST,PUT,DELETE,OPTIONS
|
||||
quarkus.http.cors.headers=Content-Type,Authorization
|
||||
@@ -88,7 +88,7 @@ quarkus.hibernate-orm.physical-naming-strategy=org.hibernate.boot.model.naming.C
|
||||
%prod.jakarta.faces.PROJECT_STAGE=Production
|
||||
%production.jakarta.faces.PROJECT_STAGE=Production
|
||||
|
||||
# Chemins d'acc<EFBFBD>s JSF
|
||||
# Chemins d'accès JSF
|
||||
#quarkus.servlet.context-path=/lions-dev
|
||||
quarkus.http.non-application-root-path=/q
|
||||
|
||||
@@ -188,7 +188,7 @@ app.admin.email=${ADMIN_EMAIL:admin@lions.dev}
|
||||
#==========================================================
|
||||
# M<>triques et documentation API
|
||||
%prod.quarkus.micrometer.export.prometheus.enabled=true
|
||||
quarkus.swagger-ui.enable=true
|
||||
quarkus.swagger-ui.enabled=true
|
||||
quarkus.swagger-ui.always-include=true
|
||||
quarkus.smallrye-openapi.info-title=Lions Dev API
|
||||
quarkus.smallrye-openapi.info-version=${app.version}
|
||||
|
||||
99
src/test/java/dev/lions/audit/AuditServiceTest.java
Normal file
99
src/test/java/dev/lions/audit/AuditServiceTest.java
Normal file
@@ -0,0 +1,99 @@
|
||||
package dev.lions.audit;
|
||||
|
||||
import io.quarkus.test.junit.QuarkusTest;
|
||||
import jakarta.inject.Inject;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.HashMap;
|
||||
|
||||
@QuarkusTest
|
||||
public class AuditServiceTest {
|
||||
|
||||
@Inject
|
||||
AuditService auditService;
|
||||
|
||||
private AuditResponse sampleResponse;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
sampleResponse = new AuditResponse();
|
||||
sampleResponse.setCompanyName("Test Company");
|
||||
sampleResponse.setContactName("John Doe");
|
||||
sampleResponse.setEmail("test@company.com");
|
||||
sampleResponse.setPhone("0123456789");
|
||||
sampleResponse.setEmployeeCount(25);
|
||||
sampleResponse.setTurnover("500000000");
|
||||
sampleResponse.setSector("Commerce");
|
||||
|
||||
// Réponses aux questions (Map questionId -> answer index)
|
||||
Map<Long, Integer> answers = new HashMap<>();
|
||||
for (long i = 1; i <= 16; i++) {
|
||||
answers.put(i, (int) (2 + (i % 4))); // Scores entre 2 et 5
|
||||
}
|
||||
sampleResponse.setAnswers(answers);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProcessAuditResponse() {
|
||||
AuditResponse result = auditService.processAuditResponse(sampleResponse);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals("Test Company", result.getCompanyName());
|
||||
assertEquals("test@company.com", result.getEmail());
|
||||
assertNotNull(result.getTotalScore());
|
||||
assertTrue(result.getTotalScore() > 0);
|
||||
assertNotNull(result.getMaturityPercentage());
|
||||
assertTrue(result.getMaturityPercentage() > 0);
|
||||
assertTrue(result.getMaturityPercentage() <= 100);
|
||||
assertNotNull(result.getCategoryScores());
|
||||
assertFalse(result.getCategoryScores().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBasicFunctionality() {
|
||||
// Test que le service peut traiter une réponse d'audit basique
|
||||
AuditResponse result = auditService.processAuditResponse(sampleResponse);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals("Test Company", result.getCompanyName());
|
||||
assertEquals("test@company.com", result.getEmail());
|
||||
|
||||
// Vérifier que les scores ont été calculés
|
||||
assertNotNull(result.getTotalScore());
|
||||
assertNotNull(result.getMaturityPercentage());
|
||||
assertNotNull(result.getCategoryScores());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testScoreCalculation() {
|
||||
AuditResponse result = auditService.processAuditResponse(sampleResponse);
|
||||
|
||||
// Vérifier que les scores sont dans des plages valides
|
||||
assertTrue(result.getTotalScore() > 0);
|
||||
assertTrue(result.getMaturityPercentage() >= 0);
|
||||
assertTrue(result.getMaturityPercentage() <= 100);
|
||||
|
||||
// Vérifier que les scores de catégorie existent
|
||||
assertNotNull(result.getCategoryScores());
|
||||
assertFalse(result.getCategoryScores().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testInputValidation() {
|
||||
// Test avec nom d'entreprise null
|
||||
sampleResponse.setCompanyName(null);
|
||||
assertThrows(Exception.class, () -> {
|
||||
auditService.processAuditResponse(sampleResponse);
|
||||
});
|
||||
|
||||
// Test avec email null
|
||||
sampleResponse.setCompanyName("Test Company");
|
||||
sampleResponse.setEmail(null);
|
||||
assertThrows(Exception.class, () -> {
|
||||
auditService.processAuditResponse(sampleResponse);
|
||||
});
|
||||
}
|
||||
}
|
||||
124
src/test/java/dev/lions/compliance/IvorianTaxServiceTest.java
Normal file
124
src/test/java/dev/lions/compliance/IvorianTaxServiceTest.java
Normal file
@@ -0,0 +1,124 @@
|
||||
package dev.lions.compliance;
|
||||
|
||||
import io.quarkus.test.junit.QuarkusTest;
|
||||
import jakarta.inject.Inject;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
|
||||
|
||||
@QuarkusTest
|
||||
public class IvorianTaxServiceTest {
|
||||
|
||||
@Inject
|
||||
IvorianTaxService taxService;
|
||||
|
||||
@Test
|
||||
void testCalculateTVAStandard() {
|
||||
double amountHT = 1000000.0; // 1M FCFA
|
||||
TaxCalculation result = taxService.calculateTVA(amountHT, false);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(amountHT, result.getAmountHT());
|
||||
assertEquals(18.0, result.getTaxRate()); // Taux standard 18%
|
||||
assertEquals(180000.0, result.getTaxAmount(), 0.01); // 18% de 1M
|
||||
assertEquals(1180000.0, result.getAmountTTC(), 0.01); // HT + TVA
|
||||
assertEquals("TVA", result.getTaxType());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCalculateTVAReduced() {
|
||||
double amountHT = 1000000.0; // 1M FCFA
|
||||
TaxCalculation result = taxService.calculateTVA(amountHT, true);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(amountHT, result.getAmountHT());
|
||||
assertEquals(9.0, result.getTaxRate()); // Taux réduit 9%
|
||||
assertEquals(90000.0, result.getTaxAmount(), 0.01); // 9% de 1M
|
||||
assertEquals(1090000.0, result.getAmountTTC(), 0.01); // HT + TVA
|
||||
assertEquals("TVA", result.getTaxType());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCalculateISStandard() {
|
||||
double profit = 10000000.0; // 10M FCFA
|
||||
TaxCalculation result = taxService.calculateIS(profit, false);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(25.0, result.getTaxRate()); // Taux standard 25%
|
||||
assertEquals(2500000.0, result.getTaxAmount(), 0.01); // 25% de 10M
|
||||
assertEquals("IS", result.getTaxType());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCalculateISPME() {
|
||||
double profit = 10000000.0; // 10M FCFA
|
||||
TaxCalculation result = taxService.calculateIS(profit, true);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(20.0, result.getTaxRate()); // Taux PME 20%
|
||||
assertEquals(2000000.0, result.getTaxAmount(), 0.01); // 20% de 10M
|
||||
assertEquals("IS", result.getTaxType());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testTaxRateConstants() {
|
||||
// Vérifier les constantes de taux
|
||||
assertEquals(18.0, IvorianTaxService.TVA_RATE);
|
||||
assertEquals(9.0, IvorianTaxService.TVA_REDUCED_RATE);
|
||||
assertEquals(25.0, IvorianTaxService.IS_RATE);
|
||||
assertEquals(20.0, IvorianTaxService.IS_REDUCED_RATE);
|
||||
assertEquals(200_000_000.0, IvorianTaxService.PME_TURNOVER_THRESHOLD);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testZeroAmounts() {
|
||||
// Test avec montant zéro
|
||||
TaxCalculation tvaResult = taxService.calculateTVA(0.0, false);
|
||||
assertEquals(0.0, tvaResult.getTaxAmount());
|
||||
assertEquals(0.0, tvaResult.getAmountTTC());
|
||||
|
||||
TaxCalculation isResult = taxService.calculateIS(0.0, false);
|
||||
assertEquals(0.0, isResult.getTaxAmount());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLargeAmounts() {
|
||||
// Test avec gros montants
|
||||
double largeAmount = 1_000_000_000.0; // 1 milliard FCFA
|
||||
|
||||
TaxCalculation tvaResult = taxService.calculateTVA(largeAmount, false);
|
||||
assertEquals(180_000_000.0, tvaResult.getTaxAmount(), 0.01); // 18% de 1B
|
||||
|
||||
TaxCalculation isResult = taxService.calculateIS(largeAmount, false);
|
||||
assertEquals(250_000_000.0, isResult.getTaxAmount(), 0.01); // 25% de 1B
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNegativeAmounts() {
|
||||
// Les montants négatifs devraient être traités correctement
|
||||
TaxCalculation tvaResult = taxService.calculateTVA(-1000000.0, false);
|
||||
assertTrue(tvaResult.getTaxAmount() <= 0); // TVA négative ou nulle
|
||||
|
||||
TaxCalculation isResult = taxService.calculateIS(-1000000.0, false);
|
||||
assertTrue(isResult.getTaxAmount() <= 0); // IS négative ou nulle
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPMEThreshold() {
|
||||
// Vérifier le seuil PME
|
||||
double threshold = IvorianTaxService.PME_TURNOVER_THRESHOLD;
|
||||
assertEquals(200_000_000.0, threshold); // 200M FCFA
|
||||
|
||||
// Test avec montant juste en dessous du seuil PME
|
||||
double belowThreshold = threshold - 1000000.0;
|
||||
TaxCalculation pmeResult = taxService.calculateIS(belowThreshold * 0.1, true); // 10% de marge
|
||||
assertEquals(20.0, pmeResult.getTaxRate()); // Taux PME
|
||||
|
||||
// Test avec montant au-dessus du seuil
|
||||
double aboveThreshold = threshold + 1000000.0;
|
||||
TaxCalculation standardResult = taxService.calculateIS(aboveThreshold * 0.1, false); // 10% de marge
|
||||
assertEquals(25.0, standardResult.getTaxRate()); // Taux standard
|
||||
}
|
||||
}
|
||||
169
src/test/java/dev/lions/integration/AuditIntegrationTest.java
Normal file
169
src/test/java/dev/lions/integration/AuditIntegrationTest.java
Normal file
@@ -0,0 +1,169 @@
|
||||
package dev.lions.integration;
|
||||
|
||||
import io.quarkus.test.junit.QuarkusTest;
|
||||
import io.restassured.http.ContentType;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static io.restassured.RestAssured.given;
|
||||
import static org.hamcrest.CoreMatchers.*;
|
||||
import static org.hamcrest.Matchers.greaterThan;
|
||||
|
||||
@QuarkusTest
|
||||
public class AuditIntegrationTest {
|
||||
|
||||
@Test
|
||||
public void testGetAuditQuestions() {
|
||||
// Endpoint retourne Map<String, List<AuditQuestion>> (catégorie → questions),
|
||||
// pas une liste plate. Donc on vérifie qu'il y a au moins une catégorie
|
||||
// et que chaque question dans la 1ère catégorie a les champs attendus.
|
||||
given()
|
||||
.when().get("/api/audit/questions")
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.contentType(ContentType.JSON)
|
||||
.body("size()", greaterThan(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSubmitAudit() {
|
||||
String auditSubmission = """
|
||||
{
|
||||
"companyName": "Test Company",
|
||||
"email": "test@company.com",
|
||||
"phone": "0123456789",
|
||||
"employeeCount": 25,
|
||||
"annualRevenue": 500000000,
|
||||
"sector": "Commerce",
|
||||
"responses": [3, 4, 2, 3, 4, 3, 2, 3, 4, 2, 3, 2, 3, 4, 2, 3]
|
||||
}
|
||||
""";
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body(auditSubmission)
|
||||
.when().post("/api/audit/submit")
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.contentType(ContentType.JSON)
|
||||
.body("companyName", equalTo("Test Company"))
|
||||
.body("email", equalTo("test@company.com"))
|
||||
.body("overallMaturity", notNullValue())
|
||||
.body("categoryScores", notNullValue())
|
||||
.body("recommendations", notNullValue())
|
||||
.body("maturityLevel", notNullValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSubmitAuditInvalidData() {
|
||||
String invalidSubmission = """
|
||||
{
|
||||
"companyName": "",
|
||||
"email": "invalid-email",
|
||||
"responses": []
|
||||
}
|
||||
""";
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body(invalidSubmission)
|
||||
.when().post("/api/audit/submit")
|
||||
.then()
|
||||
.statusCode(400);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGenerateAuditReport() {
|
||||
String auditSubmission = """
|
||||
{
|
||||
"companyName": "Test Company",
|
||||
"email": "test@company.com",
|
||||
"phone": "0123456789",
|
||||
"employeeCount": 25,
|
||||
"annualRevenue": 500000000,
|
||||
"sector": "Commerce",
|
||||
"responses": [3, 4, 2, 3, 4, 3, 2, 3, 4, 2, 3, 2, 3, 4, 2, 3]
|
||||
}
|
||||
""";
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body(auditSubmission)
|
||||
.when().post("/api/audit/report")
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.contentType("application/pdf");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetQuoteFromAudit() {
|
||||
String auditSubmission = """
|
||||
{
|
||||
"companyName": "Test Company",
|
||||
"email": "test@company.com",
|
||||
"phone": "0123456789",
|
||||
"employeeCount": 25,
|
||||
"annualRevenue": 500000000,
|
||||
"sector": "Commerce",
|
||||
"responses": [3, 4, 2, 3, 4, 3, 2, 3, 4, 2, 3, 2, 3, 4, 2, 3]
|
||||
}
|
||||
""";
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body(auditSubmission)
|
||||
.when().post("/api/quotes/from-audit")
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.contentType(ContentType.JSON)
|
||||
.body("companyName", equalTo("Test Company"))
|
||||
.body("modules", notNullValue())
|
||||
.body("totalPrice", notNullValue())
|
||||
.body("implementationDays", notNullValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testROICalculation() {
|
||||
String roiRequest = """
|
||||
{
|
||||
"annualRevenue": 500000000,
|
||||
"employeeCount": 25,
|
||||
"currentMaturity": 45.0,
|
||||
"targetMaturity": 85.0,
|
||||
"investmentAmount": 2000000.0,
|
||||
"scenario": "realistic"
|
||||
}
|
||||
""";
|
||||
|
||||
given()
|
||||
.contentType(ContentType.JSON)
|
||||
.body(roiRequest)
|
||||
.when().post("/api/roi/calculate")
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.contentType(ContentType.JSON)
|
||||
.body("annualSavings", notNullValue())
|
||||
.body("roiPercentage", notNullValue())
|
||||
.body("paybackMonths", notNullValue())
|
||||
.body("breakdownByCategory", notNullValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHealthCheck() {
|
||||
given()
|
||||
.when().get("/q/health")
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.body("status", equalTo("UP"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOpenAPISpec() {
|
||||
given()
|
||||
.when().get("/q/openapi")
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.contentType("application/yaml")
|
||||
.body(containsString("openapi"))
|
||||
.body(containsString("info"));
|
||||
}
|
||||
}
|
||||
170
src/test/java/dev/lions/roi/ROICalculatorTest.java
Normal file
170
src/test/java/dev/lions/roi/ROICalculatorTest.java
Normal file
@@ -0,0 +1,170 @@
|
||||
package dev.lions.roi;
|
||||
|
||||
import io.quarkus.test.junit.QuarkusTest;
|
||||
import jakarta.inject.Inject;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
|
||||
|
||||
@QuarkusTest
|
||||
public class ROICalculatorTest {
|
||||
|
||||
@Inject
|
||||
ROICalculator roiCalculator;
|
||||
|
||||
private ROIInput sampleRequest;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
sampleRequest = new ROIInput();
|
||||
sampleRequest.setTurnover(500000000.0); // 500M FCFA
|
||||
sampleRequest.setEmployeeCount(25);
|
||||
sampleRequest.setAverageSalary(3000000.0); // 3M FCFA par an
|
||||
sampleRequest.setInvestmentAmount(2000000.0); // 2M FCFA
|
||||
sampleRequest.setSelectedModules(java.util.Arrays.asList("CRM", "STOCK", "COMPTA"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCalculateROI() {
|
||||
ROIResult result = roiCalculator.calculateROI(sampleRequest);
|
||||
|
||||
assertNotNull(result);
|
||||
assertTrue(result.getTotalAnnualGains() > 0);
|
||||
assertTrue(result.getAnnualProductivityGains() > 0);
|
||||
assertTrue(result.getAnnualErrorReduction() > 0);
|
||||
assertTrue(result.getAnnualTimeSavings() > 0);
|
||||
assertTrue(result.getAnnualComplianceGains() > 0);
|
||||
assertTrue(result.getPaybackPeriodMonths() > 0);
|
||||
assertNotNull(result.getRoi3Years());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBasicCalculation() {
|
||||
ROIResult result = roiCalculator.calculateROI(sampleRequest);
|
||||
|
||||
assertNotNull(result);
|
||||
assertTrue(result.getTotalAnnualGains() > 0);
|
||||
assertTrue(result.getRoi3Years() != 0); // Peut être négatif ou positif
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDifferentInvestmentAmounts() {
|
||||
// Test avec investissement faible
|
||||
sampleRequest.setInvestmentAmount(1000000.0); // 1M FCFA
|
||||
ROIResult lowInvestment = roiCalculator.calculateROI(sampleRequest);
|
||||
|
||||
// Test avec investissement élevé
|
||||
sampleRequest.setInvestmentAmount(5000000.0); // 5M FCFA
|
||||
ROIResult highInvestment = roiCalculator.calculateROI(sampleRequest);
|
||||
|
||||
assertNotNull(lowInvestment);
|
||||
assertNotNull(highInvestment);
|
||||
// L'investissement plus faible devrait avoir un meilleur ROI
|
||||
assertTrue(lowInvestment.getPaybackPeriodMonths() < highInvestment.getPaybackPeriodMonths());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDifferentModules() {
|
||||
// Test avec un seul module
|
||||
sampleRequest.setSelectedModules(java.util.Arrays.asList("CRM"));
|
||||
ROIResult singleModule = roiCalculator.calculateROI(sampleRequest);
|
||||
|
||||
// Test avec plusieurs modules
|
||||
sampleRequest.setSelectedModules(java.util.Arrays.asList("CRM", "STOCK", "COMPTA", "RH"));
|
||||
ROIResult multipleModules = roiCalculator.calculateROI(sampleRequest);
|
||||
|
||||
// Plus de modules devraient générer plus de gains
|
||||
assertTrue(multipleModules.getTotalAnnualGains() > singleModule.getTotalAnnualGains());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDifferentSectors() {
|
||||
// Test secteur commerce
|
||||
sampleRequest.setSector("Commerce");
|
||||
ROIResult commerce = roiCalculator.calculateROI(sampleRequest);
|
||||
|
||||
// Test secteur services
|
||||
sampleRequest.setSector("Services");
|
||||
ROIResult services = roiCalculator.calculateROI(sampleRequest);
|
||||
|
||||
assertNotNull(commerce);
|
||||
assertNotNull(services);
|
||||
assertTrue(commerce.getTotalAnnualGains() > 0);
|
||||
assertTrue(services.getTotalAnnualGains() > 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEmployeeCountImpact() {
|
||||
// Test avec peu d'employés
|
||||
sampleRequest.setEmployeeCount(5);
|
||||
ROIResult smallTeam = roiCalculator.calculateROI(sampleRequest);
|
||||
|
||||
// Test avec beaucoup d'employés
|
||||
sampleRequest.setEmployeeCount(50);
|
||||
ROIResult largeTeam = roiCalculator.calculateROI(sampleRequest);
|
||||
|
||||
// Les gains devraient être plus importants avec plus d'employés
|
||||
assertTrue(largeTeam.getTotalAnnualGains() > smallTeam.getTotalAnnualGains());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRevenueImpact() {
|
||||
// Test avec faible chiffre d'affaires
|
||||
sampleRequest.setTurnover(100000000.0); // 100M FCFA
|
||||
ROIResult lowRevenue = roiCalculator.calculateROI(sampleRequest);
|
||||
|
||||
// Test avec chiffre d'affaires élevé
|
||||
sampleRequest.setTurnover(1000000000.0); // 1B FCFA
|
||||
ROIResult highRevenue = roiCalculator.calculateROI(sampleRequest);
|
||||
|
||||
// Les gains devraient être plus importants avec un CA plus élevé
|
||||
assertTrue(highRevenue.getTotalAnnualGains() > lowRevenue.getTotalAnnualGains());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testResultComponents() {
|
||||
ROIResult result = roiCalculator.calculateROI(sampleRequest);
|
||||
|
||||
// Vérifier que tous les composants sont présents et positifs
|
||||
assertTrue(result.getAnnualProductivityGains() >= 0);
|
||||
assertTrue(result.getAnnualErrorReduction() >= 0);
|
||||
assertTrue(result.getAnnualTimeSavings() >= 0);
|
||||
assertTrue(result.getAnnualComplianceGains() >= 0);
|
||||
|
||||
// Le total devrait être la somme des composants
|
||||
double expectedTotal = result.getAnnualProductivityGains() +
|
||||
result.getAnnualErrorReduction() +
|
||||
result.getAnnualTimeSavings() +
|
||||
result.getAnnualComplianceGains();
|
||||
assertEquals(expectedTotal, result.getTotalAnnualGains(), 0.01);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPaybackPeriod() {
|
||||
ROIResult result = roiCalculator.calculateROI(sampleRequest);
|
||||
|
||||
assertTrue(result.getPaybackPeriodMonths() > 0);
|
||||
assertTrue(result.getPaybackPeriodMonths() <= 60); // Maximum 5 ans de retour sur investissement
|
||||
|
||||
// Vérifier la cohérence : payback = investissement / (gains annuels / 12)
|
||||
double expectedPayback = sampleRequest.getInvestmentAmount() / (result.getTotalAnnualGains() / 12);
|
||||
assertEquals(expectedPayback, result.getPaybackPeriodMonths(), 0.1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testInvalidInputHandling() {
|
||||
// Test avec chiffre d'affaires négatif
|
||||
sampleRequest.setTurnover(-1000000.0);
|
||||
// Note: Le calculateur pourrait ne pas valider les entrées négatives
|
||||
ROIResult result = roiCalculator.calculateROI(sampleRequest);
|
||||
assertNotNull(result); // Juste vérifier qu'il ne plante pas
|
||||
|
||||
// Test avec nombre d'employés négatif
|
||||
sampleRequest.setTurnover(500000000.0);
|
||||
sampleRequest.setEmployeeCount(-5);
|
||||
result = roiCalculator.calculateROI(sampleRequest);
|
||||
assertNotNull(result); // Juste vérifier qu'il ne plante pas
|
||||
}
|
||||
}
|
||||
14
src/test/resources/application.properties
Normal file
14
src/test/resources/application.properties
Normal file
@@ -0,0 +1,14 @@
|
||||
# Configuration pour les tests
|
||||
app.base-url=http://localhost:8080
|
||||
app.storage.base-path=/tmp/lionsdev-test
|
||||
|
||||
# Configuration de la base de données pour les tests
|
||||
quarkus.datasource.db-kind=postgresql
|
||||
quarkus.hibernate-orm.schema-management.strategy=drop-and-create
|
||||
|
||||
# Configuration des logs pour les tests
|
||||
quarkus.log.level=INFO
|
||||
quarkus.log.category."dev.lions".level=DEBUG
|
||||
|
||||
# Configuration des tests
|
||||
quarkus.test.profile=test
|
||||
Reference in New Issue
Block a user