Compare commits
3 Commits
51265fb0fa
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 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
|
||||||
34
pom.xml
34
pom.xml
@@ -14,9 +14,9 @@
|
|||||||
<!-- Versions -->
|
<!-- Versions -->
|
||||||
<compiler-plugin.version>3.13.0</compiler-plugin.version>
|
<compiler-plugin.version>3.13.0</compiler-plugin.version>
|
||||||
<maven.compiler.release>17</maven.compiler.release>
|
<maven.compiler.release>17</maven.compiler.release>
|
||||||
<myfaces.version>4.0.2</myfaces.version>
|
<myfaces.version>4.0.1</myfaces.version>
|
||||||
<primefaces.version>14.0.0</primefaces.version>
|
<primefaces.version>13.0.5</primefaces.version>
|
||||||
<quarkus.platform.version>3.27.3</quarkus.platform.version>
|
<quarkus.platform.version>3.7.3</quarkus.platform.version>
|
||||||
<lombok.version>1.18.32</lombok.version>
|
<lombok.version>1.18.32</lombok.version>
|
||||||
<jackson.version>2.17.0</jackson.version>
|
<jackson.version>2.17.0</jackson.version>
|
||||||
|
|
||||||
@@ -85,23 +85,12 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.quarkiverse.primefaces</groupId>
|
<groupId>io.quarkiverse.primefaces</groupId>
|
||||||
<artifactId>quarkus-primefaces</artifactId>
|
<artifactId>quarkus-primefaces</artifactId>
|
||||||
<version>3.15.1</version>
|
<version>3.14.0</version>
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.primefaces</groupId>
|
|
||||||
<artifactId>primefaces</artifactId>
|
|
||||||
<version>14.0.0</version>
|
|
||||||
<classifier>jakarta</classifier>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.myfaces.core.extensions.quarkus</groupId>
|
<groupId>org.apache.myfaces.core.extensions.quarkus</groupId>
|
||||||
<artifactId>myfaces-quarkus</artifactId>
|
<artifactId>myfaces-quarkus</artifactId>
|
||||||
<version>4.0.2</version>
|
<version>4.0.1</version>
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>dev.lions</groupId>
|
|
||||||
<artifactId>primefaces-freya-extension</artifactId>
|
|
||||||
<version>1.0.0-SNAPSHOT</version>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- Persistence -->
|
<!-- Persistence -->
|
||||||
@@ -134,13 +123,6 @@
|
|||||||
<groupId>io.quarkus</groupId>
|
<groupId>io.quarkus</groupId>
|
||||||
<artifactId>quarkus-mailer</artifactId>
|
<artifactId>quarkus-mailer</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- PDF Generation -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.itextpdf</groupId>
|
|
||||||
<artifactId>itextpdf</artifactId>
|
|
||||||
<version>5.5.13.3</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.quarkus</groupId>
|
<groupId>io.quarkus</groupId>
|
||||||
<artifactId>quarkus-websockets</artifactId>
|
<artifactId>quarkus-websockets</artifactId>
|
||||||
@@ -215,11 +197,7 @@
|
|||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.quarkus</groupId>
|
<groupId>io.quarkus</groupId>
|
||||||
<artifactId>quarkus-rest</artifactId>
|
<artifactId>quarkus-resteasy-reactive</artifactId>
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>io.quarkus</groupId>
|
|
||||||
<artifactId>quarkus-rest-jackson</artifactId>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
<!-- Sécurité -->
|
<!-- Sécurité -->
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|||||||
@@ -1,393 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
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; }
|
|
||||||
}
|
|
||||||
@@ -1,305 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,198 +0,0 @@
|
|||||||
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+_.-]+@(.+)$");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
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; }
|
|
||||||
}
|
|
||||||
@@ -1,247 +0,0 @@
|
|||||||
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;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
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; }
|
|
||||||
}
|
|
||||||
@@ -1,534 +0,0 @@
|
|||||||
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; }
|
|
||||||
}
|
|
||||||
@@ -1,197 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,338 +0,0 @@
|
|||||||
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; }
|
|
||||||
}
|
|
||||||
@@ -1,257 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
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%)
|
|
||||||
}
|
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
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; }
|
|
||||||
}
|
|
||||||
@@ -1,245 +0,0 @@
|
|||||||
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é
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
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; }
|
|
||||||
}
|
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
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; }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
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; }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
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; }
|
|
||||||
}
|
|
||||||
@@ -1,421 +0,0 @@
|
|||||||
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";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,296 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,356 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,256 +0,0 @@
|
|||||||
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; }
|
|
||||||
}
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
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"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,637 +0,0 @@
|
|||||||
<!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>
|
|
||||||
@@ -1,419 +0,0 @@
|
|||||||
<!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>
|
|
||||||
@@ -1,569 +0,0 @@
|
|||||||
<!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>
|
|
||||||
@@ -1,558 +0,0 @@
|
|||||||
<!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
|
# Configuration proxy et CORS
|
||||||
quarkus.http.proxy.proxy-address-forwarding=true
|
quarkus.http.proxy.proxy-address-forwarding=true
|
||||||
quarkus.http.proxy.allow-forwarded=true
|
quarkus.http.proxy.allow-forwarded=true
|
||||||
quarkus.http.cors.enabled=true
|
quarkus.http.cors=true
|
||||||
quarkus.http.cors.origins=${CORS_ORIGINS:http://localhost:8707}
|
quarkus.http.cors.origins=${CORS_ORIGINS:http://localhost:8707}
|
||||||
quarkus.http.cors.methods=GET,POST,PUT,DELETE,OPTIONS
|
quarkus.http.cors.methods=GET,POST,PUT,DELETE,OPTIONS
|
||||||
quarkus.http.cors.headers=Content-Type,Authorization
|
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
|
%prod.jakarta.faces.PROJECT_STAGE=Production
|
||||||
%production.jakarta.faces.PROJECT_STAGE=Production
|
%production.jakarta.faces.PROJECT_STAGE=Production
|
||||||
|
|
||||||
# Chemins d'accès JSF
|
# Chemins d'acc<EFBFBD>s JSF
|
||||||
#quarkus.servlet.context-path=/lions-dev
|
#quarkus.servlet.context-path=/lions-dev
|
||||||
quarkus.http.non-application-root-path=/q
|
quarkus.http.non-application-root-path=/q
|
||||||
|
|
||||||
@@ -188,7 +188,7 @@ app.admin.email=${ADMIN_EMAIL:admin@lions.dev}
|
|||||||
#==========================================================
|
#==========================================================
|
||||||
# M<>triques et documentation API
|
# M<>triques et documentation API
|
||||||
%prod.quarkus.micrometer.export.prometheus.enabled=true
|
%prod.quarkus.micrometer.export.prometheus.enabled=true
|
||||||
quarkus.swagger-ui.enabled=true
|
quarkus.swagger-ui.enable=true
|
||||||
quarkus.swagger-ui.always-include=true
|
quarkus.swagger-ui.always-include=true
|
||||||
quarkus.smallrye-openapi.info-title=Lions Dev API
|
quarkus.smallrye-openapi.info-title=Lions Dev API
|
||||||
quarkus.smallrye-openapi.info-version=${app.version}
|
quarkus.smallrye-openapi.info-version=${app.version}
|
||||||
|
|||||||
@@ -1,99 +0,0 @@
|
|||||||
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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
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"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
# 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