chore(quarkus-327): bump to Quarkus 3.27.3 LTS, rename quarkus-resteasy-reactive → quarkus-rest, fix testGetAuditQuestions map vs list, rename deprecated config keys
This commit is contained in:
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.1</myfaces.version>
|
<myfaces.version>4.0.2</myfaces.version>
|
||||||
<primefaces.version>13.0.5</primefaces.version>
|
<primefaces.version>14.0.0</primefaces.version>
|
||||||
<quarkus.platform.version>3.7.3</quarkus.platform.version>
|
<quarkus.platform.version>3.27.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,12 +85,23 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.quarkiverse.primefaces</groupId>
|
<groupId>io.quarkiverse.primefaces</groupId>
|
||||||
<artifactId>quarkus-primefaces</artifactId>
|
<artifactId>quarkus-primefaces</artifactId>
|
||||||
<version>3.14.0</version>
|
<version>3.15.1</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.primefaces</groupId>
|
||||||
|
<artifactId>primefaces</artifactId>
|
||||||
|
<version>14.0.0</version>
|
||||||
|
<classifier>jakarta</classifier>
|
||||||
</dependency>
|
</dependency>
|
||||||
<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.1</version>
|
<version>4.0.2</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>dev.lions</groupId>
|
||||||
|
<artifactId>primefaces-freya-extension</artifactId>
|
||||||
|
<version>1.0.0-SNAPSHOT</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- Persistence -->
|
<!-- Persistence -->
|
||||||
@@ -123,6 +134,13 @@
|
|||||||
<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>
|
||||||
@@ -197,7 +215,11 @@
|
|||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.quarkus</groupId>
|
<groupId>io.quarkus</groupId>
|
||||||
<artifactId>quarkus-resteasy-reactive</artifactId>
|
<artifactId>quarkus-rest</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.quarkus</groupId>
|
||||||
|
<artifactId>quarkus-rest-jackson</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
<!-- Sécurité -->
|
<!-- Sécurité -->
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|||||||
393
src/main/java/dev/lions/audit/AuditDataInitializer.java
Normal file
393
src/main/java/dev/lions/audit/AuditDataInitializer.java
Normal file
@@ -0,0 +1,393 @@
|
|||||||
|
package dev.lions.audit;
|
||||||
|
|
||||||
|
import dev.lions.quote.ModuleCatalog;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.enterprise.event.Observes;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.persistence.EntityManager;
|
||||||
|
import jakarta.transaction.Transactional;
|
||||||
|
import io.quarkus.runtime.StartupEvent;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialisation des questions d'audit spécifiques aux PME ivoiriennes
|
||||||
|
*/
|
||||||
|
@ApplicationScoped
|
||||||
|
public class AuditDataInitializer {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
EntityManager em;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void initializeAuditQuestions(@Observes StartupEvent event) {
|
||||||
|
// Vérifier si les questions existent déjà
|
||||||
|
Long questionCount = em.createQuery("SELECT COUNT(q) FROM AuditQuestion q", Long.class)
|
||||||
|
.getSingleResult();
|
||||||
|
|
||||||
|
if (questionCount == 0) {
|
||||||
|
createCommercialQuestions();
|
||||||
|
createStockQuestions();
|
||||||
|
createComptabiliteQuestions();
|
||||||
|
createRhQuestions();
|
||||||
|
createInfrastructureQuestions();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier si le catalogue existe déjà
|
||||||
|
Long catalogCount = em.createQuery("SELECT COUNT(m) FROM ModuleCatalog m", Long.class)
|
||||||
|
.getSingleResult();
|
||||||
|
|
||||||
|
if (catalogCount == 0) {
|
||||||
|
createModuleCatalog();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createCommercialQuestions() {
|
||||||
|
// Question 1: Gestion des clients
|
||||||
|
AuditQuestion q1 = new AuditQuestion();
|
||||||
|
q1.setCategory("commercial");
|
||||||
|
q1.setQuestion("Comment gérez-vous actuellement vos contacts clients ?");
|
||||||
|
q1.setOptions(Arrays.asList(
|
||||||
|
"Carnet papier ou fichier Excel basique",
|
||||||
|
"Fichier Excel avec historique des contacts",
|
||||||
|
"Logiciel de gestion simple (contacts + ventes)",
|
||||||
|
"CRM complet avec suivi des opportunités",
|
||||||
|
"CRM avancé avec automation marketing"
|
||||||
|
));
|
||||||
|
q1.setScores(Arrays.asList(1, 2, 3, 4, 5));
|
||||||
|
q1.setWeight(3);
|
||||||
|
q1.setDisplayOrder(1);
|
||||||
|
q1.setRecommendation("Un CRM adapté améliore la relation client et augmente les ventes de 20-30%");
|
||||||
|
em.persist(q1);
|
||||||
|
|
||||||
|
// Question 2: Facturation
|
||||||
|
AuditQuestion q2 = new AuditQuestion();
|
||||||
|
q2.setCategory("commercial");
|
||||||
|
q2.setQuestion("Comment établissez-vous vos factures ?");
|
||||||
|
q2.setOptions(Arrays.asList(
|
||||||
|
"Factures manuscrites ou Word/Excel",
|
||||||
|
"Modèle Excel avec calculs automatiques",
|
||||||
|
"Logiciel de facturation simple",
|
||||||
|
"Système intégré (devis → facture → paiement)",
|
||||||
|
"Facturation automatisée avec relances"
|
||||||
|
));
|
||||||
|
q2.setScores(Arrays.asList(1, 2, 3, 4, 5));
|
||||||
|
q2.setWeight(3);
|
||||||
|
q2.setDisplayOrder(2);
|
||||||
|
q2.setRecommendation("La facturation automatisée réduit les erreurs et accélère les paiements");
|
||||||
|
em.persist(q2);
|
||||||
|
|
||||||
|
// Question 3: Suivi des ventes
|
||||||
|
AuditQuestion q3 = new AuditQuestion();
|
||||||
|
q3.setCategory("commercial");
|
||||||
|
q3.setQuestion("Avez-vous une visibilité sur vos performances commerciales ?");
|
||||||
|
q3.setOptions(Arrays.asList(
|
||||||
|
"Aucun suivi régulier",
|
||||||
|
"Calculs manuels mensuels",
|
||||||
|
"Tableaux de bord Excel",
|
||||||
|
"Rapports automatiques hebdomadaires",
|
||||||
|
"Dashboard temps réel avec KPI"
|
||||||
|
));
|
||||||
|
q3.setScores(Arrays.asList(1, 2, 3, 4, 5));
|
||||||
|
q3.setWeight(2);
|
||||||
|
q3.setDisplayOrder(3);
|
||||||
|
em.persist(q3);
|
||||||
|
|
||||||
|
// Question 4: Gestion des devis
|
||||||
|
AuditQuestion q4 = new AuditQuestion();
|
||||||
|
q4.setCategory("commercial");
|
||||||
|
q4.setQuestion("Comment gérez-vous vos devis et propositions commerciales ?");
|
||||||
|
q4.setOptions(Arrays.asList(
|
||||||
|
"Devis manuscrits ou Word",
|
||||||
|
"Modèles Excel personnalisables",
|
||||||
|
"Logiciel de devis avec bibliothèque",
|
||||||
|
"Système intégré avec signature électronique",
|
||||||
|
"Configurateur automatique avec approbation workflow"
|
||||||
|
));
|
||||||
|
q4.setScores(Arrays.asList(1, 2, 3, 4, 5));
|
||||||
|
q4.setWeight(2);
|
||||||
|
q4.setDisplayOrder(4);
|
||||||
|
em.persist(q4);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createStockQuestions() {
|
||||||
|
// Question 1: Inventaire
|
||||||
|
AuditQuestion q1 = new AuditQuestion();
|
||||||
|
q1.setCategory("stock");
|
||||||
|
q1.setQuestion("Comment suivez-vous vos stocks ?");
|
||||||
|
q1.setOptions(Arrays.asList(
|
||||||
|
"Comptage manuel périodique",
|
||||||
|
"Fichier Excel mis à jour manuellement",
|
||||||
|
"Logiciel de stock simple",
|
||||||
|
"Système avec codes-barres",
|
||||||
|
"Gestion automatisée avec alertes"
|
||||||
|
));
|
||||||
|
q1.setScores(Arrays.asList(1, 2, 3, 4, 5));
|
||||||
|
q1.setWeight(3);
|
||||||
|
q1.setDisplayOrder(1);
|
||||||
|
em.persist(q1);
|
||||||
|
|
||||||
|
// Question 2: Approvisionnement
|
||||||
|
AuditQuestion q2 = new AuditQuestion();
|
||||||
|
q2.setCategory("stock");
|
||||||
|
q2.setQuestion("Comment planifiez-vous vos achats et approvisionnements ?");
|
||||||
|
q2.setOptions(Arrays.asList(
|
||||||
|
"Achats au feeling quand stock bas",
|
||||||
|
"Planning mensuel basique",
|
||||||
|
"Calculs de stock minimum",
|
||||||
|
"Prévisions basées sur l'historique",
|
||||||
|
"Optimisation automatique avec IA"
|
||||||
|
));
|
||||||
|
q2.setScores(Arrays.asList(1, 2, 3, 4, 5));
|
||||||
|
q2.setWeight(2);
|
||||||
|
q2.setDisplayOrder(2);
|
||||||
|
em.persist(q2);
|
||||||
|
|
||||||
|
// Question 3: Valorisation
|
||||||
|
AuditQuestion q3 = new AuditQuestion();
|
||||||
|
q3.setCategory("stock");
|
||||||
|
q3.setQuestion("Connaissez-vous la valeur exacte de votre stock ?");
|
||||||
|
q3.setOptions(Arrays.asList(
|
||||||
|
"Estimation approximative",
|
||||||
|
"Calcul manuel périodique",
|
||||||
|
"Valorisation Excel",
|
||||||
|
"Valorisation automatique temps réel",
|
||||||
|
"Analyse de rotation et obsolescence"
|
||||||
|
));
|
||||||
|
q3.setScores(Arrays.asList(1, 2, 3, 4, 5));
|
||||||
|
q3.setWeight(2);
|
||||||
|
q3.setDisplayOrder(3);
|
||||||
|
em.persist(q3);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createComptabiliteQuestions() {
|
||||||
|
// Question 1: Tenue comptable
|
||||||
|
AuditQuestion q1 = new AuditQuestion();
|
||||||
|
q1.setCategory("comptabilite");
|
||||||
|
q1.setQuestion("Comment tenez-vous votre comptabilité ?");
|
||||||
|
q1.setOptions(Arrays.asList(
|
||||||
|
"Cahiers manuscrits",
|
||||||
|
"Excel avec saisie manuelle",
|
||||||
|
"Logiciel comptable simple",
|
||||||
|
"Logiciel intégré avec automatisation",
|
||||||
|
"ERP complet avec expert-comptable connecté"
|
||||||
|
));
|
||||||
|
q1.setScores(Arrays.asList(1, 2, 3, 4, 5));
|
||||||
|
q1.setWeight(3);
|
||||||
|
q1.setDisplayOrder(1);
|
||||||
|
em.persist(q1);
|
||||||
|
|
||||||
|
// Question 2: Déclarations fiscales
|
||||||
|
AuditQuestion q2 = new AuditQuestion();
|
||||||
|
q2.setCategory("comptabilite");
|
||||||
|
q2.setQuestion("Comment gérez-vous vos obligations fiscales (TVA, IS, etc.) ?");
|
||||||
|
q2.setOptions(Arrays.asList(
|
||||||
|
"Calculs manuels avec expert-comptable",
|
||||||
|
"Excel avec formules de calcul",
|
||||||
|
"Logiciel avec aide au calcul",
|
||||||
|
"Génération automatique des déclarations",
|
||||||
|
"Télédéclaration automatisée"
|
||||||
|
));
|
||||||
|
q2.setScores(Arrays.asList(1, 2, 3, 4, 5));
|
||||||
|
q2.setWeight(3);
|
||||||
|
q2.setDisplayOrder(2);
|
||||||
|
em.persist(q2);
|
||||||
|
|
||||||
|
// Question 3: Analyse financière
|
||||||
|
AuditQuestion q3 = new AuditQuestion();
|
||||||
|
q3.setCategory("comptabilite");
|
||||||
|
q3.setQuestion("Avez-vous une vision claire de votre situation financière ?");
|
||||||
|
q3.setOptions(Arrays.asList(
|
||||||
|
"Bilan annuel uniquement",
|
||||||
|
"Situation trimestrielle",
|
||||||
|
"Tableaux de bord mensuels",
|
||||||
|
"Reporting automatique hebdomadaire",
|
||||||
|
"Dashboard temps réel avec prévisions"
|
||||||
|
));
|
||||||
|
q3.setScores(Arrays.asList(1, 2, 3, 4, 5));
|
||||||
|
q3.setWeight(2);
|
||||||
|
q3.setDisplayOrder(3);
|
||||||
|
em.persist(q3);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createRhQuestions() {
|
||||||
|
// Question 1: Gestion du personnel
|
||||||
|
AuditQuestion q1 = new AuditQuestion();
|
||||||
|
q1.setCategory("rh");
|
||||||
|
q1.setQuestion("Comment gérez-vous les dossiers de vos employés ?");
|
||||||
|
q1.setOptions(Arrays.asList(
|
||||||
|
"Dossiers papier classés",
|
||||||
|
"Fichiers Excel par employé",
|
||||||
|
"Logiciel RH basique",
|
||||||
|
"SIRH avec self-service employé",
|
||||||
|
"SIRH complet avec workflow"
|
||||||
|
));
|
||||||
|
q1.setScores(Arrays.asList(1, 2, 3, 4, 5));
|
||||||
|
q1.setWeight(2);
|
||||||
|
q1.setDisplayOrder(1);
|
||||||
|
em.persist(q1);
|
||||||
|
|
||||||
|
// Question 2: Paie et CNPS
|
||||||
|
AuditQuestion q2 = new AuditQuestion();
|
||||||
|
q2.setCategory("rh");
|
||||||
|
q2.setQuestion("Comment calculez-vous la paie et les cotisations CNPS ?");
|
||||||
|
q2.setOptions(Arrays.asList(
|
||||||
|
"Calculs manuels",
|
||||||
|
"Excel avec formules",
|
||||||
|
"Logiciel de paie simple",
|
||||||
|
"Paie automatisée avec déclarations",
|
||||||
|
"Intégration complète CNPS/DGI"
|
||||||
|
));
|
||||||
|
q2.setScores(Arrays.asList(1, 2, 3, 4, 5));
|
||||||
|
q2.setWeight(3);
|
||||||
|
q2.setDisplayOrder(2);
|
||||||
|
em.persist(q2);
|
||||||
|
|
||||||
|
// Question 3: Gestion des congés
|
||||||
|
AuditQuestion q3 = new AuditQuestion();
|
||||||
|
q3.setCategory("rh");
|
||||||
|
q3.setQuestion("Comment gérez-vous les demandes de congés ?");
|
||||||
|
q3.setOptions(Arrays.asList(
|
||||||
|
"Demandes papier",
|
||||||
|
"Email et validation manuelle",
|
||||||
|
"Fichier Excel partagé",
|
||||||
|
"Système de workflow digital",
|
||||||
|
"Application mobile avec approbation"
|
||||||
|
));
|
||||||
|
q3.setScores(Arrays.asList(1, 2, 3, 4, 5));
|
||||||
|
q3.setWeight(1);
|
||||||
|
q3.setDisplayOrder(3);
|
||||||
|
em.persist(q3);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createInfrastructureQuestions() {
|
||||||
|
// Question 1: Équipement informatique
|
||||||
|
AuditQuestion q1 = new AuditQuestion();
|
||||||
|
q1.setCategory("infrastructure");
|
||||||
|
q1.setQuestion("Quel est l'état de votre parc informatique ?");
|
||||||
|
q1.setOptions(Arrays.asList(
|
||||||
|
"Ordinateurs anciens (>5 ans), pas de réseau",
|
||||||
|
"Mix d'ordinateurs, réseau basique",
|
||||||
|
"Parc récent, réseau WiFi, serveur local",
|
||||||
|
"Infrastructure moderne, cloud hybride",
|
||||||
|
"Infrastructure cloud-native sécurisée"
|
||||||
|
));
|
||||||
|
q1.setScores(Arrays.asList(1, 2, 3, 4, 5));
|
||||||
|
q1.setWeight(2);
|
||||||
|
q1.setDisplayOrder(1);
|
||||||
|
em.persist(q1);
|
||||||
|
|
||||||
|
// Question 2: Sauvegardes
|
||||||
|
AuditQuestion q2 = new AuditQuestion();
|
||||||
|
q2.setCategory("infrastructure");
|
||||||
|
q2.setQuestion("Comment protégez-vous vos données importantes ?");
|
||||||
|
q2.setOptions(Arrays.asList(
|
||||||
|
"Aucune sauvegarde régulière",
|
||||||
|
"Copies manuelles sur clé USB",
|
||||||
|
"Sauvegarde externe périodique",
|
||||||
|
"Sauvegarde automatique cloud",
|
||||||
|
"Sauvegarde redondante avec plan de reprise"
|
||||||
|
));
|
||||||
|
q2.setScores(Arrays.asList(1, 2, 3, 4, 5));
|
||||||
|
q2.setWeight(3);
|
||||||
|
q2.setDisplayOrder(2);
|
||||||
|
em.persist(q2);
|
||||||
|
|
||||||
|
// Question 3: Sécurité
|
||||||
|
AuditQuestion q3 = new AuditQuestion();
|
||||||
|
q3.setCategory("infrastructure");
|
||||||
|
q3.setQuestion("Quelles mesures de sécurité informatique avez-vous ?");
|
||||||
|
q3.setOptions(Arrays.asList(
|
||||||
|
"Antivirus basique uniquement",
|
||||||
|
"Antivirus + mots de passe",
|
||||||
|
"Sécurité réseau + formation utilisateurs",
|
||||||
|
"Sécurité multicouche + monitoring",
|
||||||
|
"Sécurité enterprise avec audit régulier"
|
||||||
|
));
|
||||||
|
q3.setScores(Arrays.asList(1, 2, 3, 4, 5));
|
||||||
|
q3.setWeight(3);
|
||||||
|
q3.setDisplayOrder(3);
|
||||||
|
em.persist(q3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée le catalogue des modules avec tarification
|
||||||
|
*/
|
||||||
|
private void createModuleCatalog() {
|
||||||
|
// Module CRM Commercial
|
||||||
|
ModuleCatalog crmModule = new ModuleCatalog("CRM", "Gestion Commerciale CRM", "commercial");
|
||||||
|
crmModule.setDescription("Solution complète de gestion de la relation client avec suivi des prospects, opportunités et ventes");
|
||||||
|
crmModule.setFeatures("• Gestion contacts et prospects\n• Pipeline de ventes\n• Suivi des opportunités\n• Facturation intégrée\n• Rapports commerciaux\n• Tableaux de bord");
|
||||||
|
crmModule.setBasicPrice(150000.0); // 150K FCFA
|
||||||
|
crmModule.setStandardPrice(250000.0); // 250K FCFA
|
||||||
|
crmModule.setAdvancedPrice(400000.0); // 400K FCFA
|
||||||
|
crmModule.setEnterprisePrice(650000.0); // 650K FCFA
|
||||||
|
crmModule.setBaseImplementationDays(10);
|
||||||
|
crmModule.setMaxUsers(50);
|
||||||
|
crmModule.setSupportLevel("Email + Téléphone");
|
||||||
|
crmModule.setTechnicalRequirements("Windows/Mac/Linux, Navigateur web moderne, 4GB RAM minimum");
|
||||||
|
crmModule.setDisplayOrder(1);
|
||||||
|
crmModule.setPopular(true);
|
||||||
|
em.persist(crmModule);
|
||||||
|
|
||||||
|
// Module Gestion de Stock
|
||||||
|
ModuleCatalog stockModule = new ModuleCatalog("STOCK", "Gestion des Stocks", "stock");
|
||||||
|
stockModule.setDescription("Système de gestion des stocks avec codes-barres, inventaires et approvisionnements automatisés");
|
||||||
|
stockModule.setFeatures("• Gestion multi-entrepôts\n• Codes-barres et QR codes\n• Inventaires automatisés\n• Alertes stock minimum\n• Prévisions d'achat\n• Valorisation FIFO/LIFO");
|
||||||
|
stockModule.setBasicPrice(120000.0);
|
||||||
|
stockModule.setStandardPrice(200000.0);
|
||||||
|
stockModule.setAdvancedPrice(350000.0);
|
||||||
|
stockModule.setEnterprisePrice(550000.0);
|
||||||
|
stockModule.setBaseImplementationDays(8);
|
||||||
|
stockModule.setMaxUsers(20);
|
||||||
|
stockModule.setSupportLevel("Email + Téléphone");
|
||||||
|
stockModule.setTechnicalRequirements("Windows/Mac/Linux, Lecteur codes-barres (optionnel), Imprimante étiquettes");
|
||||||
|
stockModule.setDisplayOrder(2);
|
||||||
|
em.persist(stockModule);
|
||||||
|
|
||||||
|
// Module Comptabilité
|
||||||
|
ModuleCatalog comptaModule = new ModuleCatalog("COMPTA", "Comptabilité Intégrée", "comptabilite");
|
||||||
|
comptaModule.setDescription("Comptabilité complète conforme aux normes ivoiriennes avec déclarations fiscales automatisées");
|
||||||
|
comptaModule.setFeatures("• Plan comptable SYSCOHADA\n• Saisie automatisée\n• Déclarations TVA/IS\n• Bilan et compte de résultat\n• Rapports DGI\n• Intégration bancaire");
|
||||||
|
comptaModule.setBasicPrice(180000.0);
|
||||||
|
comptaModule.setStandardPrice(300000.0);
|
||||||
|
comptaModule.setAdvancedPrice(500000.0);
|
||||||
|
comptaModule.setEnterprisePrice(800000.0);
|
||||||
|
comptaModule.setBaseImplementationDays(12);
|
||||||
|
comptaModule.setMaxUsers(10);
|
||||||
|
comptaModule.setSupportLevel("Email + Téléphone + Expert-comptable");
|
||||||
|
comptaModule.setTechnicalRequirements("Windows/Mac/Linux, Connexion internet sécurisée, Sauvegarde automatique");
|
||||||
|
comptaModule.setDisplayOrder(3);
|
||||||
|
comptaModule.setPopular(true);
|
||||||
|
em.persist(comptaModule);
|
||||||
|
|
||||||
|
// Module RH
|
||||||
|
ModuleCatalog rhModule = new ModuleCatalog("RH", "Gestion des Ressources Humaines", "rh");
|
||||||
|
rhModule.setDescription("SIRH complet avec paie, congés, formation et conformité CNPS");
|
||||||
|
rhModule.setFeatures("• Dossiers employés\n• Calcul de paie CNPS\n• Gestion des congés\n• Formation et évaluation\n• Déclarations sociales\n• Self-service employé");
|
||||||
|
rhModule.setBasicPrice(100000.0);
|
||||||
|
rhModule.setStandardPrice(180000.0);
|
||||||
|
rhModule.setAdvancedPrice(320000.0);
|
||||||
|
rhModule.setEnterprisePrice(500000.0);
|
||||||
|
rhModule.setBaseImplementationDays(6);
|
||||||
|
rhModule.setMaxUsers(100);
|
||||||
|
rhModule.setSupportLevel("Email + Téléphone");
|
||||||
|
rhModule.setTechnicalRequirements("Windows/Mac/Linux, Connexion CNPS (optionnelle), Scanner documents");
|
||||||
|
rhModule.setDisplayOrder(4);
|
||||||
|
em.persist(rhModule);
|
||||||
|
|
||||||
|
// Module Infrastructure
|
||||||
|
ModuleCatalog infraModule = new ModuleCatalog("INFRA", "Infrastructure IT", "infrastructure");
|
||||||
|
infraModule.setDescription("Mise en place et sécurisation de l'infrastructure informatique avec sauvegarde cloud");
|
||||||
|
infraModule.setFeatures("• Audit infrastructure\n• Sécurisation réseau\n• Sauvegarde automatique\n• Antivirus enterprise\n• Monitoring 24/7\n• Support technique");
|
||||||
|
infraModule.setBasicPrice(200000.0);
|
||||||
|
infraModule.setStandardPrice(350000.0);
|
||||||
|
infraModule.setAdvancedPrice(600000.0);
|
||||||
|
infraModule.setEnterprisePrice(1000000.0);
|
||||||
|
infraModule.setBaseImplementationDays(5);
|
||||||
|
infraModule.setMaxUsers(999);
|
||||||
|
infraModule.setSupportLevel("Email + Téléphone + Intervention sur site");
|
||||||
|
infraModule.setTechnicalRequirements("Réseau existant, Serveur ou Cloud, Postes de travail Windows/Mac");
|
||||||
|
infraModule.setDisplayOrder(5);
|
||||||
|
em.persist(infraModule);
|
||||||
|
}
|
||||||
|
}
|
||||||
86
src/main/java/dev/lions/audit/AuditQuestion.java
Normal file
86
src/main/java/dev/lions/audit/AuditQuestion.java
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
package dev.lions.audit;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Question d'audit pour évaluer la maturité digitale des PME
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "audit_questions")
|
||||||
|
public class AuditQuestion {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String category; // "commercial", "stock", "comptabilite", "rh", "infrastructure"
|
||||||
|
|
||||||
|
@Column(nullable = false, length = 500)
|
||||||
|
private String question;
|
||||||
|
|
||||||
|
@Column(name = "question_en", length = 500)
|
||||||
|
private String questionEn; // Version anglaise
|
||||||
|
|
||||||
|
@ElementCollection
|
||||||
|
@CollectionTable(name = "audit_question_options")
|
||||||
|
private List<String> options;
|
||||||
|
|
||||||
|
@ElementCollection
|
||||||
|
@CollectionTable(name = "audit_question_scores")
|
||||||
|
private List<Integer> scores; // Score pour chaque option
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private Integer weight = 1; // Poids de la question
|
||||||
|
|
||||||
|
@Column(length = 1000)
|
||||||
|
private String recommendation; // Recommandation selon la réponse
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private Boolean active = true;
|
||||||
|
|
||||||
|
@Column(name = "display_order")
|
||||||
|
private Integer displayOrder;
|
||||||
|
|
||||||
|
// Constructeurs
|
||||||
|
public AuditQuestion() {}
|
||||||
|
|
||||||
|
public AuditQuestion(String category, String question, List<String> options, List<Integer> scores) {
|
||||||
|
this.category = category;
|
||||||
|
this.question = question;
|
||||||
|
this.options = options;
|
||||||
|
this.scores = scores;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters et Setters
|
||||||
|
public Long getId() { return id; }
|
||||||
|
public void setId(Long id) { this.id = id; }
|
||||||
|
|
||||||
|
public String getCategory() { return category; }
|
||||||
|
public void setCategory(String category) { this.category = category; }
|
||||||
|
|
||||||
|
public String getQuestion() { return question; }
|
||||||
|
public void setQuestion(String question) { this.question = question; }
|
||||||
|
|
||||||
|
public String getQuestionEn() { return questionEn; }
|
||||||
|
public void setQuestionEn(String questionEn) { this.questionEn = questionEn; }
|
||||||
|
|
||||||
|
public List<String> getOptions() { return options; }
|
||||||
|
public void setOptions(List<String> options) { this.options = options; }
|
||||||
|
|
||||||
|
public List<Integer> getScores() { return scores; }
|
||||||
|
public void setScores(List<Integer> scores) { this.scores = scores; }
|
||||||
|
|
||||||
|
public Integer getWeight() { return weight; }
|
||||||
|
public void setWeight(Integer weight) { this.weight = weight; }
|
||||||
|
|
||||||
|
public String getRecommendation() { return recommendation; }
|
||||||
|
public void setRecommendation(String recommendation) { this.recommendation = recommendation; }
|
||||||
|
|
||||||
|
public Boolean getActive() { return active; }
|
||||||
|
public void setActive(Boolean active) { this.active = active; }
|
||||||
|
|
||||||
|
public Integer getDisplayOrder() { return displayOrder; }
|
||||||
|
public void setDisplayOrder(Integer displayOrder) { this.displayOrder = displayOrder; }
|
||||||
|
}
|
||||||
305
src/main/java/dev/lions/audit/AuditReportService.java
Normal file
305
src/main/java/dev/lions/audit/AuditReportService.java
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
package dev.lions.audit;
|
||||||
|
|
||||||
|
import com.itextpdf.text.*;
|
||||||
|
import com.itextpdf.text.pdf.*;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import io.quarkus.mailer.Mail;
|
||||||
|
import io.quarkus.mailer.Mailer;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service de génération de rapports PDF d'audit
|
||||||
|
*/
|
||||||
|
@ApplicationScoped
|
||||||
|
public class AuditReportService {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
Mailer mailer;
|
||||||
|
|
||||||
|
private static final Font TITLE_FONT = new Font(Font.FontFamily.HELVETICA, 18, Font.BOLD, BaseColor.DARK_GRAY);
|
||||||
|
private static final Font HEADER_FONT = new Font(Font.FontFamily.HELVETICA, 14, Font.BOLD, BaseColor.BLACK);
|
||||||
|
private static final Font NORMAL_FONT = new Font(Font.FontFamily.HELVETICA, 11, Font.NORMAL, BaseColor.BLACK);
|
||||||
|
private static final Font SMALL_FONT = new Font(Font.FontFamily.HELVETICA, 9, Font.NORMAL, BaseColor.GRAY);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère le rapport PDF d'audit
|
||||||
|
*/
|
||||||
|
public byte[] generateAuditReport(AuditResponse audit) throws Exception {
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
Document document = new Document(PageSize.A4, 50, 50, 50, 50);
|
||||||
|
PdfWriter writer = PdfWriter.getInstance(document, baos);
|
||||||
|
|
||||||
|
document.open();
|
||||||
|
|
||||||
|
// En-tête Lions Dev
|
||||||
|
addHeader(document);
|
||||||
|
|
||||||
|
// Informations entreprise
|
||||||
|
addCompanyInfo(document, audit);
|
||||||
|
|
||||||
|
// Résumé exécutif
|
||||||
|
addExecutiveSummary(document, audit);
|
||||||
|
|
||||||
|
// Scores par catégorie
|
||||||
|
addCategoryScores(document, audit);
|
||||||
|
|
||||||
|
// Recommandations détaillées
|
||||||
|
addRecommendations(document, audit);
|
||||||
|
|
||||||
|
// Estimation budgétaire
|
||||||
|
addBudgetEstimation(document, audit);
|
||||||
|
|
||||||
|
// Prochaines étapes
|
||||||
|
addNextSteps(document);
|
||||||
|
|
||||||
|
// Pied de page
|
||||||
|
addFooter(document);
|
||||||
|
|
||||||
|
document.close();
|
||||||
|
return baos.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addHeader(Document document) throws DocumentException {
|
||||||
|
// Logo et titre Lions Dev
|
||||||
|
Paragraph title = new Paragraph("LIONS DEV", TITLE_FONT);
|
||||||
|
title.setAlignment(Element.ALIGN_CENTER);
|
||||||
|
title.setSpacingAfter(10);
|
||||||
|
document.add(title);
|
||||||
|
|
||||||
|
Paragraph subtitle = new Paragraph("Audit de Maturité Digitale", HEADER_FONT);
|
||||||
|
subtitle.setAlignment(Element.ALIGN_CENTER);
|
||||||
|
subtitle.setSpacingAfter(20);
|
||||||
|
document.add(subtitle);
|
||||||
|
|
||||||
|
// Ligne de séparation
|
||||||
|
com.itextpdf.text.pdf.draw.LineSeparator line = new com.itextpdf.text.pdf.draw.LineSeparator();
|
||||||
|
document.add(new Chunk(line));
|
||||||
|
document.add(Chunk.NEWLINE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addCompanyInfo(Document document, AuditResponse audit) throws DocumentException {
|
||||||
|
Paragraph section = new Paragraph("INFORMATIONS ENTREPRISE", HEADER_FONT);
|
||||||
|
section.setSpacingBefore(10);
|
||||||
|
section.setSpacingAfter(10);
|
||||||
|
document.add(section);
|
||||||
|
|
||||||
|
PdfPTable table = new PdfPTable(2);
|
||||||
|
table.setWidthPercentage(100);
|
||||||
|
table.setSpacingAfter(15);
|
||||||
|
|
||||||
|
addTableRow(table, "Entreprise:", audit.getCompanyName());
|
||||||
|
addTableRow(table, "Contact:", audit.getContactName());
|
||||||
|
addTableRow(table, "Email:", audit.getEmail());
|
||||||
|
addTableRow(table, "Téléphone:", audit.getPhone());
|
||||||
|
addTableRow(table, "Secteur:", audit.getSector());
|
||||||
|
addTableRow(table, "Employés:", audit.getEmployeeCount() + " personnes");
|
||||||
|
addTableRow(table, "CA annuel:", audit.getTurnover());
|
||||||
|
addTableRow(table, "Date audit:", audit.getSubmittedAt().format(DateTimeFormatter.ofPattern("dd/MM/yyyy")));
|
||||||
|
|
||||||
|
document.add(table);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addExecutiveSummary(Document document, AuditResponse audit) throws DocumentException {
|
||||||
|
Paragraph section = new Paragraph("RÉSUMÉ EXÉCUTIF", HEADER_FONT);
|
||||||
|
section.setSpacingBefore(15);
|
||||||
|
section.setSpacingAfter(10);
|
||||||
|
document.add(section);
|
||||||
|
|
||||||
|
// Score global avec couleur
|
||||||
|
String maturityLevel = getMaturityLevel(audit.getMaturityPercentage());
|
||||||
|
BaseColor scoreColor = getScoreColor(audit.getMaturityPercentage());
|
||||||
|
|
||||||
|
Paragraph scoreText = new Paragraph();
|
||||||
|
scoreText.add(new Chunk("Score de maturité digitale: ", NORMAL_FONT));
|
||||||
|
scoreText.add(new Chunk(String.format("%.1f%% (%s)", audit.getMaturityPercentage(), maturityLevel),
|
||||||
|
new Font(Font.FontFamily.HELVETICA, 12, Font.BOLD, scoreColor)));
|
||||||
|
scoreText.setSpacingAfter(15);
|
||||||
|
document.add(scoreText);
|
||||||
|
|
||||||
|
// Graphique en barres des scores par catégorie
|
||||||
|
addCategoryChart(document, audit);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addCategoryScores(Document document, AuditResponse audit) throws DocumentException {
|
||||||
|
Paragraph section = new Paragraph("ANALYSE DÉTAILLÉE PAR DOMAINE", HEADER_FONT);
|
||||||
|
section.setSpacingBefore(15);
|
||||||
|
section.setSpacingAfter(10);
|
||||||
|
document.add(section);
|
||||||
|
|
||||||
|
PdfPTable table = new PdfPTable(3);
|
||||||
|
table.setWidthPercentage(100);
|
||||||
|
table.setWidths(new float[]{3, 1, 2});
|
||||||
|
|
||||||
|
// En-têtes
|
||||||
|
addTableHeader(table, "Domaine");
|
||||||
|
addTableHeader(table, "Score");
|
||||||
|
addTableHeader(table, "Niveau");
|
||||||
|
|
||||||
|
for (Map.Entry<String, Integer> entry : audit.getCategoryScores().entrySet()) {
|
||||||
|
String category = getCategoryDisplayName(entry.getKey());
|
||||||
|
Integer score = entry.getValue();
|
||||||
|
// Calcul du pourcentage (approximatif)
|
||||||
|
double percentage = (double) score / 100 * 100; // À ajuster selon le scoring réel
|
||||||
|
String level = getMaturityLevel(percentage);
|
||||||
|
|
||||||
|
addTableRow(table, category, score.toString() + " - " + level);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.add(table);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addRecommendations(Document document, AuditResponse audit) throws DocumentException {
|
||||||
|
Paragraph section = new Paragraph("RECOMMANDATIONS PRIORITAIRES", HEADER_FONT);
|
||||||
|
section.setSpacingBefore(15);
|
||||||
|
section.setSpacingAfter(10);
|
||||||
|
document.add(section);
|
||||||
|
|
||||||
|
Paragraph recommendations = new Paragraph(audit.getRecommendations(), NORMAL_FONT);
|
||||||
|
recommendations.setSpacingAfter(10);
|
||||||
|
document.add(recommendations);
|
||||||
|
|
||||||
|
if (audit.getPriorityActions() != null && !audit.getPriorityActions().isEmpty()) {
|
||||||
|
Paragraph actions = new Paragraph("Actions prioritaires: " + audit.getPriorityActions(), NORMAL_FONT);
|
||||||
|
actions.setSpacingAfter(15);
|
||||||
|
document.add(actions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addBudgetEstimation(Document document, AuditResponse audit) throws DocumentException {
|
||||||
|
if (audit.getEstimatedBudgetMin() != null && audit.getEstimatedBudgetMax() != null) {
|
||||||
|
Paragraph section = new Paragraph("ESTIMATION BUDGÉTAIRE", HEADER_FONT);
|
||||||
|
section.setSpacingBefore(15);
|
||||||
|
section.setSpacingAfter(10);
|
||||||
|
document.add(section);
|
||||||
|
|
||||||
|
String budgetText = String.format("Investissement estimé pour la digitalisation: %,.0f - %,.0f FCFA",
|
||||||
|
audit.getEstimatedBudgetMin(), audit.getEstimatedBudgetMax());
|
||||||
|
Paragraph budget = new Paragraph(budgetText, NORMAL_FONT);
|
||||||
|
budget.setSpacingAfter(10);
|
||||||
|
document.add(budget);
|
||||||
|
|
||||||
|
Paragraph note = new Paragraph("* Estimation basée sur votre niveau de maturité actuel. " +
|
||||||
|
"Un devis personnalisé sera établi après analyse détaillée.", SMALL_FONT);
|
||||||
|
note.setSpacingAfter(15);
|
||||||
|
document.add(note);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addNextSteps(Document document) throws DocumentException {
|
||||||
|
Paragraph section = new Paragraph("PROCHAINES ÉTAPES", HEADER_FONT);
|
||||||
|
section.setSpacingBefore(15);
|
||||||
|
section.setSpacingAfter(10);
|
||||||
|
document.add(section);
|
||||||
|
|
||||||
|
List list = new List(List.ORDERED);
|
||||||
|
list.add(new ListItem("Rendez-vous diagnostic approfondi (gratuit)", NORMAL_FONT));
|
||||||
|
list.add(new ListItem("Analyse détaillée de vos processus métier", NORMAL_FONT));
|
||||||
|
list.add(new ListItem("Proposition de solution personnalisée", NORMAL_FONT));
|
||||||
|
list.add(new ListItem("Planification du déploiement", NORMAL_FONT));
|
||||||
|
list.add(new ListItem("Formation de vos équipes", NORMAL_FONT));
|
||||||
|
|
||||||
|
document.add(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addFooter(Document document) throws DocumentException {
|
||||||
|
Paragraph footer = new Paragraph("\nLions Dev - Solutions Digitales Innovantes\n" +
|
||||||
|
"Abidjan, Côte d'Ivoire | +225 01 01 75 95 25 | contact@lions.dev", SMALL_FONT);
|
||||||
|
footer.setAlignment(Element.ALIGN_CENTER);
|
||||||
|
footer.setSpacingBefore(20);
|
||||||
|
document.add(footer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Envoie le rapport par email
|
||||||
|
*/
|
||||||
|
public void sendAuditReportByEmail(AuditResponse audit, byte[] pdfReport) {
|
||||||
|
try {
|
||||||
|
Mail mail = Mail.withHtml(audit.getEmail(),
|
||||||
|
"Votre Audit de Maturité Digitale - Lions Dev",
|
||||||
|
generateEmailContent(audit))
|
||||||
|
.addAttachment("audit-" + audit.getCompanyName() + ".pdf",
|
||||||
|
pdfReport, "application/pdf");
|
||||||
|
|
||||||
|
mailer.send(mail);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Log l'erreur mais ne fait pas échouer le processus
|
||||||
|
System.err.println("Erreur envoi email: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String generateEmailContent(AuditResponse audit) {
|
||||||
|
return String.format("""
|
||||||
|
<h2>Bonjour %s,</h2>
|
||||||
|
|
||||||
|
<p>Merci d'avoir réalisé l'audit de maturité digitale avec Lions Dev.</p>
|
||||||
|
|
||||||
|
<p><strong>Votre score global: %.1f%%</strong></p>
|
||||||
|
|
||||||
|
<p>Vous trouverez en pièce jointe votre rapport détaillé avec nos recommandations personnalisées.</p>
|
||||||
|
|
||||||
|
<p>Notre équipe vous contactera dans les 24h pour planifier un rendez-vous diagnostic gratuit.</p>
|
||||||
|
|
||||||
|
<p>Cordialement,<br>
|
||||||
|
L'équipe Lions Dev<br>
|
||||||
|
+225 01 01 75 95 25</p>
|
||||||
|
""", audit.getContactName(), audit.getMaturityPercentage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Méthodes utilitaires
|
||||||
|
private void addTableRow(PdfPTable table, String label, String value) {
|
||||||
|
table.addCell(new PdfPCell(new Phrase(label, NORMAL_FONT)));
|
||||||
|
table.addCell(new PdfPCell(new Phrase(value != null ? value : "-", NORMAL_FONT)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addTableHeader(PdfPTable table, String header) {
|
||||||
|
PdfPCell cell = new PdfPCell(new Phrase(header, HEADER_FONT));
|
||||||
|
cell.setBackgroundColor(BaseColor.LIGHT_GRAY);
|
||||||
|
table.addCell(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getMaturityLevel(double percentage) {
|
||||||
|
if (percentage < 30) return "Débutant";
|
||||||
|
if (percentage < 60) return "Intermédiaire";
|
||||||
|
if (percentage < 80) return "Avancé";
|
||||||
|
return "Expert";
|
||||||
|
}
|
||||||
|
|
||||||
|
private BaseColor getScoreColor(double percentage) {
|
||||||
|
if (percentage < 30) return BaseColor.RED;
|
||||||
|
if (percentage < 60) return BaseColor.ORANGE;
|
||||||
|
if (percentage < 80) return BaseColor.BLUE;
|
||||||
|
return BaseColor.GREEN;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getCategoryDisplayName(String category) {
|
||||||
|
return switch (category) {
|
||||||
|
case "commercial" -> "Gestion Commerciale";
|
||||||
|
case "stock" -> "Gestion des Stocks";
|
||||||
|
case "comptabilite" -> "Comptabilité";
|
||||||
|
case "rh" -> "Ressources Humaines";
|
||||||
|
case "infrastructure" -> "Infrastructure IT";
|
||||||
|
default -> category;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addCategoryChart(Document document, AuditResponse audit) throws DocumentException {
|
||||||
|
// Graphique simple en texte (pour une vraie implémentation, utiliser JFreeChart)
|
||||||
|
Paragraph chart = new Paragraph("Répartition des scores par domaine:", NORMAL_FONT);
|
||||||
|
chart.setSpacingAfter(5);
|
||||||
|
document.add(chart);
|
||||||
|
|
||||||
|
for (Map.Entry<String, Integer> entry : audit.getCategoryScores().entrySet()) {
|
||||||
|
String category = getCategoryDisplayName(entry.getKey());
|
||||||
|
Integer score = entry.getValue();
|
||||||
|
String bar = "█".repeat(Math.max(1, score / 10)) + " " + score + "%";
|
||||||
|
|
||||||
|
Paragraph barChart = new Paragraph(category + ": " + bar, SMALL_FONT);
|
||||||
|
document.add(barChart);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.add(Chunk.NEWLINE);
|
||||||
|
}
|
||||||
|
}
|
||||||
198
src/main/java/dev/lions/audit/AuditResource.java
Normal file
198
src/main/java/dev/lions/audit/AuditResource.java
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
package dev.lions.audit;
|
||||||
|
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.ws.rs.*;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
import jakarta.ws.rs.core.Response;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API REST pour l'outil d'audit de maturité digitale
|
||||||
|
*/
|
||||||
|
@Path("/api/audit")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
public class AuditResource {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
AuditService auditService;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
AuditReportService reportService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère toutes les questions d'audit par catégorie
|
||||||
|
*/
|
||||||
|
@GET
|
||||||
|
@Path("/questions")
|
||||||
|
public Response getQuestions(@QueryParam("lang") @DefaultValue("fr") String language) {
|
||||||
|
try {
|
||||||
|
Map<String, List<AuditQuestion>> questions = auditService.getQuestionsByCategory();
|
||||||
|
return Response.ok(questions).build();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||||
|
.entity(Map.of("error", "Erreur lors du chargement des questions"))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Soumet les réponses d'audit et génère le rapport
|
||||||
|
*/
|
||||||
|
@POST
|
||||||
|
@Path("/submit")
|
||||||
|
public Response submitAudit(AuditSubmissionDTO submission) {
|
||||||
|
try {
|
||||||
|
// Validation des données
|
||||||
|
if (submission.getCompanyName() == null || submission.getCompanyName().trim().isEmpty()) {
|
||||||
|
return Response.status(Response.Status.BAD_REQUEST)
|
||||||
|
.entity(Map.of("error", "Le nom de l'entreprise est requis"))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (submission.getEmail() == null || !isValidEmail(submission.getEmail())) {
|
||||||
|
return Response.status(Response.Status.BAD_REQUEST)
|
||||||
|
.entity(Map.of("error", "Email valide requis"))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Création de la réponse d'audit
|
||||||
|
AuditResponse response = new AuditResponse();
|
||||||
|
response.setCompanyName(submission.getCompanyName());
|
||||||
|
response.setContactName(submission.getContactName());
|
||||||
|
response.setEmail(submission.getEmail());
|
||||||
|
response.setPhone(submission.getPhone());
|
||||||
|
response.setSector(submission.getSector());
|
||||||
|
response.setEmployeeCount(submission.getEmployeeCount());
|
||||||
|
response.setTurnover(submission.getTurnover());
|
||||||
|
response.setAnswers(submission.getAnswers());
|
||||||
|
|
||||||
|
// Traitement de l'audit
|
||||||
|
AuditResponse processedResponse = auditService.processAuditResponse(response);
|
||||||
|
|
||||||
|
// Génération du rapport PDF
|
||||||
|
byte[] pdfReport = reportService.generateAuditReport(processedResponse);
|
||||||
|
|
||||||
|
// Envoi par email
|
||||||
|
reportService.sendAuditReportByEmail(processedResponse, pdfReport);
|
||||||
|
|
||||||
|
// Réponse avec résumé
|
||||||
|
AuditResultDTO result = new AuditResultDTO();
|
||||||
|
result.setAuditId(processedResponse.getId());
|
||||||
|
result.setMaturityPercentage(processedResponse.getMaturityPercentage());
|
||||||
|
result.setCategoryScores(processedResponse.getCategoryScores());
|
||||||
|
result.setRecommendations(processedResponse.getRecommendations());
|
||||||
|
result.setPriorityActions(processedResponse.getPriorityActions());
|
||||||
|
result.setEstimatedBudgetMin(processedResponse.getEstimatedBudgetMin());
|
||||||
|
result.setEstimatedBudgetMax(processedResponse.getEstimatedBudgetMax());
|
||||||
|
|
||||||
|
return Response.ok(result).build();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||||
|
.entity(Map.of("error", "Erreur lors du traitement de l'audit: " + e.getMessage()))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Télécharge le rapport PDF d'un audit
|
||||||
|
*/
|
||||||
|
@GET
|
||||||
|
@Path("/report/{auditId}")
|
||||||
|
@Produces("application/pdf")
|
||||||
|
public Response downloadReport(@PathParam("auditId") Long auditId) {
|
||||||
|
try {
|
||||||
|
AuditResponse audit = auditService.getAuditById(auditId);
|
||||||
|
if (audit == null) {
|
||||||
|
return Response.status(Response.Status.NOT_FOUND)
|
||||||
|
.entity("Audit non trouvé")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] pdfReport = reportService.generateAuditReport(audit);
|
||||||
|
|
||||||
|
return Response.ok(pdfReport)
|
||||||
|
.header("Content-Disposition",
|
||||||
|
"attachment; filename=\"audit-" + audit.getCompanyName() + ".pdf\"")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||||
|
.entity("Erreur lors de la génération du rapport")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les statistiques d'audit pour le dashboard admin
|
||||||
|
*/
|
||||||
|
@GET
|
||||||
|
@Path("/stats")
|
||||||
|
public Response getAuditStats() {
|
||||||
|
try {
|
||||||
|
Map<String, Object> stats = auditService.getAuditStatistics();
|
||||||
|
return Response.ok(stats).build();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||||
|
.entity(Map.of("error", "Erreur lors du chargement des statistiques"))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les audits non contactés (pour équipe commerciale)
|
||||||
|
*/
|
||||||
|
@GET
|
||||||
|
@Path("/leads")
|
||||||
|
public Response getUncontactedLeads() {
|
||||||
|
try {
|
||||||
|
List<AuditResponse> leads = auditService.getUncontactedAudits();
|
||||||
|
return Response.ok(leads).build();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||||
|
.entity(Map.of("error", "Erreur lors du chargement des leads"))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marque un audit comme contacté
|
||||||
|
*/
|
||||||
|
@PUT
|
||||||
|
@Path("/contact/{auditId}")
|
||||||
|
public Response markAsContacted(@PathParam("auditId") Long auditId,
|
||||||
|
Map<String, String> notes) {
|
||||||
|
try {
|
||||||
|
auditService.markAsContacted(auditId, notes.get("notes"));
|
||||||
|
return Response.ok(Map.of("success", true)).build();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||||
|
.entity(Map.of("error", "Erreur lors de la mise à jour"))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Demande de rendez-vous après audit
|
||||||
|
*/
|
||||||
|
@POST
|
||||||
|
@Path("/request-meeting")
|
||||||
|
public Response requestMeeting(MeetingRequestDTO request) {
|
||||||
|
try {
|
||||||
|
auditService.processMeetingRequest(request);
|
||||||
|
return Response.ok(Map.of("success", true,
|
||||||
|
"message", "Demande de rendez-vous enregistrée"))
|
||||||
|
.build();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||||
|
.entity(Map.of("error", "Erreur lors de l'enregistrement"))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isValidEmail(String email) {
|
||||||
|
return email != null && email.matches("^[A-Za-z0-9+_.-]+@(.+)$");
|
||||||
|
}
|
||||||
|
}
|
||||||
145
src/main/java/dev/lions/audit/AuditResponse.java
Normal file
145
src/main/java/dev/lions/audit/AuditResponse.java
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
package dev.lions.audit;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Réponse d'audit d'une PME avec scoring et recommandations
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "audit_responses")
|
||||||
|
public class AuditResponse {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
// Informations entreprise
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String companyName;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String contactName;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String email;
|
||||||
|
|
||||||
|
private String phone;
|
||||||
|
private String sector; // Secteur d'activité
|
||||||
|
private Integer employeeCount;
|
||||||
|
private String turnover; // Chiffre d'affaires
|
||||||
|
|
||||||
|
// Audit
|
||||||
|
@ElementCollection
|
||||||
|
@CollectionTable(name = "audit_answers")
|
||||||
|
@MapKeyColumn(name = "question_id")
|
||||||
|
@Column(name = "answer_index")
|
||||||
|
private Map<Long, Integer> answers; // questionId -> index de réponse
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private LocalDateTime submittedAt;
|
||||||
|
|
||||||
|
// Scoring
|
||||||
|
private Integer totalScore;
|
||||||
|
private Integer maxPossibleScore;
|
||||||
|
private Double maturityPercentage;
|
||||||
|
|
||||||
|
@ElementCollection
|
||||||
|
@CollectionTable(name = "audit_category_scores")
|
||||||
|
@MapKeyColumn(name = "category")
|
||||||
|
@Column(name = "score")
|
||||||
|
private Map<String, Integer> categoryScores;
|
||||||
|
|
||||||
|
// Recommandations
|
||||||
|
@Column(length = 2000)
|
||||||
|
private String recommendations;
|
||||||
|
|
||||||
|
@Column(length = 1000)
|
||||||
|
private String priorityActions;
|
||||||
|
|
||||||
|
// Estimation budgétaire
|
||||||
|
private Double estimatedBudgetMin;
|
||||||
|
private Double estimatedBudgetMax;
|
||||||
|
|
||||||
|
// Suivi commercial
|
||||||
|
private Boolean contacted = false;
|
||||||
|
private LocalDateTime contactedAt;
|
||||||
|
private String salesNotes;
|
||||||
|
|
||||||
|
// Constructeurs
|
||||||
|
public AuditResponse() {
|
||||||
|
this.submittedAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
public AuditResponse(String companyName, String contactName, String email) {
|
||||||
|
this();
|
||||||
|
this.companyName = companyName;
|
||||||
|
this.contactName = contactName;
|
||||||
|
this.email = email;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters et Setters
|
||||||
|
public Long getId() { return id; }
|
||||||
|
public void setId(Long id) { this.id = id; }
|
||||||
|
|
||||||
|
public String getCompanyName() { return companyName; }
|
||||||
|
public void setCompanyName(String companyName) { this.companyName = companyName; }
|
||||||
|
|
||||||
|
public String getContactName() { return contactName; }
|
||||||
|
public void setContactName(String contactName) { this.contactName = contactName; }
|
||||||
|
|
||||||
|
public String getEmail() { return email; }
|
||||||
|
public void setEmail(String email) { this.email = email; }
|
||||||
|
|
||||||
|
public String getPhone() { return phone; }
|
||||||
|
public void setPhone(String phone) { this.phone = phone; }
|
||||||
|
|
||||||
|
public String getSector() { return sector; }
|
||||||
|
public void setSector(String sector) { this.sector = sector; }
|
||||||
|
|
||||||
|
public Integer getEmployeeCount() { return employeeCount; }
|
||||||
|
public void setEmployeeCount(Integer employeeCount) { this.employeeCount = employeeCount; }
|
||||||
|
|
||||||
|
public String getTurnover() { return turnover; }
|
||||||
|
public void setTurnover(String turnover) { this.turnover = turnover; }
|
||||||
|
|
||||||
|
public Map<Long, Integer> getAnswers() { return answers; }
|
||||||
|
public void setAnswers(Map<Long, Integer> answers) { this.answers = answers; }
|
||||||
|
|
||||||
|
public LocalDateTime getSubmittedAt() { return submittedAt; }
|
||||||
|
public void setSubmittedAt(LocalDateTime submittedAt) { this.submittedAt = submittedAt; }
|
||||||
|
|
||||||
|
public Integer getTotalScore() { return totalScore; }
|
||||||
|
public void setTotalScore(Integer totalScore) { this.totalScore = totalScore; }
|
||||||
|
|
||||||
|
public Integer getMaxPossibleScore() { return maxPossibleScore; }
|
||||||
|
public void setMaxPossibleScore(Integer maxPossibleScore) { this.maxPossibleScore = maxPossibleScore; }
|
||||||
|
|
||||||
|
public Double getMaturityPercentage() { return maturityPercentage; }
|
||||||
|
public void setMaturityPercentage(Double maturityPercentage) { this.maturityPercentage = maturityPercentage; }
|
||||||
|
|
||||||
|
public Map<String, Integer> getCategoryScores() { return categoryScores; }
|
||||||
|
public void setCategoryScores(Map<String, Integer> categoryScores) { this.categoryScores = categoryScores; }
|
||||||
|
|
||||||
|
public String getRecommendations() { return recommendations; }
|
||||||
|
public void setRecommendations(String recommendations) { this.recommendations = recommendations; }
|
||||||
|
|
||||||
|
public String getPriorityActions() { return priorityActions; }
|
||||||
|
public void setPriorityActions(String priorityActions) { this.priorityActions = priorityActions; }
|
||||||
|
|
||||||
|
public Double getEstimatedBudgetMin() { return estimatedBudgetMin; }
|
||||||
|
public void setEstimatedBudgetMin(Double estimatedBudgetMin) { this.estimatedBudgetMin = estimatedBudgetMin; }
|
||||||
|
|
||||||
|
public Double getEstimatedBudgetMax() { return estimatedBudgetMax; }
|
||||||
|
public void setEstimatedBudgetMax(Double estimatedBudgetMax) { this.estimatedBudgetMax = estimatedBudgetMax; }
|
||||||
|
|
||||||
|
public Boolean getContacted() { return contacted; }
|
||||||
|
public void setContacted(Boolean contacted) { this.contacted = contacted; }
|
||||||
|
|
||||||
|
public LocalDateTime getContactedAt() { return contactedAt; }
|
||||||
|
public void setContactedAt(LocalDateTime contactedAt) { this.contactedAt = contactedAt; }
|
||||||
|
|
||||||
|
public String getSalesNotes() { return salesNotes; }
|
||||||
|
public void setSalesNotes(String salesNotes) { this.salesNotes = salesNotes; }
|
||||||
|
}
|
||||||
247
src/main/java/dev/lions/audit/AuditService.java
Normal file
247
src/main/java/dev/lions/audit/AuditService.java
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
package dev.lions.audit;
|
||||||
|
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.persistence.EntityManager;
|
||||||
|
import jakarta.transaction.Transactional;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service de gestion des audits de maturité digitale
|
||||||
|
*/
|
||||||
|
@ApplicationScoped
|
||||||
|
public class AuditService {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
EntityManager em;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère toutes les questions d'audit actives par catégorie
|
||||||
|
*/
|
||||||
|
public Map<String, List<AuditQuestion>> getQuestionsByCategory() {
|
||||||
|
List<AuditQuestion> questions = em.createQuery(
|
||||||
|
"SELECT q FROM AuditQuestion q WHERE q.active = true ORDER BY q.displayOrder",
|
||||||
|
AuditQuestion.class
|
||||||
|
).getResultList();
|
||||||
|
|
||||||
|
return questions.stream()
|
||||||
|
.collect(Collectors.groupingBy(AuditQuestion::getCategory));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calcule le score d'un audit et génère les recommandations
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public AuditResponse processAuditResponse(AuditResponse response) {
|
||||||
|
Map<String, List<AuditQuestion>> questionsByCategory = getQuestionsByCategory();
|
||||||
|
Map<String, Integer> categoryScores = new HashMap<>();
|
||||||
|
Map<String, Integer> categoryMaxScores = new HashMap<>();
|
||||||
|
|
||||||
|
int totalScore = 0;
|
||||||
|
int maxPossibleScore = 0;
|
||||||
|
|
||||||
|
// Calcul des scores par catégorie
|
||||||
|
for (Map.Entry<String, List<AuditQuestion>> entry : questionsByCategory.entrySet()) {
|
||||||
|
String category = entry.getKey();
|
||||||
|
List<AuditQuestion> questions = entry.getValue();
|
||||||
|
|
||||||
|
int categoryScore = 0;
|
||||||
|
int categoryMaxScore = 0;
|
||||||
|
|
||||||
|
for (AuditQuestion question : questions) {
|
||||||
|
Integer answerIndex = response.getAnswers().get(question.getId());
|
||||||
|
if (answerIndex != null && answerIndex < question.getScores().size()) {
|
||||||
|
int questionScore = question.getScores().get(answerIndex) * question.getWeight();
|
||||||
|
categoryScore += questionScore;
|
||||||
|
totalScore += questionScore;
|
||||||
|
}
|
||||||
|
|
||||||
|
int maxQuestionScore = Collections.max(question.getScores()) * question.getWeight();
|
||||||
|
categoryMaxScore += maxQuestionScore;
|
||||||
|
maxPossibleScore += maxQuestionScore;
|
||||||
|
}
|
||||||
|
|
||||||
|
categoryScores.put(category, categoryScore);
|
||||||
|
categoryMaxScores.put(category, categoryMaxScore);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mise à jour des scores
|
||||||
|
response.setTotalScore(totalScore);
|
||||||
|
response.setMaxPossibleScore(maxPossibleScore);
|
||||||
|
response.setMaturityPercentage((double) totalScore / maxPossibleScore * 100);
|
||||||
|
response.setCategoryScores(categoryScores);
|
||||||
|
|
||||||
|
// Génération des recommandations
|
||||||
|
generateRecommendations(response, categoryScores, categoryMaxScores);
|
||||||
|
|
||||||
|
// Estimation budgétaire
|
||||||
|
estimateBudget(response, categoryScores);
|
||||||
|
|
||||||
|
// Sauvegarde
|
||||||
|
em.persist(response);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère les recommandations personnalisées
|
||||||
|
*/
|
||||||
|
private void generateRecommendations(AuditResponse response,
|
||||||
|
Map<String, Integer> categoryScores,
|
||||||
|
Map<String, Integer> categoryMaxScores) {
|
||||||
|
|
||||||
|
StringBuilder recommendations = new StringBuilder();
|
||||||
|
List<String> priorities = new ArrayList<>();
|
||||||
|
|
||||||
|
// Analyse par catégorie
|
||||||
|
for (Map.Entry<String, Integer> entry : categoryScores.entrySet()) {
|
||||||
|
String category = entry.getKey();
|
||||||
|
int score = entry.getValue();
|
||||||
|
int maxScore = categoryMaxScores.get(category);
|
||||||
|
double percentage = (double) score / maxScore * 100;
|
||||||
|
|
||||||
|
String categoryName = getCategoryDisplayName(category);
|
||||||
|
|
||||||
|
if (percentage < 30) {
|
||||||
|
recommendations.append("🚨 ").append(categoryName).append(" : Niveau critique - Digitalisation urgente nécessaire\n");
|
||||||
|
priorities.add("Digitaliser " + categoryName.toLowerCase());
|
||||||
|
} else if (percentage < 60) {
|
||||||
|
recommendations.append("⚠️ ").append(categoryName).append(" : Niveau faible - Améliorations importantes recommandées\n");
|
||||||
|
priorities.add("Améliorer " + categoryName.toLowerCase());
|
||||||
|
} else if (percentage < 80) {
|
||||||
|
recommendations.append("✅ ").append(categoryName).append(" : Niveau correct - Optimisations possibles\n");
|
||||||
|
} else {
|
||||||
|
recommendations.append("🏆 ").append(categoryName).append(" : Excellent niveau de digitalisation\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response.setRecommendations(recommendations.toString());
|
||||||
|
response.setPriorityActions(String.join(", ", priorities));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Estime le budget nécessaire selon les scores
|
||||||
|
*/
|
||||||
|
private void estimateBudget(AuditResponse response, Map<String, Integer> categoryScores) {
|
||||||
|
double budgetMin = 0;
|
||||||
|
double budgetMax = 0;
|
||||||
|
|
||||||
|
// Tarification par module selon le niveau de maturité
|
||||||
|
Map<String, Double[]> modulePricing = Map.of(
|
||||||
|
"commercial", new Double[]{150000.0, 300000.0}, // CRM
|
||||||
|
"stock", new Double[]{100000.0, 250000.0}, // Gestion stocks
|
||||||
|
"comptabilite", new Double[]{200000.0, 400000.0}, // Comptabilité
|
||||||
|
"rh", new Double[]{80000.0, 200000.0}, // RH
|
||||||
|
"infrastructure", new Double[]{100000.0, 300000.0} // IT
|
||||||
|
);
|
||||||
|
|
||||||
|
for (Map.Entry<String, Integer> entry : categoryScores.entrySet()) {
|
||||||
|
String category = entry.getKey();
|
||||||
|
int score = entry.getValue();
|
||||||
|
|
||||||
|
if (score < 50 && modulePricing.containsKey(category)) { // Besoin d'amélioration
|
||||||
|
Double[] pricing = modulePricing.get(category);
|
||||||
|
budgetMin += pricing[0];
|
||||||
|
budgetMax += pricing[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response.setEstimatedBudgetMin(budgetMin);
|
||||||
|
response.setEstimatedBudgetMax(budgetMax);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les audits non contactés pour le suivi commercial
|
||||||
|
*/
|
||||||
|
public List<AuditResponse> getUncontactedAudits() {
|
||||||
|
return em.createQuery(
|
||||||
|
"SELECT a FROM AuditResponse a WHERE a.contacted = false ORDER BY a.submittedAt DESC",
|
||||||
|
AuditResponse.class
|
||||||
|
).getResultList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marque un audit comme contacté
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public void markAsContacted(Long auditId, String notes) {
|
||||||
|
AuditResponse audit = em.find(AuditResponse.class, auditId);
|
||||||
|
if (audit != null) {
|
||||||
|
audit.setContacted(true);
|
||||||
|
audit.setContactedAt(java.time.LocalDateTime.now());
|
||||||
|
audit.setSalesNotes(notes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère un audit par ID
|
||||||
|
*/
|
||||||
|
public AuditResponse getAuditById(Long auditId) {
|
||||||
|
return em.find(AuditResponse.class, auditId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les statistiques d'audit
|
||||||
|
*/
|
||||||
|
public Map<String, Object> getAuditStatistics() {
|
||||||
|
Map<String, Object> stats = new HashMap<>();
|
||||||
|
|
||||||
|
// Nombre total d'audits
|
||||||
|
Long totalAudits = em.createQuery("SELECT COUNT(a) FROM AuditResponse a", Long.class)
|
||||||
|
.getSingleResult();
|
||||||
|
stats.put("totalAudits", totalAudits);
|
||||||
|
|
||||||
|
// Audits cette semaine
|
||||||
|
Long weeklyAudits = em.createQuery(
|
||||||
|
"SELECT COUNT(a) FROM AuditResponse a WHERE a.submittedAt >= :weekStart", Long.class)
|
||||||
|
.setParameter("weekStart", java.time.LocalDateTime.now().minusDays(7))
|
||||||
|
.getSingleResult();
|
||||||
|
stats.put("weeklyAudits", weeklyAudits);
|
||||||
|
|
||||||
|
// Score moyen
|
||||||
|
Double avgScore = em.createQuery(
|
||||||
|
"SELECT AVG(a.maturityPercentage) FROM AuditResponse a", Double.class)
|
||||||
|
.getSingleResult();
|
||||||
|
stats.put("averageScore", avgScore != null ? avgScore : 0.0);
|
||||||
|
|
||||||
|
// Répartition par secteur
|
||||||
|
List<Object[]> sectorStats = em.createQuery(
|
||||||
|
"SELECT a.sector, COUNT(a) FROM AuditResponse a GROUP BY a.sector", Object[].class)
|
||||||
|
.getResultList();
|
||||||
|
Map<String, Long> sectorDistribution = new HashMap<>();
|
||||||
|
for (Object[] row : sectorStats) {
|
||||||
|
sectorDistribution.put((String) row[0], (Long) row[1]);
|
||||||
|
}
|
||||||
|
stats.put("sectorDistribution", sectorDistribution);
|
||||||
|
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Traite une demande de rendez-vous
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public void processMeetingRequest(MeetingRequestDTO request) {
|
||||||
|
// Ici on pourrait créer une entité MeetingRequest
|
||||||
|
// Pour l'instant, on met à jour les notes de l'audit
|
||||||
|
AuditResponse audit = em.find(AuditResponse.class, request.getAuditId());
|
||||||
|
if (audit != null) {
|
||||||
|
String meetingNote = String.format("RDV demandé: %s à %s (%s) - %s",
|
||||||
|
request.getPreferredDate(), request.getPreferredTime(),
|
||||||
|
request.getMeetingType(), request.getMessage());
|
||||||
|
audit.setSalesNotes(meetingNote);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getCategoryDisplayName(String category) {
|
||||||
|
return switch (category) {
|
||||||
|
case "commercial" -> "Gestion Commerciale";
|
||||||
|
case "stock" -> "Gestion des Stocks";
|
||||||
|
case "comptabilite" -> "Comptabilité";
|
||||||
|
case "rh" -> "Ressources Humaines";
|
||||||
|
case "infrastructure" -> "Infrastructure IT";
|
||||||
|
default -> category;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
129
src/main/java/dev/lions/audit/AuditSubmissionDTO.java
Normal file
129
src/main/java/dev/lions/audit/AuditSubmissionDTO.java
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
package dev.lions.audit;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO pour la soumission d'un audit
|
||||||
|
*/
|
||||||
|
public class AuditSubmissionDTO {
|
||||||
|
|
||||||
|
// Informations entreprise
|
||||||
|
private String companyName;
|
||||||
|
private String contactName;
|
||||||
|
private String email;
|
||||||
|
private String phone;
|
||||||
|
private String sector;
|
||||||
|
private Integer employeeCount;
|
||||||
|
private String turnover;
|
||||||
|
|
||||||
|
// Réponses aux questions
|
||||||
|
private Map<Long, Integer> answers; // questionId -> answerIndex
|
||||||
|
|
||||||
|
// Préférences
|
||||||
|
private String preferredContactTime;
|
||||||
|
private String additionalComments;
|
||||||
|
private Boolean acceptsMarketing = false;
|
||||||
|
|
||||||
|
// Constructeurs
|
||||||
|
public AuditSubmissionDTO() {}
|
||||||
|
|
||||||
|
// Getters et Setters
|
||||||
|
public String getCompanyName() { return companyName; }
|
||||||
|
public void setCompanyName(String companyName) { this.companyName = companyName; }
|
||||||
|
|
||||||
|
public String getContactName() { return contactName; }
|
||||||
|
public void setContactName(String contactName) { this.contactName = contactName; }
|
||||||
|
|
||||||
|
public String getEmail() { return email; }
|
||||||
|
public void setEmail(String email) { this.email = email; }
|
||||||
|
|
||||||
|
public String getPhone() { return phone; }
|
||||||
|
public void setPhone(String phone) { this.phone = phone; }
|
||||||
|
|
||||||
|
public String getSector() { return sector; }
|
||||||
|
public void setSector(String sector) { this.sector = sector; }
|
||||||
|
|
||||||
|
public Integer getEmployeeCount() { return employeeCount; }
|
||||||
|
public void setEmployeeCount(Integer employeeCount) { this.employeeCount = employeeCount; }
|
||||||
|
|
||||||
|
public String getTurnover() { return turnover; }
|
||||||
|
public void setTurnover(String turnover) { this.turnover = turnover; }
|
||||||
|
|
||||||
|
public Map<Long, Integer> getAnswers() { return answers; }
|
||||||
|
public void setAnswers(Map<Long, Integer> answers) { this.answers = answers; }
|
||||||
|
|
||||||
|
public String getPreferredContactTime() { return preferredContactTime; }
|
||||||
|
public void setPreferredContactTime(String preferredContactTime) { this.preferredContactTime = preferredContactTime; }
|
||||||
|
|
||||||
|
public String getAdditionalComments() { return additionalComments; }
|
||||||
|
public void setAdditionalComments(String additionalComments) { this.additionalComments = additionalComments; }
|
||||||
|
|
||||||
|
public Boolean getAcceptsMarketing() { return acceptsMarketing; }
|
||||||
|
public void setAcceptsMarketing(Boolean acceptsMarketing) { this.acceptsMarketing = acceptsMarketing; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO pour le résultat d'audit
|
||||||
|
*/
|
||||||
|
class AuditResultDTO {
|
||||||
|
private Long auditId;
|
||||||
|
private Double maturityPercentage;
|
||||||
|
private Map<String, Integer> categoryScores;
|
||||||
|
private String recommendations;
|
||||||
|
private String priorityActions;
|
||||||
|
private Double estimatedBudgetMin;
|
||||||
|
private Double estimatedBudgetMax;
|
||||||
|
private String nextSteps;
|
||||||
|
|
||||||
|
// Getters et Setters
|
||||||
|
public Long getAuditId() { return auditId; }
|
||||||
|
public void setAuditId(Long auditId) { this.auditId = auditId; }
|
||||||
|
|
||||||
|
public Double getMaturityPercentage() { return maturityPercentage; }
|
||||||
|
public void setMaturityPercentage(Double maturityPercentage) { this.maturityPercentage = maturityPercentage; }
|
||||||
|
|
||||||
|
public Map<String, Integer> getCategoryScores() { return categoryScores; }
|
||||||
|
public void setCategoryScores(Map<String, Integer> categoryScores) { this.categoryScores = categoryScores; }
|
||||||
|
|
||||||
|
public String getRecommendations() { return recommendations; }
|
||||||
|
public void setRecommendations(String recommendations) { this.recommendations = recommendations; }
|
||||||
|
|
||||||
|
public String getPriorityActions() { return priorityActions; }
|
||||||
|
public void setPriorityActions(String priorityActions) { this.priorityActions = priorityActions; }
|
||||||
|
|
||||||
|
public Double getEstimatedBudgetMin() { return estimatedBudgetMin; }
|
||||||
|
public void setEstimatedBudgetMin(Double estimatedBudgetMin) { this.estimatedBudgetMin = estimatedBudgetMin; }
|
||||||
|
|
||||||
|
public Double getEstimatedBudgetMax() { return estimatedBudgetMax; }
|
||||||
|
public void setEstimatedBudgetMax(Double estimatedBudgetMax) { this.estimatedBudgetMax = estimatedBudgetMax; }
|
||||||
|
|
||||||
|
public String getNextSteps() { return nextSteps; }
|
||||||
|
public void setNextSteps(String nextSteps) { this.nextSteps = nextSteps; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO pour demande de rendez-vous
|
||||||
|
*/
|
||||||
|
class MeetingRequestDTO {
|
||||||
|
private Long auditId;
|
||||||
|
private String preferredDate;
|
||||||
|
private String preferredTime;
|
||||||
|
private String meetingType; // "phone", "video", "onsite"
|
||||||
|
private String message;
|
||||||
|
|
||||||
|
// Getters et Setters
|
||||||
|
public Long getAuditId() { return auditId; }
|
||||||
|
public void setAuditId(Long auditId) { this.auditId = auditId; }
|
||||||
|
|
||||||
|
public String getPreferredDate() { return preferredDate; }
|
||||||
|
public void setPreferredDate(String preferredDate) { this.preferredDate = preferredDate; }
|
||||||
|
|
||||||
|
public String getPreferredTime() { return preferredTime; }
|
||||||
|
public void setPreferredTime(String preferredTime) { this.preferredTime = preferredTime; }
|
||||||
|
|
||||||
|
public String getMeetingType() { return meetingType; }
|
||||||
|
public void setMeetingType(String meetingType) { this.meetingType = meetingType; }
|
||||||
|
|
||||||
|
public String getMessage() { return message; }
|
||||||
|
public void setMessage(String message) { this.message = message; }
|
||||||
|
}
|
||||||
534
src/main/java/dev/lions/compliance/IvorianTaxService.java
Normal file
534
src/main/java/dev/lions/compliance/IvorianTaxService.java
Normal file
@@ -0,0 +1,534 @@
|
|||||||
|
package dev.lions.compliance;
|
||||||
|
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.YearMonth;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service de conformité fiscale ivoirienne
|
||||||
|
*/
|
||||||
|
@ApplicationScoped
|
||||||
|
public class IvorianTaxService {
|
||||||
|
|
||||||
|
// Taux de TVA en Côte d'Ivoire
|
||||||
|
public static final double TVA_RATE = 18.0; // 18%
|
||||||
|
public static final double TVA_REDUCED_RATE = 9.0; // 9% pour certains produits
|
||||||
|
|
||||||
|
// Taux d'impôt sur les sociétés
|
||||||
|
public static final double IS_RATE = 25.0; // 25%
|
||||||
|
public static final double IS_REDUCED_RATE = 20.0; // 20% pour PME
|
||||||
|
|
||||||
|
// Seuils PME
|
||||||
|
public static final double PME_TURNOVER_THRESHOLD = 200_000_000; // 200M FCFA
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calcule la TVA sur un montant
|
||||||
|
*/
|
||||||
|
public TaxCalculation calculateTVA(double amountHT, boolean reducedRate) {
|
||||||
|
double rate = reducedRate ? TVA_REDUCED_RATE : TVA_RATE;
|
||||||
|
double tvaAmount = amountHT * (rate / 100);
|
||||||
|
double amountTTC = amountHT + tvaAmount;
|
||||||
|
|
||||||
|
TaxCalculation calculation = new TaxCalculation();
|
||||||
|
calculation.setAmountHT(amountHT);
|
||||||
|
calculation.setTaxRate(rate);
|
||||||
|
calculation.setTaxAmount(tvaAmount);
|
||||||
|
calculation.setAmountTTC(amountTTC);
|
||||||
|
calculation.setTaxType("TVA");
|
||||||
|
|
||||||
|
return calculation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calcule l'impôt sur les sociétés
|
||||||
|
*/
|
||||||
|
public TaxCalculation calculateIS(double annualProfit, boolean isPME) {
|
||||||
|
double rate = isPME ? IS_REDUCED_RATE : IS_RATE;
|
||||||
|
double isAmount = annualProfit * (rate / 100);
|
||||||
|
|
||||||
|
TaxCalculation calculation = new TaxCalculation();
|
||||||
|
calculation.setAmountHT(annualProfit);
|
||||||
|
calculation.setTaxRate(rate);
|
||||||
|
calculation.setTaxAmount(isAmount);
|
||||||
|
calculation.setAmountTTC(annualProfit - isAmount); // Bénéfice net après IS
|
||||||
|
calculation.setTaxType("IS");
|
||||||
|
|
||||||
|
return calculation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère la déclaration TVA mensuelle
|
||||||
|
*/
|
||||||
|
public TVADeclaration generateTVADeclaration(String tenantId, YearMonth period,
|
||||||
|
List<TVATransaction> transactions) {
|
||||||
|
TVADeclaration declaration = new TVADeclaration();
|
||||||
|
declaration.setTenantId(tenantId);
|
||||||
|
declaration.setPeriod(period);
|
||||||
|
declaration.setDeclarationType("MENSUELLE");
|
||||||
|
|
||||||
|
double totalVentesHT = 0;
|
||||||
|
double totalTVACollectee = 0;
|
||||||
|
double totalAchatsHT = 0;
|
||||||
|
double totalTVADeductible = 0;
|
||||||
|
|
||||||
|
for (TVATransaction transaction : transactions) {
|
||||||
|
if (transaction.getType() == TransactionType.VENTE) {
|
||||||
|
totalVentesHT += transaction.getAmountHT();
|
||||||
|
totalTVACollectee += transaction.getTvaAmount();
|
||||||
|
} else if (transaction.getType() == TransactionType.ACHAT) {
|
||||||
|
totalAchatsHT += transaction.getAmountHT();
|
||||||
|
totalTVADeductible += transaction.getTvaAmount();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
double tvaAVerser = totalTVACollectee - totalTVADeductible;
|
||||||
|
|
||||||
|
declaration.setVentesHT(totalVentesHT);
|
||||||
|
declaration.setTvaCollectee(totalTVACollectee);
|
||||||
|
declaration.setAchatsHT(totalAchatsHT);
|
||||||
|
declaration.setTvaDeductible(totalTVADeductible);
|
||||||
|
declaration.setTvaAVerser(Math.max(0, tvaAVerser));
|
||||||
|
declaration.setCreditTVA(Math.max(0, -tvaAVerser));
|
||||||
|
|
||||||
|
return declaration;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère la déclaration IS annuelle
|
||||||
|
*/
|
||||||
|
public ISDeclaration generateISDeclaration(String tenantId, int year,
|
||||||
|
double chiffreAffaires, double charges,
|
||||||
|
double amortissements) {
|
||||||
|
ISDeclaration declaration = new ISDeclaration();
|
||||||
|
declaration.setTenantId(tenantId);
|
||||||
|
declaration.setYear(year);
|
||||||
|
|
||||||
|
double beneficeBrut = chiffreAffaires - charges;
|
||||||
|
double beneficeImposable = beneficeBrut - amortissements;
|
||||||
|
|
||||||
|
boolean isPME = chiffreAffaires <= PME_TURNOVER_THRESHOLD;
|
||||||
|
TaxCalculation isCalculation = calculateIS(beneficeImposable, isPME);
|
||||||
|
|
||||||
|
declaration.setChiffreAffaires(chiffreAffaires);
|
||||||
|
declaration.setCharges(charges);
|
||||||
|
declaration.setAmortissements(amortissements);
|
||||||
|
declaration.setBeneficeBrut(beneficeBrut);
|
||||||
|
declaration.setBeneficeImposable(beneficeImposable);
|
||||||
|
declaration.setTauxIS(isCalculation.getTaxRate());
|
||||||
|
declaration.setMontantIS(isCalculation.getTaxAmount());
|
||||||
|
declaration.setBeneficeNet(isCalculation.getAmountTTC());
|
||||||
|
declaration.setIsPME(isPME);
|
||||||
|
|
||||||
|
return declaration;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calcule les cotisations CNPS
|
||||||
|
*/
|
||||||
|
public CNPSCalculation calculateCNPS(double salaireBrut) {
|
||||||
|
CNPSCalculation calculation = new CNPSCalculation();
|
||||||
|
|
||||||
|
// Plafond CNPS (à ajuster selon les barèmes en vigueur)
|
||||||
|
double plafondCNPS = 1_800_000; // 1.8M FCFA par an
|
||||||
|
double salaireImposable = Math.min(salaireBrut, plafondCNPS);
|
||||||
|
|
||||||
|
// Taux CNPS
|
||||||
|
double tauxEmployeur = 16.75; // 16.75%
|
||||||
|
double tauxEmploye = 6.3; // 6.3%
|
||||||
|
|
||||||
|
double cotisationEmployeur = salaireImposable * (tauxEmployeur / 100);
|
||||||
|
double cotisationEmploye = salaireImposable * (tauxEmploye / 100);
|
||||||
|
double cotisationTotale = cotisationEmployeur + cotisationEmploye;
|
||||||
|
|
||||||
|
calculation.setSalaireBrut(salaireBrut);
|
||||||
|
calculation.setSalaireImposable(salaireImposable);
|
||||||
|
calculation.setTauxEmployeur(tauxEmployeur);
|
||||||
|
calculation.setTauxEmploye(tauxEmploye);
|
||||||
|
calculation.setCotisationEmployeur(cotisationEmployeur);
|
||||||
|
calculation.setCotisationEmploye(cotisationEmploye);
|
||||||
|
calculation.setCotisationTotale(cotisationTotale);
|
||||||
|
calculation.setSalaireNet(salaireBrut - cotisationEmploye);
|
||||||
|
|
||||||
|
return calculation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie la conformité d'une entreprise
|
||||||
|
*/
|
||||||
|
public ComplianceReport checkCompliance(String tenantId) {
|
||||||
|
ComplianceReport report = new ComplianceReport();
|
||||||
|
report.setTenantId(tenantId);
|
||||||
|
report.setCheckDate(LocalDate.now());
|
||||||
|
|
||||||
|
List<ComplianceIssue> issues = new ArrayList<>();
|
||||||
|
|
||||||
|
// Vérifications de base
|
||||||
|
if (!hasValidTaxNumber(tenantId)) {
|
||||||
|
issues.add(new ComplianceIssue("TAX_NUMBER", "Numéro contribuable DGI manquant", "HIGH"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasValidCNPSNumber(tenantId)) {
|
||||||
|
issues.add(new ComplianceIssue("CNPS_NUMBER", "Numéro CNPS manquant", "HIGH"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasValidRCCM(tenantId)) {
|
||||||
|
issues.add(new ComplianceIssue("RCCM", "Numéro RCCM manquant", "MEDIUM"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifications déclarations
|
||||||
|
if (!hasRecentTVADeclaration(tenantId)) {
|
||||||
|
issues.add(new ComplianceIssue("TVA_DECLARATION", "Déclaration TVA en retard", "HIGH"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasRecentISDeclaration(tenantId)) {
|
||||||
|
issues.add(new ComplianceIssue("IS_DECLARATION", "Déclaration IS en retard", "HIGH"));
|
||||||
|
}
|
||||||
|
|
||||||
|
report.setIssues(issues);
|
||||||
|
report.setComplianceScore(calculateComplianceScore(issues));
|
||||||
|
report.setStatus(issues.isEmpty() ? "COMPLIANT" : "NON_COMPLIANT");
|
||||||
|
|
||||||
|
return report;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère les échéances fiscales
|
||||||
|
*/
|
||||||
|
public List<TaxDeadline> getTaxDeadlines(int year) {
|
||||||
|
List<TaxDeadline> deadlines = new ArrayList<>();
|
||||||
|
|
||||||
|
// Déclarations TVA mensuelles (15 de chaque mois)
|
||||||
|
for (int month = 1; month <= 12; month++) {
|
||||||
|
deadlines.add(new TaxDeadline(
|
||||||
|
"TVA_MENSUELLE",
|
||||||
|
"Déclaration TVA " + getMonthName(month),
|
||||||
|
LocalDate.of(year, month, 15),
|
||||||
|
"HIGH"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Déclaration IS annuelle (30 avril)
|
||||||
|
deadlines.add(new TaxDeadline(
|
||||||
|
"IS_ANNUELLE",
|
||||||
|
"Déclaration Impôt sur les Sociétés " + year,
|
||||||
|
LocalDate.of(year + 1, 4, 30),
|
||||||
|
"HIGH"
|
||||||
|
));
|
||||||
|
|
||||||
|
// Déclarations CNPS trimestrielles
|
||||||
|
deadlines.add(new TaxDeadline("CNPS_T1", "Déclaration CNPS T1", LocalDate.of(year, 4, 15), "MEDIUM"));
|
||||||
|
deadlines.add(new TaxDeadline("CNPS_T2", "Déclaration CNPS T2", LocalDate.of(year, 7, 15), "MEDIUM"));
|
||||||
|
deadlines.add(new TaxDeadline("CNPS_T3", "Déclaration CNPS T3", LocalDate.of(year, 10, 15), "MEDIUM"));
|
||||||
|
deadlines.add(new TaxDeadline("CNPS_T4", "Déclaration CNPS T4", LocalDate.of(year + 1, 1, 15), "MEDIUM"));
|
||||||
|
|
||||||
|
return deadlines;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Méthodes utilitaires privées
|
||||||
|
private boolean hasValidTaxNumber(String tenantId) {
|
||||||
|
// Simulation - en réalité, vérifier en base
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasValidCNPSNumber(String tenantId) {
|
||||||
|
// Simulation - en réalité, vérifier en base
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasValidRCCM(String tenantId) {
|
||||||
|
// Simulation - en réalité, vérifier en base
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasRecentTVADeclaration(String tenantId) {
|
||||||
|
// Simulation - en réalité, vérifier les déclarations récentes
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasRecentISDeclaration(String tenantId) {
|
||||||
|
// Simulation - en réalité, vérifier les déclarations récentes
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private double calculateComplianceScore(List<ComplianceIssue> issues) {
|
||||||
|
if (issues.isEmpty()) return 100.0;
|
||||||
|
|
||||||
|
double penalty = 0;
|
||||||
|
for (ComplianceIssue issue : issues) {
|
||||||
|
switch (issue.getSeverity()) {
|
||||||
|
case "HIGH" -> penalty += 20;
|
||||||
|
case "MEDIUM" -> penalty += 10;
|
||||||
|
case "LOW" -> penalty += 5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.max(0, 100 - penalty);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getMonthName(int month) {
|
||||||
|
String[] months = {"", "Janvier", "Février", "Mars", "Avril", "Mai", "Juin",
|
||||||
|
"Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre"};
|
||||||
|
return months[month];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calcul fiscal
|
||||||
|
*/
|
||||||
|
class TaxCalculation {
|
||||||
|
private double amountHT;
|
||||||
|
private double taxRate;
|
||||||
|
private double taxAmount;
|
||||||
|
private double amountTTC;
|
||||||
|
private String taxType;
|
||||||
|
|
||||||
|
// Getters et Setters
|
||||||
|
public double getAmountHT() { return amountHT; }
|
||||||
|
public void setAmountHT(double amountHT) { this.amountHT = amountHT; }
|
||||||
|
|
||||||
|
public double getTaxRate() { return taxRate; }
|
||||||
|
public void setTaxRate(double taxRate) { this.taxRate = taxRate; }
|
||||||
|
|
||||||
|
public double getTaxAmount() { return taxAmount; }
|
||||||
|
public void setTaxAmount(double taxAmount) { this.taxAmount = taxAmount; }
|
||||||
|
|
||||||
|
public double getAmountTTC() { return amountTTC; }
|
||||||
|
public void setAmountTTC(double amountTTC) { this.amountTTC = amountTTC; }
|
||||||
|
|
||||||
|
public String getTaxType() { return taxType; }
|
||||||
|
public void setTaxType(String taxType) { this.taxType = taxType; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transaction TVA
|
||||||
|
*/
|
||||||
|
class TVATransaction {
|
||||||
|
private TransactionType type;
|
||||||
|
private double amountHT;
|
||||||
|
private double tvaAmount;
|
||||||
|
private LocalDate date;
|
||||||
|
|
||||||
|
// Constructeurs et getters/setters
|
||||||
|
public TransactionType getType() { return type; }
|
||||||
|
public void setType(TransactionType type) { this.type = type; }
|
||||||
|
|
||||||
|
public double getAmountHT() { return amountHT; }
|
||||||
|
public void setAmountHT(double amountHT) { this.amountHT = amountHT; }
|
||||||
|
|
||||||
|
public double getTvaAmount() { return tvaAmount; }
|
||||||
|
public void setTvaAmount(double tvaAmount) { this.tvaAmount = tvaAmount; }
|
||||||
|
|
||||||
|
public LocalDate getDate() { return date; }
|
||||||
|
public void setDate(LocalDate date) { this.date = date; }
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TransactionType {
|
||||||
|
VENTE, ACHAT
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Déclaration TVA
|
||||||
|
*/
|
||||||
|
class TVADeclaration {
|
||||||
|
private String tenantId;
|
||||||
|
private YearMonth period;
|
||||||
|
private String declarationType;
|
||||||
|
private double ventesHT;
|
||||||
|
private double tvaCollectee;
|
||||||
|
private double achatsHT;
|
||||||
|
private double tvaDeductible;
|
||||||
|
private double tvaAVerser;
|
||||||
|
private double creditTVA;
|
||||||
|
|
||||||
|
// Getters et Setters complets...
|
||||||
|
public String getTenantId() { return tenantId; }
|
||||||
|
public void setTenantId(String tenantId) { this.tenantId = tenantId; }
|
||||||
|
|
||||||
|
public YearMonth getPeriod() { return period; }
|
||||||
|
public void setPeriod(YearMonth period) { this.period = period; }
|
||||||
|
|
||||||
|
public String getDeclarationType() { return declarationType; }
|
||||||
|
public void setDeclarationType(String declarationType) { this.declarationType = declarationType; }
|
||||||
|
|
||||||
|
public double getVentesHT() { return ventesHT; }
|
||||||
|
public void setVentesHT(double ventesHT) { this.ventesHT = ventesHT; }
|
||||||
|
|
||||||
|
public double getTvaCollectee() { return tvaCollectee; }
|
||||||
|
public void setTvaCollectee(double tvaCollectee) { this.tvaCollectee = tvaCollectee; }
|
||||||
|
|
||||||
|
public double getAchatsHT() { return achatsHT; }
|
||||||
|
public void setAchatsHT(double achatsHT) { this.achatsHT = achatsHT; }
|
||||||
|
|
||||||
|
public double getTvaDeductible() { return tvaDeductible; }
|
||||||
|
public void setTvaDeductible(double tvaDeductible) { this.tvaDeductible = tvaDeductible; }
|
||||||
|
|
||||||
|
public double getTvaAVerser() { return tvaAVerser; }
|
||||||
|
public void setTvaAVerser(double tvaAVerser) { this.tvaAVerser = tvaAVerser; }
|
||||||
|
|
||||||
|
public double getCreditTVA() { return creditTVA; }
|
||||||
|
public void setCreditTVA(double creditTVA) { this.creditTVA = creditTVA; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Déclaration IS
|
||||||
|
*/
|
||||||
|
class ISDeclaration {
|
||||||
|
private String tenantId;
|
||||||
|
private int year;
|
||||||
|
private double chiffreAffaires;
|
||||||
|
private double charges;
|
||||||
|
private double amortissements;
|
||||||
|
private double beneficeBrut;
|
||||||
|
private double beneficeImposable;
|
||||||
|
private double tauxIS;
|
||||||
|
private double montantIS;
|
||||||
|
private double beneficeNet;
|
||||||
|
private boolean isPME;
|
||||||
|
|
||||||
|
// Getters et Setters complets...
|
||||||
|
public String getTenantId() { return tenantId; }
|
||||||
|
public void setTenantId(String tenantId) { this.tenantId = tenantId; }
|
||||||
|
|
||||||
|
public int getYear() { return year; }
|
||||||
|
public void setYear(int year) { this.year = year; }
|
||||||
|
|
||||||
|
public double getChiffreAffaires() { return chiffreAffaires; }
|
||||||
|
public void setChiffreAffaires(double chiffreAffaires) { this.chiffreAffaires = chiffreAffaires; }
|
||||||
|
|
||||||
|
public double getCharges() { return charges; }
|
||||||
|
public void setCharges(double charges) { this.charges = charges; }
|
||||||
|
|
||||||
|
public double getAmortissements() { return amortissements; }
|
||||||
|
public void setAmortissements(double amortissements) { this.amortissements = amortissements; }
|
||||||
|
|
||||||
|
public double getBeneficeBrut() { return beneficeBrut; }
|
||||||
|
public void setBeneficeBrut(double beneficeBrut) { this.beneficeBrut = beneficeBrut; }
|
||||||
|
|
||||||
|
public double getBeneficeImposable() { return beneficeImposable; }
|
||||||
|
public void setBeneficeImposable(double beneficeImposable) { this.beneficeImposable = beneficeImposable; }
|
||||||
|
|
||||||
|
public double getTauxIS() { return tauxIS; }
|
||||||
|
public void setTauxIS(double tauxIS) { this.tauxIS = tauxIS; }
|
||||||
|
|
||||||
|
public double getMontantIS() { return montantIS; }
|
||||||
|
public void setMontantIS(double montantIS) { this.montantIS = montantIS; }
|
||||||
|
|
||||||
|
public double getBeneficeNet() { return beneficeNet; }
|
||||||
|
public void setBeneficeNet(double beneficeNet) { this.beneficeNet = beneficeNet; }
|
||||||
|
|
||||||
|
public boolean getIsPME() { return isPME; }
|
||||||
|
public void setIsPME(boolean isPME) { this.isPME = isPME; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calcul CNPS
|
||||||
|
*/
|
||||||
|
class CNPSCalculation {
|
||||||
|
private double salaireBrut;
|
||||||
|
private double salaireImposable;
|
||||||
|
private double tauxEmployeur;
|
||||||
|
private double tauxEmploye;
|
||||||
|
private double cotisationEmployeur;
|
||||||
|
private double cotisationEmploye;
|
||||||
|
private double cotisationTotale;
|
||||||
|
private double salaireNet;
|
||||||
|
|
||||||
|
// Getters et Setters complets...
|
||||||
|
public double getSalaireBrut() { return salaireBrut; }
|
||||||
|
public void setSalaireBrut(double salaireBrut) { this.salaireBrut = salaireBrut; }
|
||||||
|
|
||||||
|
public double getSalaireImposable() { return salaireImposable; }
|
||||||
|
public void setSalaireImposable(double salaireImposable) { this.salaireImposable = salaireImposable; }
|
||||||
|
|
||||||
|
public double getTauxEmployeur() { return tauxEmployeur; }
|
||||||
|
public void setTauxEmployeur(double tauxEmployeur) { this.tauxEmployeur = tauxEmployeur; }
|
||||||
|
|
||||||
|
public double getTauxEmploye() { return tauxEmploye; }
|
||||||
|
public void setTauxEmploye(double tauxEmploye) { this.tauxEmploye = tauxEmploye; }
|
||||||
|
|
||||||
|
public double getCotisationEmployeur() { return cotisationEmployeur; }
|
||||||
|
public void setCotisationEmployeur(double cotisationEmployeur) { this.cotisationEmployeur = cotisationEmployeur; }
|
||||||
|
|
||||||
|
public double getCotisationEmploye() { return cotisationEmploye; }
|
||||||
|
public void setCotisationEmploye(double cotisationEmploye) { this.cotisationEmploye = cotisationEmploye; }
|
||||||
|
|
||||||
|
public double getCotisationTotale() { return cotisationTotale; }
|
||||||
|
public void setCotisationTotale(double cotisationTotale) { this.cotisationTotale = cotisationTotale; }
|
||||||
|
|
||||||
|
public double getSalaireNet() { return salaireNet; }
|
||||||
|
public void setSalaireNet(double salaireNet) { this.salaireNet = salaireNet; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rapport de conformité
|
||||||
|
*/
|
||||||
|
class ComplianceReport {
|
||||||
|
private String tenantId;
|
||||||
|
private LocalDate checkDate;
|
||||||
|
private List<ComplianceIssue> issues;
|
||||||
|
private double complianceScore;
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
// Getters et Setters
|
||||||
|
public String getTenantId() { return tenantId; }
|
||||||
|
public void setTenantId(String tenantId) { this.tenantId = tenantId; }
|
||||||
|
|
||||||
|
public LocalDate getCheckDate() { return checkDate; }
|
||||||
|
public void setCheckDate(LocalDate checkDate) { this.checkDate = checkDate; }
|
||||||
|
|
||||||
|
public List<ComplianceIssue> getIssues() { return issues; }
|
||||||
|
public void setIssues(List<ComplianceIssue> issues) { this.issues = issues; }
|
||||||
|
|
||||||
|
public double getComplianceScore() { return complianceScore; }
|
||||||
|
public void setComplianceScore(double complianceScore) { this.complianceScore = complianceScore; }
|
||||||
|
|
||||||
|
public String getStatus() { return status; }
|
||||||
|
public void setStatus(String status) { this.status = status; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Problème de conformité
|
||||||
|
*/
|
||||||
|
class ComplianceIssue {
|
||||||
|
private String code;
|
||||||
|
private String description;
|
||||||
|
private String severity;
|
||||||
|
|
||||||
|
public ComplianceIssue(String code, String description, String severity) {
|
||||||
|
this.code = code;
|
||||||
|
this.description = description;
|
||||||
|
this.severity = severity;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
public String getCode() { return code; }
|
||||||
|
public String getDescription() { return description; }
|
||||||
|
public String getSeverity() { return severity; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Échéance fiscale
|
||||||
|
*/
|
||||||
|
class TaxDeadline {
|
||||||
|
private String type;
|
||||||
|
private String description;
|
||||||
|
private LocalDate dueDate;
|
||||||
|
private String priority;
|
||||||
|
|
||||||
|
public TaxDeadline(String type, String description, LocalDate dueDate, String priority) {
|
||||||
|
this.type = type;
|
||||||
|
this.description = description;
|
||||||
|
this.dueDate = dueDate;
|
||||||
|
this.priority = priority;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
public String getType() { return type; }
|
||||||
|
public String getDescription() { return description; }
|
||||||
|
public LocalDate getDueDate() { return dueDate; }
|
||||||
|
public String getPriority() { return priority; }
|
||||||
|
}
|
||||||
197
src/main/java/dev/lions/erp/core/Company.java
Normal file
197
src/main/java/dev/lions/erp/core/Company.java
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
package dev.lions.erp.core;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entité Entreprise - Multi-tenant
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "companies")
|
||||||
|
public class Company {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(nullable = false, unique = true)
|
||||||
|
private String tenantId; // Identifiant unique pour multi-tenant
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
private String legalName;
|
||||||
|
private String registrationNumber; // Numéro RCCM
|
||||||
|
private String taxNumber; // Numéro contribuable DGI
|
||||||
|
private String cnpsNumber; // Numéro CNPS
|
||||||
|
|
||||||
|
// Adresse
|
||||||
|
private String address;
|
||||||
|
private String city;
|
||||||
|
private String postalCode;
|
||||||
|
private String country = "Côte d'Ivoire";
|
||||||
|
|
||||||
|
// Contact
|
||||||
|
private String phone;
|
||||||
|
private String email;
|
||||||
|
private String website;
|
||||||
|
|
||||||
|
// Informations métier
|
||||||
|
private String sector;
|
||||||
|
private String activity;
|
||||||
|
private Integer employeeCount;
|
||||||
|
private String currency = "FCFA";
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
private CompanyStatus status = CompanyStatus.ACTIVE;
|
||||||
|
|
||||||
|
@ElementCollection
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@CollectionTable(name = "company_modules")
|
||||||
|
private List<ModuleType> enabledModules = new ArrayList<>();
|
||||||
|
|
||||||
|
// Dates
|
||||||
|
@Column(nullable = false)
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
private LocalDateTime lastLoginAt;
|
||||||
|
|
||||||
|
// Constructeurs
|
||||||
|
public Company() {
|
||||||
|
this.createdAt = LocalDateTime.now();
|
||||||
|
this.updatedAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Company(String name, String tenantId) {
|
||||||
|
this();
|
||||||
|
this.name = name;
|
||||||
|
this.tenantId = tenantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Méthodes métier
|
||||||
|
public boolean hasModule(ModuleType moduleType) {
|
||||||
|
return enabledModules.contains(moduleType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void enableModule(ModuleType moduleType) {
|
||||||
|
if (!enabledModules.contains(moduleType)) {
|
||||||
|
enabledModules.add(moduleType);
|
||||||
|
this.updatedAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void disableModule(ModuleType moduleType) {
|
||||||
|
if (enabledModules.remove(moduleType)) {
|
||||||
|
this.updatedAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isActive() {
|
||||||
|
return status == CompanyStatus.ACTIVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreUpdate
|
||||||
|
public void preUpdate() {
|
||||||
|
this.updatedAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters et Setters
|
||||||
|
public Long getId() { return id; }
|
||||||
|
public void setId(Long id) { this.id = id; }
|
||||||
|
|
||||||
|
public String getTenantId() { return tenantId; }
|
||||||
|
public void setTenantId(String tenantId) { this.tenantId = tenantId; }
|
||||||
|
|
||||||
|
public String getName() { return name; }
|
||||||
|
public void setName(String name) { this.name = name; }
|
||||||
|
|
||||||
|
public String getLegalName() { return legalName; }
|
||||||
|
public void setLegalName(String legalName) { this.legalName = legalName; }
|
||||||
|
|
||||||
|
public String getRegistrationNumber() { return registrationNumber; }
|
||||||
|
public void setRegistrationNumber(String registrationNumber) { this.registrationNumber = registrationNumber; }
|
||||||
|
|
||||||
|
public String getTaxNumber() { return taxNumber; }
|
||||||
|
public void setTaxNumber(String taxNumber) { this.taxNumber = taxNumber; }
|
||||||
|
|
||||||
|
public String getCnpsNumber() { return cnpsNumber; }
|
||||||
|
public void setCnpsNumber(String cnpsNumber) { this.cnpsNumber = cnpsNumber; }
|
||||||
|
|
||||||
|
public String getAddress() { return address; }
|
||||||
|
public void setAddress(String address) { this.address = address; }
|
||||||
|
|
||||||
|
public String getCity() { return city; }
|
||||||
|
public void setCity(String city) { this.city = city; }
|
||||||
|
|
||||||
|
public String getPostalCode() { return postalCode; }
|
||||||
|
public void setPostalCode(String postalCode) { this.postalCode = postalCode; }
|
||||||
|
|
||||||
|
public String getCountry() { return country; }
|
||||||
|
public void setCountry(String country) { this.country = country; }
|
||||||
|
|
||||||
|
public String getPhone() { return phone; }
|
||||||
|
public void setPhone(String phone) { this.phone = phone; }
|
||||||
|
|
||||||
|
public String getEmail() { return email; }
|
||||||
|
public void setEmail(String email) { this.email = email; }
|
||||||
|
|
||||||
|
public String getWebsite() { return website; }
|
||||||
|
public void setWebsite(String website) { this.website = website; }
|
||||||
|
|
||||||
|
public String getSector() { return sector; }
|
||||||
|
public void setSector(String sector) { this.sector = sector; }
|
||||||
|
|
||||||
|
public String getActivity() { return activity; }
|
||||||
|
public void setActivity(String activity) { this.activity = activity; }
|
||||||
|
|
||||||
|
public Integer getEmployeeCount() { return employeeCount; }
|
||||||
|
public void setEmployeeCount(Integer employeeCount) { this.employeeCount = employeeCount; }
|
||||||
|
|
||||||
|
public String getCurrency() { return currency; }
|
||||||
|
public void setCurrency(String currency) { this.currency = currency; }
|
||||||
|
|
||||||
|
public CompanyStatus getStatus() { return status; }
|
||||||
|
public void setStatus(CompanyStatus status) { this.status = status; }
|
||||||
|
|
||||||
|
public List<ModuleType> getEnabledModules() { return enabledModules; }
|
||||||
|
public void setEnabledModules(List<ModuleType> enabledModules) { this.enabledModules = enabledModules; }
|
||||||
|
|
||||||
|
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||||
|
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||||
|
|
||||||
|
public LocalDateTime getUpdatedAt() { return updatedAt; }
|
||||||
|
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
|
||||||
|
|
||||||
|
public LocalDateTime getLastLoginAt() { return lastLoginAt; }
|
||||||
|
public void setLastLoginAt(LocalDateTime lastLoginAt) { this.lastLoginAt = lastLoginAt; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Statut de l'entreprise
|
||||||
|
*/
|
||||||
|
enum CompanyStatus {
|
||||||
|
ACTIVE, // Active
|
||||||
|
SUSPENDED, // Suspendue
|
||||||
|
TRIAL, // En période d'essai
|
||||||
|
EXPIRED // Expirée
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Types de modules ERP
|
||||||
|
*/
|
||||||
|
enum ModuleType {
|
||||||
|
CRM, // Gestion commerciale
|
||||||
|
STOCK, // Gestion des stocks
|
||||||
|
ACCOUNTING, // Comptabilité
|
||||||
|
HR, // Ressources humaines
|
||||||
|
PROJECT, // Gestion de projets
|
||||||
|
PURCHASE, // Achats
|
||||||
|
SALES, // Ventes
|
||||||
|
INVENTORY, // Inventaire
|
||||||
|
REPORTING, // Rapports
|
||||||
|
DASHBOARD // Tableaux de bord
|
||||||
|
}
|
||||||
338
src/main/java/dev/lions/erp/core/TenantService.java
Normal file
338
src/main/java/dev/lions/erp/core/TenantService.java
Normal file
@@ -0,0 +1,338 @@
|
|||||||
|
package dev.lions.erp.core;
|
||||||
|
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.persistence.EntityManager;
|
||||||
|
import jakarta.transaction.Transactional;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service de gestion multi-tenant
|
||||||
|
*/
|
||||||
|
@ApplicationScoped
|
||||||
|
public class TenantService {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
EntityManager em;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée une nouvelle entreprise (tenant)
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public Company createCompany(String name, String email, String contactName) {
|
||||||
|
// Génération d'un tenant ID unique
|
||||||
|
String tenantId = generateTenantId();
|
||||||
|
|
||||||
|
// Création de l'entreprise
|
||||||
|
Company company = new Company(name, tenantId);
|
||||||
|
company.setEmail(email);
|
||||||
|
|
||||||
|
// Modules par défaut
|
||||||
|
company.enableModule(ModuleType.CRM);
|
||||||
|
company.enableModule(ModuleType.DASHBOARD);
|
||||||
|
|
||||||
|
em.persist(company);
|
||||||
|
|
||||||
|
// Création de l'utilisateur administrateur
|
||||||
|
User admin = createAdminUser(contactName, email, tenantId);
|
||||||
|
|
||||||
|
return company;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée l'utilisateur administrateur pour une entreprise
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public User createAdminUser(String fullName, String email, String tenantId) {
|
||||||
|
String[] names = fullName.split(" ", 2);
|
||||||
|
String firstName = names[0];
|
||||||
|
String lastName = names.length > 1 ? names[1] : "";
|
||||||
|
|
||||||
|
User admin = new User(email, firstName, lastName, tenantId);
|
||||||
|
admin.addRole(UserRole.ADMIN);
|
||||||
|
admin.setPosition("Administrateur");
|
||||||
|
admin.setEmailVerified(true);
|
||||||
|
|
||||||
|
// Permissions complètes
|
||||||
|
admin.addPermission(Permission.USER_MANAGEMENT);
|
||||||
|
admin.addPermission(Permission.COMPANY_SETTINGS);
|
||||||
|
admin.addPermission(Permission.CRM_READ);
|
||||||
|
admin.addPermission(Permission.CRM_WRITE);
|
||||||
|
admin.addPermission(Permission.REPORTS_VIEW);
|
||||||
|
|
||||||
|
// Mot de passe temporaire (à changer au premier login)
|
||||||
|
admin.setPasswordHash(hashPassword("TempPass123!"));
|
||||||
|
|
||||||
|
em.persist(admin);
|
||||||
|
|
||||||
|
return admin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère une entreprise par tenant ID
|
||||||
|
*/
|
||||||
|
public Optional<Company> getCompanyByTenantId(String tenantId) {
|
||||||
|
try {
|
||||||
|
Company company = em.createQuery(
|
||||||
|
"SELECT c FROM Company c WHERE c.tenantId = :tenantId", Company.class)
|
||||||
|
.setParameter("tenantId", tenantId)
|
||||||
|
.getSingleResult();
|
||||||
|
return Optional.of(company);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère un utilisateur par email et tenant
|
||||||
|
*/
|
||||||
|
public Optional<User> getUserByEmailAndTenant(String email, String tenantId) {
|
||||||
|
try {
|
||||||
|
User user = em.createQuery(
|
||||||
|
"SELECT u FROM User u WHERE u.email = :email AND u.tenantId = :tenantId", User.class)
|
||||||
|
.setParameter("email", email)
|
||||||
|
.setParameter("tenantId", tenantId)
|
||||||
|
.getSingleResult();
|
||||||
|
return Optional.of(user);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liste les utilisateurs d'une entreprise
|
||||||
|
*/
|
||||||
|
public List<User> getUsersByTenant(String tenantId) {
|
||||||
|
return em.createQuery(
|
||||||
|
"SELECT u FROM User u WHERE u.tenantId = :tenantId ORDER BY u.firstName, u.lastName", User.class)
|
||||||
|
.setParameter("tenantId", tenantId)
|
||||||
|
.getResultList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Active un module pour une entreprise
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public void enableModule(String tenantId, ModuleType moduleType) {
|
||||||
|
Company company = getCompanyByTenantId(tenantId)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("Entreprise non trouvée"));
|
||||||
|
|
||||||
|
company.enableModule(moduleType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Désactive un module pour une entreprise
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public void disableModule(String tenantId, ModuleType moduleType) {
|
||||||
|
Company company = getCompanyByTenantId(tenantId)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("Entreprise non trouvée"));
|
||||||
|
|
||||||
|
company.disableModule(moduleType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si une entreprise a accès à un module
|
||||||
|
*/
|
||||||
|
public boolean hasModuleAccess(String tenantId, ModuleType moduleType) {
|
||||||
|
return getCompanyByTenantId(tenantId)
|
||||||
|
.map(company -> company.hasModule(moduleType))
|
||||||
|
.orElse(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Met à jour les informations d'une entreprise
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public Company updateCompany(String tenantId, CompanyUpdateDTO updateData) {
|
||||||
|
Company company = getCompanyByTenantId(tenantId)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("Entreprise non trouvée"));
|
||||||
|
|
||||||
|
if (updateData.getName() != null) {
|
||||||
|
company.setName(updateData.getName());
|
||||||
|
}
|
||||||
|
if (updateData.getAddress() != null) {
|
||||||
|
company.setAddress(updateData.getAddress());
|
||||||
|
}
|
||||||
|
if (updateData.getPhone() != null) {
|
||||||
|
company.setPhone(updateData.getPhone());
|
||||||
|
}
|
||||||
|
if (updateData.getEmail() != null) {
|
||||||
|
company.setEmail(updateData.getEmail());
|
||||||
|
}
|
||||||
|
if (updateData.getSector() != null) {
|
||||||
|
company.setSector(updateData.getSector());
|
||||||
|
}
|
||||||
|
if (updateData.getEmployeeCount() != null) {
|
||||||
|
company.setEmployeeCount(updateData.getEmployeeCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
return company;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée un nouvel utilisateur dans une entreprise
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public User createUser(String tenantId, UserCreateDTO userData) {
|
||||||
|
// Vérification que l'entreprise existe
|
||||||
|
getCompanyByTenantId(tenantId)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("Entreprise non trouvée"));
|
||||||
|
|
||||||
|
// Vérification que l'email n'existe pas déjà
|
||||||
|
if (getUserByEmailAndTenant(userData.getEmail(), tenantId).isPresent()) {
|
||||||
|
throw new IllegalArgumentException("Un utilisateur avec cet email existe déjà");
|
||||||
|
}
|
||||||
|
|
||||||
|
User user = new User(userData.getEmail(), userData.getFirstName(),
|
||||||
|
userData.getLastName(), tenantId);
|
||||||
|
user.setPhone(userData.getPhone());
|
||||||
|
user.setPosition(userData.getPosition());
|
||||||
|
user.setDepartment(userData.getDepartment());
|
||||||
|
|
||||||
|
// Rôle par défaut
|
||||||
|
user.addRole(UserRole.USER);
|
||||||
|
|
||||||
|
// Permissions de base
|
||||||
|
user.addPermission(Permission.CRM_READ);
|
||||||
|
user.addPermission(Permission.REPORTS_VIEW);
|
||||||
|
|
||||||
|
// Mot de passe temporaire
|
||||||
|
user.setPasswordHash(hashPassword("TempPass123!"));
|
||||||
|
|
||||||
|
em.persist(user);
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supprime une entreprise et tous ses utilisateurs
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public void deleteCompany(String tenantId) {
|
||||||
|
// Suppression des utilisateurs
|
||||||
|
em.createQuery("DELETE FROM User u WHERE u.tenantId = :tenantId")
|
||||||
|
.setParameter("tenantId", tenantId)
|
||||||
|
.executeUpdate();
|
||||||
|
|
||||||
|
// Suppression de l'entreprise
|
||||||
|
em.createQuery("DELETE FROM Company c WHERE c.tenantId = :tenantId")
|
||||||
|
.setParameter("tenantId", tenantId)
|
||||||
|
.executeUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Statistiques d'une entreprise
|
||||||
|
*/
|
||||||
|
public TenantStats getTenantStats(String tenantId) {
|
||||||
|
Company company = getCompanyByTenantId(tenantId)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("Entreprise non trouvée"));
|
||||||
|
|
||||||
|
Long userCount = em.createQuery(
|
||||||
|
"SELECT COUNT(u) FROM User u WHERE u.tenantId = :tenantId", Long.class)
|
||||||
|
.setParameter("tenantId", tenantId)
|
||||||
|
.getSingleResult();
|
||||||
|
|
||||||
|
Long activeUserCount = em.createQuery(
|
||||||
|
"SELECT COUNT(u) FROM User u WHERE u.tenantId = :tenantId AND u.status = :status", Long.class)
|
||||||
|
.setParameter("tenantId", tenantId)
|
||||||
|
.setParameter("status", UserStatus.ACTIVE)
|
||||||
|
.getSingleResult();
|
||||||
|
|
||||||
|
return new TenantStats(company, userCount, activeUserCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Méthodes utilitaires
|
||||||
|
private String generateTenantId() {
|
||||||
|
return "tenant_" + UUID.randomUUID().toString().replace("-", "").substring(0, 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String hashPassword(String password) {
|
||||||
|
// Implémentation simple - en production, utiliser BCrypt ou Argon2
|
||||||
|
return "hashed_" + password;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO pour mise à jour entreprise
|
||||||
|
*/
|
||||||
|
class CompanyUpdateDTO {
|
||||||
|
private String name;
|
||||||
|
private String address;
|
||||||
|
private String phone;
|
||||||
|
private String email;
|
||||||
|
private String sector;
|
||||||
|
private Integer employeeCount;
|
||||||
|
|
||||||
|
// Getters et Setters
|
||||||
|
public String getName() { return name; }
|
||||||
|
public void setName(String name) { this.name = name; }
|
||||||
|
|
||||||
|
public String getAddress() { return address; }
|
||||||
|
public void setAddress(String address) { this.address = address; }
|
||||||
|
|
||||||
|
public String getPhone() { return phone; }
|
||||||
|
public void setPhone(String phone) { this.phone = phone; }
|
||||||
|
|
||||||
|
public String getEmail() { return email; }
|
||||||
|
public void setEmail(String email) { this.email = email; }
|
||||||
|
|
||||||
|
public String getSector() { return sector; }
|
||||||
|
public void setSector(String sector) { this.sector = sector; }
|
||||||
|
|
||||||
|
public Integer getEmployeeCount() { return employeeCount; }
|
||||||
|
public void setEmployeeCount(Integer employeeCount) { this.employeeCount = employeeCount; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO pour création utilisateur
|
||||||
|
*/
|
||||||
|
class UserCreateDTO {
|
||||||
|
private String email;
|
||||||
|
private String firstName;
|
||||||
|
private String lastName;
|
||||||
|
private String phone;
|
||||||
|
private String position;
|
||||||
|
private String department;
|
||||||
|
|
||||||
|
// Getters et Setters
|
||||||
|
public String getEmail() { return email; }
|
||||||
|
public void setEmail(String email) { this.email = email; }
|
||||||
|
|
||||||
|
public String getFirstName() { return firstName; }
|
||||||
|
public void setFirstName(String firstName) { this.firstName = firstName; }
|
||||||
|
|
||||||
|
public String getLastName() { return lastName; }
|
||||||
|
public void setLastName(String lastName) { this.lastName = lastName; }
|
||||||
|
|
||||||
|
public String getPhone() { return phone; }
|
||||||
|
public void setPhone(String phone) { this.phone = phone; }
|
||||||
|
|
||||||
|
public String getPosition() { return position; }
|
||||||
|
public void setPosition(String position) { this.position = position; }
|
||||||
|
|
||||||
|
public String getDepartment() { return department; }
|
||||||
|
public void setDepartment(String department) { this.department = department; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Statistiques tenant
|
||||||
|
*/
|
||||||
|
class TenantStats {
|
||||||
|
private Company company;
|
||||||
|
private Long totalUsers;
|
||||||
|
private Long activeUsers;
|
||||||
|
|
||||||
|
public TenantStats(Company company, Long totalUsers, Long activeUsers) {
|
||||||
|
this.company = company;
|
||||||
|
this.totalUsers = totalUsers;
|
||||||
|
this.activeUsers = activeUsers;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
public Company getCompany() { return company; }
|
||||||
|
public Long getTotalUsers() { return totalUsers; }
|
||||||
|
public Long getActiveUsers() { return activeUsers; }
|
||||||
|
}
|
||||||
257
src/main/java/dev/lions/erp/core/User.java
Normal file
257
src/main/java/dev/lions/erp/core/User.java
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
package dev.lions.erp.core;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.HashSet;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entité Utilisateur - Multi-tenant
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "users", uniqueConstraints = {
|
||||||
|
@UniqueConstraint(columnNames = {"email", "tenantId"})
|
||||||
|
})
|
||||||
|
public class User {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String tenantId; // Lien vers l'entreprise
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String email;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String firstName;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String lastName;
|
||||||
|
|
||||||
|
private String phone;
|
||||||
|
private String position; // Poste dans l'entreprise
|
||||||
|
private String department;
|
||||||
|
|
||||||
|
// Authentification
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String passwordHash;
|
||||||
|
|
||||||
|
private String resetToken;
|
||||||
|
private LocalDateTime resetTokenExpiry;
|
||||||
|
|
||||||
|
// Permissions
|
||||||
|
@ElementCollection
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@CollectionTable(name = "user_roles")
|
||||||
|
private Set<UserRole> roles = new HashSet<>();
|
||||||
|
|
||||||
|
@ElementCollection
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@CollectionTable(name = "user_permissions")
|
||||||
|
private Set<Permission> permissions = new HashSet<>();
|
||||||
|
|
||||||
|
// Statut
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
private UserStatus status = UserStatus.ACTIVE;
|
||||||
|
|
||||||
|
private Boolean emailVerified = false;
|
||||||
|
private String emailVerificationToken;
|
||||||
|
|
||||||
|
// Dates
|
||||||
|
@Column(nullable = false)
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
private LocalDateTime lastLoginAt;
|
||||||
|
private LocalDateTime lastActivityAt;
|
||||||
|
|
||||||
|
// Préférences
|
||||||
|
private String language = "fr";
|
||||||
|
private String timezone = "Africa/Abidjan";
|
||||||
|
private String theme = "light";
|
||||||
|
|
||||||
|
// Constructeurs
|
||||||
|
public User() {
|
||||||
|
this.createdAt = LocalDateTime.now();
|
||||||
|
this.updatedAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
public User(String email, String firstName, String lastName, String tenantId) {
|
||||||
|
this();
|
||||||
|
this.email = email;
|
||||||
|
this.firstName = firstName;
|
||||||
|
this.lastName = lastName;
|
||||||
|
this.tenantId = tenantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Méthodes métier
|
||||||
|
public String getFullName() {
|
||||||
|
return firstName + " " + lastName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasRole(UserRole role) {
|
||||||
|
return roles.contains(role);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasPermission(Permission permission) {
|
||||||
|
return permissions.contains(permission) || hasAdminRole();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasAdminRole() {
|
||||||
|
return roles.contains(UserRole.ADMIN) || roles.contains(UserRole.SUPER_ADMIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addRole(UserRole role) {
|
||||||
|
roles.add(role);
|
||||||
|
this.updatedAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeRole(UserRole role) {
|
||||||
|
roles.remove(role);
|
||||||
|
this.updatedAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addPermission(Permission permission) {
|
||||||
|
permissions.add(permission);
|
||||||
|
this.updatedAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removePermission(Permission permission) {
|
||||||
|
permissions.remove(permission);
|
||||||
|
this.updatedAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isActive() {
|
||||||
|
return status == UserStatus.ACTIVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateLastActivity() {
|
||||||
|
this.lastActivityAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateLastLogin() {
|
||||||
|
this.lastLoginAt = LocalDateTime.now();
|
||||||
|
this.lastActivityAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreUpdate
|
||||||
|
public void preUpdate() {
|
||||||
|
this.updatedAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters et Setters
|
||||||
|
public Long getId() { return id; }
|
||||||
|
public void setId(Long id) { this.id = id; }
|
||||||
|
|
||||||
|
public String getTenantId() { return tenantId; }
|
||||||
|
public void setTenantId(String tenantId) { this.tenantId = tenantId; }
|
||||||
|
|
||||||
|
public String getEmail() { return email; }
|
||||||
|
public void setEmail(String email) { this.email = email; }
|
||||||
|
|
||||||
|
public String getFirstName() { return firstName; }
|
||||||
|
public void setFirstName(String firstName) { this.firstName = firstName; }
|
||||||
|
|
||||||
|
public String getLastName() { return lastName; }
|
||||||
|
public void setLastName(String lastName) { this.lastName = lastName; }
|
||||||
|
|
||||||
|
public String getPhone() { return phone; }
|
||||||
|
public void setPhone(String phone) { this.phone = phone; }
|
||||||
|
|
||||||
|
public String getPosition() { return position; }
|
||||||
|
public void setPosition(String position) { this.position = position; }
|
||||||
|
|
||||||
|
public String getDepartment() { return department; }
|
||||||
|
public void setDepartment(String department) { this.department = department; }
|
||||||
|
|
||||||
|
public String getPasswordHash() { return passwordHash; }
|
||||||
|
public void setPasswordHash(String passwordHash) { this.passwordHash = passwordHash; }
|
||||||
|
|
||||||
|
public String getResetToken() { return resetToken; }
|
||||||
|
public void setResetToken(String resetToken) { this.resetToken = resetToken; }
|
||||||
|
|
||||||
|
public LocalDateTime getResetTokenExpiry() { return resetTokenExpiry; }
|
||||||
|
public void setResetTokenExpiry(LocalDateTime resetTokenExpiry) { this.resetTokenExpiry = resetTokenExpiry; }
|
||||||
|
|
||||||
|
public Set<UserRole> getRoles() { return roles; }
|
||||||
|
public void setRoles(Set<UserRole> roles) { this.roles = roles; }
|
||||||
|
|
||||||
|
public Set<Permission> getPermissions() { return permissions; }
|
||||||
|
public void setPermissions(Set<Permission> permissions) { this.permissions = permissions; }
|
||||||
|
|
||||||
|
public UserStatus getStatus() { return status; }
|
||||||
|
public void setStatus(UserStatus status) { this.status = status; }
|
||||||
|
|
||||||
|
public Boolean getEmailVerified() { return emailVerified; }
|
||||||
|
public void setEmailVerified(Boolean emailVerified) { this.emailVerified = emailVerified; }
|
||||||
|
|
||||||
|
public String getEmailVerificationToken() { return emailVerificationToken; }
|
||||||
|
public void setEmailVerificationToken(String emailVerificationToken) { this.emailVerificationToken = emailVerificationToken; }
|
||||||
|
|
||||||
|
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||||
|
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||||
|
|
||||||
|
public LocalDateTime getUpdatedAt() { return updatedAt; }
|
||||||
|
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
|
||||||
|
|
||||||
|
public LocalDateTime getLastLoginAt() { return lastLoginAt; }
|
||||||
|
public void setLastLoginAt(LocalDateTime lastLoginAt) { this.lastLoginAt = lastLoginAt; }
|
||||||
|
|
||||||
|
public LocalDateTime getLastActivityAt() { return lastActivityAt; }
|
||||||
|
public void setLastActivityAt(LocalDateTime lastActivityAt) { this.lastActivityAt = lastActivityAt; }
|
||||||
|
|
||||||
|
public String getLanguage() { return language; }
|
||||||
|
public void setLanguage(String language) { this.language = language; }
|
||||||
|
|
||||||
|
public String getTimezone() { return timezone; }
|
||||||
|
public void setTimezone(String timezone) { this.timezone = timezone; }
|
||||||
|
|
||||||
|
public String getTheme() { return theme; }
|
||||||
|
public void setTheme(String theme) { this.theme = theme; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rôles utilisateur
|
||||||
|
*/
|
||||||
|
enum UserRole {
|
||||||
|
SUPER_ADMIN, // Super administrateur Lions Dev
|
||||||
|
ADMIN, // Administrateur entreprise
|
||||||
|
MANAGER, // Manager/Responsable
|
||||||
|
USER, // Utilisateur standard
|
||||||
|
VIEWER // Consultation uniquement
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Permissions spécifiques
|
||||||
|
*/
|
||||||
|
enum Permission {
|
||||||
|
// CRM
|
||||||
|
CRM_READ, CRM_WRITE, CRM_DELETE,
|
||||||
|
|
||||||
|
// Stock
|
||||||
|
STOCK_READ, STOCK_WRITE, STOCK_DELETE,
|
||||||
|
|
||||||
|
// Comptabilité
|
||||||
|
ACCOUNTING_READ, ACCOUNTING_WRITE, ACCOUNTING_DELETE,
|
||||||
|
|
||||||
|
// RH
|
||||||
|
HR_READ, HR_WRITE, HR_DELETE,
|
||||||
|
|
||||||
|
// Administration
|
||||||
|
USER_MANAGEMENT, COMPANY_SETTINGS, SYSTEM_CONFIG,
|
||||||
|
|
||||||
|
// Rapports
|
||||||
|
REPORTS_VIEW, REPORTS_EXPORT, REPORTS_CREATE
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Statut utilisateur
|
||||||
|
*/
|
||||||
|
enum UserStatus {
|
||||||
|
ACTIVE, // Actif
|
||||||
|
INACTIVE, // Inactif
|
||||||
|
SUSPENDED, // Suspendu
|
||||||
|
PENDING_VERIFICATION // En attente de vérification
|
||||||
|
}
|
||||||
11
src/main/java/dev/lions/quote/ComplexityLevel.java
Normal file
11
src/main/java/dev/lions/quote/ComplexityLevel.java
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package dev.lions.quote;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Niveau de complexité d'un module
|
||||||
|
*/
|
||||||
|
public enum ComplexityLevel {
|
||||||
|
BASIC, // Basique (-20%)
|
||||||
|
STANDARD, // Standard (prix de base)
|
||||||
|
ADVANCED, // Avancé (+30%)
|
||||||
|
ENTERPRISE // Enterprise (+60%)
|
||||||
|
}
|
||||||
158
src/main/java/dev/lions/quote/ModuleCatalog.java
Normal file
158
src/main/java/dev/lions/quote/ModuleCatalog.java
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
package dev.lions.quote;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Catalogue des modules disponibles avec tarification
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "module_catalog")
|
||||||
|
public class ModuleCatalog {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(nullable = false, unique = true)
|
||||||
|
private String moduleCode;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String moduleName;
|
||||||
|
|
||||||
|
@Column(length = 2000)
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@Column(length = 3000)
|
||||||
|
private String features; // Fonctionnalités principales
|
||||||
|
|
||||||
|
// Tarification par niveau
|
||||||
|
private Double basicPrice; // Prix niveau basique
|
||||||
|
private Double standardPrice; // Prix niveau standard
|
||||||
|
private Double advancedPrice; // Prix niveau avancé
|
||||||
|
private Double enterprisePrice; // Prix niveau enterprise
|
||||||
|
|
||||||
|
// Détails techniques
|
||||||
|
@Column(length = 2000)
|
||||||
|
private String technicalRequirements;
|
||||||
|
|
||||||
|
private Integer baseImplementationDays;
|
||||||
|
private Integer maxUsers;
|
||||||
|
private String supportLevel;
|
||||||
|
|
||||||
|
// Prérequis et dépendances
|
||||||
|
@ElementCollection
|
||||||
|
@CollectionTable(name = "module_prerequisites")
|
||||||
|
private List<String> prerequisites = new ArrayList<>();
|
||||||
|
|
||||||
|
@ElementCollection
|
||||||
|
@CollectionTable(name = "module_integrations")
|
||||||
|
private List<String> integrations = new ArrayList<>();
|
||||||
|
|
||||||
|
// Métadonnées
|
||||||
|
private String category; // commercial, stock, comptabilite, rh, infrastructure
|
||||||
|
private Integer displayOrder;
|
||||||
|
private Boolean active = true;
|
||||||
|
private Boolean popular = false;
|
||||||
|
|
||||||
|
// Constructeurs
|
||||||
|
public ModuleCatalog() {}
|
||||||
|
|
||||||
|
public ModuleCatalog(String moduleCode, String moduleName, String category) {
|
||||||
|
this.moduleCode = moduleCode;
|
||||||
|
this.moduleName = moduleName;
|
||||||
|
this.category = category;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Méthodes métier
|
||||||
|
public Double getPriceForComplexity(ComplexityLevel complexity) {
|
||||||
|
return switch (complexity) {
|
||||||
|
case BASIC -> basicPrice;
|
||||||
|
case STANDARD -> standardPrice;
|
||||||
|
case ADVANCED -> advancedPrice;
|
||||||
|
case ENTERPRISE -> enterprisePrice;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public ComplexityLevel getRecommendedComplexity(Double auditScore, Integer employeeCount) {
|
||||||
|
// Logique de recommandation basée sur l'audit et la taille
|
||||||
|
if (auditScore < 30 || employeeCount <= 5) {
|
||||||
|
return ComplexityLevel.BASIC;
|
||||||
|
} else if (auditScore < 60 || employeeCount <= 20) {
|
||||||
|
return ComplexityLevel.STANDARD;
|
||||||
|
} else if (auditScore < 80 || employeeCount <= 50) {
|
||||||
|
return ComplexityLevel.ADVANCED;
|
||||||
|
} else {
|
||||||
|
return ComplexityLevel.ENTERPRISE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getEstimatedImplementationDays(ComplexityLevel complexity) {
|
||||||
|
double multiplier = switch (complexity) {
|
||||||
|
case BASIC -> 0.7;
|
||||||
|
case STANDARD -> 1.0;
|
||||||
|
case ADVANCED -> 1.4;
|
||||||
|
case ENTERPRISE -> 2.0;
|
||||||
|
};
|
||||||
|
return (int) Math.ceil(baseImplementationDays * multiplier);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters et Setters
|
||||||
|
public Long getId() { return id; }
|
||||||
|
public void setId(Long id) { this.id = id; }
|
||||||
|
|
||||||
|
public String getModuleCode() { return moduleCode; }
|
||||||
|
public void setModuleCode(String moduleCode) { this.moduleCode = moduleCode; }
|
||||||
|
|
||||||
|
public String getModuleName() { return moduleName; }
|
||||||
|
public void setModuleName(String moduleName) { this.moduleName = moduleName; }
|
||||||
|
|
||||||
|
public String getDescription() { return description; }
|
||||||
|
public void setDescription(String description) { this.description = description; }
|
||||||
|
|
||||||
|
public String getFeatures() { return features; }
|
||||||
|
public void setFeatures(String features) { this.features = features; }
|
||||||
|
|
||||||
|
public Double getBasicPrice() { return basicPrice; }
|
||||||
|
public void setBasicPrice(Double basicPrice) { this.basicPrice = basicPrice; }
|
||||||
|
|
||||||
|
public Double getStandardPrice() { return standardPrice; }
|
||||||
|
public void setStandardPrice(Double standardPrice) { this.standardPrice = standardPrice; }
|
||||||
|
|
||||||
|
public Double getAdvancedPrice() { return advancedPrice; }
|
||||||
|
public void setAdvancedPrice(Double advancedPrice) { this.advancedPrice = advancedPrice; }
|
||||||
|
|
||||||
|
public Double getEnterprisePrice() { return enterprisePrice; }
|
||||||
|
public void setEnterprisePrice(Double enterprisePrice) { this.enterprisePrice = enterprisePrice; }
|
||||||
|
|
||||||
|
public String getTechnicalRequirements() { return technicalRequirements; }
|
||||||
|
public void setTechnicalRequirements(String technicalRequirements) { this.technicalRequirements = technicalRequirements; }
|
||||||
|
|
||||||
|
public Integer getBaseImplementationDays() { return baseImplementationDays; }
|
||||||
|
public void setBaseImplementationDays(Integer baseImplementationDays) { this.baseImplementationDays = baseImplementationDays; }
|
||||||
|
|
||||||
|
public Integer getMaxUsers() { return maxUsers; }
|
||||||
|
public void setMaxUsers(Integer maxUsers) { this.maxUsers = maxUsers; }
|
||||||
|
|
||||||
|
public String getSupportLevel() { return supportLevel; }
|
||||||
|
public void setSupportLevel(String supportLevel) { this.supportLevel = supportLevel; }
|
||||||
|
|
||||||
|
public List<String> getPrerequisites() { return prerequisites; }
|
||||||
|
public void setPrerequisites(List<String> prerequisites) { this.prerequisites = prerequisites; }
|
||||||
|
|
||||||
|
public List<String> getIntegrations() { return integrations; }
|
||||||
|
public void setIntegrations(List<String> integrations) { this.integrations = integrations; }
|
||||||
|
|
||||||
|
public String getCategory() { return category; }
|
||||||
|
public void setCategory(String category) { this.category = category; }
|
||||||
|
|
||||||
|
public Integer getDisplayOrder() { return displayOrder; }
|
||||||
|
public void setDisplayOrder(Integer displayOrder) { this.displayOrder = displayOrder; }
|
||||||
|
|
||||||
|
public Boolean getActive() { return active; }
|
||||||
|
public void setActive(Boolean active) { this.active = active; }
|
||||||
|
|
||||||
|
public Boolean getPopular() { return popular; }
|
||||||
|
public void setPopular(Boolean popular) { this.popular = popular; }
|
||||||
|
}
|
||||||
245
src/main/java/dev/lions/quote/Quote.java
Normal file
245
src/main/java/dev/lions/quote/Quote.java
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
package dev.lions.quote;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Devis personnalisé généré pour une PME
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "quotes")
|
||||||
|
public class Quote {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(nullable = false, unique = true)
|
||||||
|
private String quoteNumber; // QUO-2024-001
|
||||||
|
|
||||||
|
// Informations client
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String companyName;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String contactName;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String email;
|
||||||
|
|
||||||
|
private String phone;
|
||||||
|
private String address;
|
||||||
|
private String sector;
|
||||||
|
private Integer employeeCount;
|
||||||
|
|
||||||
|
// Référence audit
|
||||||
|
private Long auditId;
|
||||||
|
private Double auditScore;
|
||||||
|
|
||||||
|
// Modules sélectionnés
|
||||||
|
@OneToMany(mappedBy = "quote", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
|
||||||
|
private List<QuoteModule> modules = new ArrayList<>();
|
||||||
|
|
||||||
|
// Tarification
|
||||||
|
private Double subtotalHT; // Sous-total HT
|
||||||
|
private Double discountPercentage = 0.0; // Remise %
|
||||||
|
private Double discountAmount = 0.0; // Montant remise
|
||||||
|
private Double totalHT; // Total HT après remise
|
||||||
|
private Double vatRate = 18.0; // TVA 18% Côte d'Ivoire
|
||||||
|
private Double vatAmount; // Montant TVA
|
||||||
|
private Double totalTTC; // Total TTC
|
||||||
|
|
||||||
|
// Services additionnels
|
||||||
|
private Double formationHours = 0.0;
|
||||||
|
private Double formationRate = 15000.0; // 15K FCFA/heure
|
||||||
|
private Double supportMonths = 0.0;
|
||||||
|
private Double supportRate = 25000.0; // 25K FCFA/mois
|
||||||
|
|
||||||
|
// Conditions
|
||||||
|
private Integer validityDays = 30; // Validité 30 jours
|
||||||
|
private String paymentTerms = "50% à la commande, 50% à la livraison";
|
||||||
|
private String deliveryTerms = "6-8 semaines après signature";
|
||||||
|
|
||||||
|
// Statut
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
private QuoteStatus status = QuoteStatus.DRAFT;
|
||||||
|
|
||||||
|
// Dates
|
||||||
|
@Column(nullable = false)
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
private LocalDateTime sentAt;
|
||||||
|
private LocalDateTime viewedAt;
|
||||||
|
private LocalDateTime acceptedAt;
|
||||||
|
private LocalDateTime expiredAt;
|
||||||
|
|
||||||
|
// Suivi commercial
|
||||||
|
private String salesNotes;
|
||||||
|
private String clientFeedback;
|
||||||
|
|
||||||
|
// Constructeurs
|
||||||
|
public Quote() {
|
||||||
|
this.createdAt = LocalDateTime.now();
|
||||||
|
this.expiredAt = LocalDateTime.now().plusDays(validityDays);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Quote(String companyName, String contactName, String email) {
|
||||||
|
this();
|
||||||
|
this.companyName = companyName;
|
||||||
|
this.contactName = contactName;
|
||||||
|
this.email = email;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Méthodes métier
|
||||||
|
public void calculateTotals() {
|
||||||
|
// Calcul sous-total modules
|
||||||
|
this.subtotalHT = modules.stream()
|
||||||
|
.mapToDouble(m -> m.getUnitPrice() * m.getQuantity())
|
||||||
|
.sum();
|
||||||
|
|
||||||
|
// Ajout formation et support
|
||||||
|
this.subtotalHT += (formationHours * formationRate);
|
||||||
|
this.subtotalHT += (supportMonths * supportRate);
|
||||||
|
|
||||||
|
// Calcul remise
|
||||||
|
this.discountAmount = subtotalHT * (discountPercentage / 100);
|
||||||
|
this.totalHT = subtotalHT - discountAmount;
|
||||||
|
|
||||||
|
// Calcul TVA
|
||||||
|
this.vatAmount = totalHT * (vatRate / 100);
|
||||||
|
this.totalTTC = totalHT + vatAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void generateQuoteNumber() {
|
||||||
|
if (this.quoteNumber == null) {
|
||||||
|
int year = LocalDateTime.now().getYear();
|
||||||
|
// Le numéro sera généré par le service avec séquence
|
||||||
|
this.quoteNumber = String.format("QUO-%d-XXX", year);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isExpired() {
|
||||||
|
return LocalDateTime.now().isAfter(expiredAt) && status == QuoteStatus.SENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isValid() {
|
||||||
|
return LocalDateTime.now().isBefore(expiredAt) && status == QuoteStatus.SENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters et Setters
|
||||||
|
public Long getId() { return id; }
|
||||||
|
public void setId(Long id) { this.id = id; }
|
||||||
|
|
||||||
|
public String getQuoteNumber() { return quoteNumber; }
|
||||||
|
public void setQuoteNumber(String quoteNumber) { this.quoteNumber = quoteNumber; }
|
||||||
|
|
||||||
|
public String getCompanyName() { return companyName; }
|
||||||
|
public void setCompanyName(String companyName) { this.companyName = companyName; }
|
||||||
|
|
||||||
|
public String getContactName() { return contactName; }
|
||||||
|
public void setContactName(String contactName) { this.contactName = contactName; }
|
||||||
|
|
||||||
|
public String getEmail() { return email; }
|
||||||
|
public void setEmail(String email) { this.email = email; }
|
||||||
|
|
||||||
|
public String getPhone() { return phone; }
|
||||||
|
public void setPhone(String phone) { this.phone = phone; }
|
||||||
|
|
||||||
|
public String getAddress() { return address; }
|
||||||
|
public void setAddress(String address) { this.address = address; }
|
||||||
|
|
||||||
|
public String getSector() { return sector; }
|
||||||
|
public void setSector(String sector) { this.sector = sector; }
|
||||||
|
|
||||||
|
public Integer getEmployeeCount() { return employeeCount; }
|
||||||
|
public void setEmployeeCount(Integer employeeCount) { this.employeeCount = employeeCount; }
|
||||||
|
|
||||||
|
public Long getAuditId() { return auditId; }
|
||||||
|
public void setAuditId(Long auditId) { this.auditId = auditId; }
|
||||||
|
|
||||||
|
public Double getAuditScore() { return auditScore; }
|
||||||
|
public void setAuditScore(Double auditScore) { this.auditScore = auditScore; }
|
||||||
|
|
||||||
|
public List<QuoteModule> getModules() { return modules; }
|
||||||
|
public void setModules(List<QuoteModule> modules) { this.modules = modules; }
|
||||||
|
|
||||||
|
public Double getSubtotalHT() { return subtotalHT; }
|
||||||
|
public void setSubtotalHT(Double subtotalHT) { this.subtotalHT = subtotalHT; }
|
||||||
|
|
||||||
|
public Double getDiscountPercentage() { return discountPercentage; }
|
||||||
|
public void setDiscountPercentage(Double discountPercentage) { this.discountPercentage = discountPercentage; }
|
||||||
|
|
||||||
|
public Double getDiscountAmount() { return discountAmount; }
|
||||||
|
public void setDiscountAmount(Double discountAmount) { this.discountAmount = discountAmount; }
|
||||||
|
|
||||||
|
public Double getTotalHT() { return totalHT; }
|
||||||
|
public void setTotalHT(Double totalHT) { this.totalHT = totalHT; }
|
||||||
|
|
||||||
|
public Double getVatRate() { return vatRate; }
|
||||||
|
public void setVatRate(Double vatRate) { this.vatRate = vatRate; }
|
||||||
|
|
||||||
|
public Double getVatAmount() { return vatAmount; }
|
||||||
|
public void setVatAmount(Double vatAmount) { this.vatAmount = vatAmount; }
|
||||||
|
|
||||||
|
public Double getTotalTTC() { return totalTTC; }
|
||||||
|
public void setTotalTTC(Double totalTTC) { this.totalTTC = totalTTC; }
|
||||||
|
|
||||||
|
public Double getFormationHours() { return formationHours; }
|
||||||
|
public void setFormationHours(Double formationHours) { this.formationHours = formationHours; }
|
||||||
|
|
||||||
|
public Double getFormationRate() { return formationRate; }
|
||||||
|
public void setFormationRate(Double formationRate) { this.formationRate = formationRate; }
|
||||||
|
|
||||||
|
public Double getSupportMonths() { return supportMonths; }
|
||||||
|
public void setSupportMonths(Double supportMonths) { this.supportMonths = supportMonths; }
|
||||||
|
|
||||||
|
public Double getSupportRate() { return supportRate; }
|
||||||
|
public void setSupportRate(Double supportRate) { this.supportRate = supportRate; }
|
||||||
|
|
||||||
|
public Integer getValidityDays() { return validityDays; }
|
||||||
|
public void setValidityDays(Integer validityDays) { this.validityDays = validityDays; }
|
||||||
|
|
||||||
|
public String getPaymentTerms() { return paymentTerms; }
|
||||||
|
public void setPaymentTerms(String paymentTerms) { this.paymentTerms = paymentTerms; }
|
||||||
|
|
||||||
|
public String getDeliveryTerms() { return deliveryTerms; }
|
||||||
|
public void setDeliveryTerms(String deliveryTerms) { this.deliveryTerms = deliveryTerms; }
|
||||||
|
|
||||||
|
public QuoteStatus getStatus() { return status; }
|
||||||
|
public void setStatus(QuoteStatus status) { this.status = status; }
|
||||||
|
|
||||||
|
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||||
|
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||||
|
|
||||||
|
public LocalDateTime getSentAt() { return sentAt; }
|
||||||
|
public void setSentAt(LocalDateTime sentAt) { this.sentAt = sentAt; }
|
||||||
|
|
||||||
|
public LocalDateTime getViewedAt() { return viewedAt; }
|
||||||
|
public void setViewedAt(LocalDateTime viewedAt) { this.viewedAt = viewedAt; }
|
||||||
|
|
||||||
|
public LocalDateTime getAcceptedAt() { return acceptedAt; }
|
||||||
|
public void setAcceptedAt(LocalDateTime acceptedAt) { this.acceptedAt = acceptedAt; }
|
||||||
|
|
||||||
|
public LocalDateTime getExpiredAt() { return expiredAt; }
|
||||||
|
public void setExpiredAt(LocalDateTime expiredAt) { this.expiredAt = expiredAt; }
|
||||||
|
|
||||||
|
public String getSalesNotes() { return salesNotes; }
|
||||||
|
public void setSalesNotes(String salesNotes) { this.salesNotes = salesNotes; }
|
||||||
|
|
||||||
|
public String getClientFeedback() { return clientFeedback; }
|
||||||
|
public void setClientFeedback(String clientFeedback) { this.clientFeedback = clientFeedback; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Statut du devis
|
||||||
|
*/
|
||||||
|
enum QuoteStatus {
|
||||||
|
DRAFT, // Brouillon
|
||||||
|
SENT, // Envoyé
|
||||||
|
VIEWED, // Consulté par le client
|
||||||
|
ACCEPTED, // Accepté
|
||||||
|
REJECTED, // Refusé
|
||||||
|
EXPIRED // Expiré
|
||||||
|
}
|
||||||
42
src/main/java/dev/lions/quote/QuoteCustomizationDTO.java
Normal file
42
src/main/java/dev/lions/quote/QuoteCustomizationDTO.java
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package dev.lions.quote;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO pour la personnalisation de devis
|
||||||
|
*/
|
||||||
|
public class QuoteCustomizationDTO {
|
||||||
|
|
||||||
|
private List<QuoteModuleDTO> modules;
|
||||||
|
private Double formationHours;
|
||||||
|
private Double supportMonths;
|
||||||
|
private String paymentTerms;
|
||||||
|
private String deliveryTerms;
|
||||||
|
private Double discountPercentage;
|
||||||
|
private String discountReason;
|
||||||
|
|
||||||
|
// Constructeurs
|
||||||
|
public QuoteCustomizationDTO() {}
|
||||||
|
|
||||||
|
// Getters et Setters
|
||||||
|
public List<QuoteModuleDTO> getModules() { return modules; }
|
||||||
|
public void setModules(List<QuoteModuleDTO> modules) { this.modules = modules; }
|
||||||
|
|
||||||
|
public Double getFormationHours() { return formationHours; }
|
||||||
|
public void setFormationHours(Double formationHours) { this.formationHours = formationHours; }
|
||||||
|
|
||||||
|
public Double getSupportMonths() { return supportMonths; }
|
||||||
|
public void setSupportMonths(Double supportMonths) { this.supportMonths = supportMonths; }
|
||||||
|
|
||||||
|
public String getPaymentTerms() { return paymentTerms; }
|
||||||
|
public void setPaymentTerms(String paymentTerms) { this.paymentTerms = paymentTerms; }
|
||||||
|
|
||||||
|
public String getDeliveryTerms() { return deliveryTerms; }
|
||||||
|
public void setDeliveryTerms(String deliveryTerms) { this.deliveryTerms = deliveryTerms; }
|
||||||
|
|
||||||
|
public Double getDiscountPercentage() { return discountPercentage; }
|
||||||
|
public void setDiscountPercentage(Double discountPercentage) { this.discountPercentage = discountPercentage; }
|
||||||
|
|
||||||
|
public String getDiscountReason() { return discountReason; }
|
||||||
|
public void setDiscountReason(String discountReason) { this.discountReason = discountReason; }
|
||||||
|
}
|
||||||
181
src/main/java/dev/lions/quote/QuoteDTO.java
Normal file
181
src/main/java/dev/lions/quote/QuoteDTO.java
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
package dev.lions.quote;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTOs pour les devis
|
||||||
|
*/
|
||||||
|
public class QuoteDTO {
|
||||||
|
|
||||||
|
private Long id;
|
||||||
|
private String quoteNumber;
|
||||||
|
private String companyName;
|
||||||
|
private String contactName;
|
||||||
|
private String email;
|
||||||
|
private String phone;
|
||||||
|
private String sector;
|
||||||
|
private Integer employeeCount;
|
||||||
|
private Long auditId;
|
||||||
|
private Double auditScore;
|
||||||
|
|
||||||
|
private List<QuoteModuleDTO> modules = new ArrayList<>();
|
||||||
|
|
||||||
|
private Double subtotalHT;
|
||||||
|
private Double discountPercentage;
|
||||||
|
private Double discountAmount;
|
||||||
|
private Double totalHT;
|
||||||
|
private Double vatRate;
|
||||||
|
private Double vatAmount;
|
||||||
|
private Double totalTTC;
|
||||||
|
|
||||||
|
private Double formationHours;
|
||||||
|
private Double formationRate;
|
||||||
|
private Double supportMonths;
|
||||||
|
private Double supportRate;
|
||||||
|
|
||||||
|
private String paymentTerms;
|
||||||
|
private String deliveryTerms;
|
||||||
|
private Integer validityDays;
|
||||||
|
|
||||||
|
private QuoteStatus status;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private LocalDateTime sentAt;
|
||||||
|
private LocalDateTime expiredAt;
|
||||||
|
|
||||||
|
// Constructeurs
|
||||||
|
public QuoteDTO() {}
|
||||||
|
|
||||||
|
public QuoteDTO(Quote quote) {
|
||||||
|
this.id = quote.getId();
|
||||||
|
this.quoteNumber = quote.getQuoteNumber();
|
||||||
|
this.companyName = quote.getCompanyName();
|
||||||
|
this.contactName = quote.getContactName();
|
||||||
|
this.email = quote.getEmail();
|
||||||
|
this.phone = quote.getPhone();
|
||||||
|
this.sector = quote.getSector();
|
||||||
|
this.employeeCount = quote.getEmployeeCount();
|
||||||
|
this.auditId = quote.getAuditId();
|
||||||
|
this.auditScore = quote.getAuditScore();
|
||||||
|
|
||||||
|
this.subtotalHT = quote.getSubtotalHT();
|
||||||
|
this.discountPercentage = quote.getDiscountPercentage();
|
||||||
|
this.discountAmount = quote.getDiscountAmount();
|
||||||
|
this.totalHT = quote.getTotalHT();
|
||||||
|
this.vatRate = quote.getVatRate();
|
||||||
|
this.vatAmount = quote.getVatAmount();
|
||||||
|
this.totalTTC = quote.getTotalTTC();
|
||||||
|
|
||||||
|
this.formationHours = quote.getFormationHours();
|
||||||
|
this.formationRate = quote.getFormationRate();
|
||||||
|
this.supportMonths = quote.getSupportMonths();
|
||||||
|
this.supportRate = quote.getSupportRate();
|
||||||
|
|
||||||
|
this.paymentTerms = quote.getPaymentTerms();
|
||||||
|
this.deliveryTerms = quote.getDeliveryTerms();
|
||||||
|
this.validityDays = quote.getValidityDays();
|
||||||
|
|
||||||
|
this.status = quote.getStatus();
|
||||||
|
this.createdAt = quote.getCreatedAt();
|
||||||
|
this.sentAt = quote.getSentAt();
|
||||||
|
this.expiredAt = quote.getExpiredAt();
|
||||||
|
|
||||||
|
// Conversion des modules
|
||||||
|
if (quote.getModules() != null) {
|
||||||
|
for (QuoteModule module : quote.getModules()) {
|
||||||
|
this.modules.add(new QuoteModuleDTO(module));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters et Setters
|
||||||
|
public Long getId() { return id; }
|
||||||
|
public void setId(Long id) { this.id = id; }
|
||||||
|
|
||||||
|
public String getQuoteNumber() { return quoteNumber; }
|
||||||
|
public void setQuoteNumber(String quoteNumber) { this.quoteNumber = quoteNumber; }
|
||||||
|
|
||||||
|
public String getCompanyName() { return companyName; }
|
||||||
|
public void setCompanyName(String companyName) { this.companyName = companyName; }
|
||||||
|
|
||||||
|
public String getContactName() { return contactName; }
|
||||||
|
public void setContactName(String contactName) { this.contactName = contactName; }
|
||||||
|
|
||||||
|
public String getEmail() { return email; }
|
||||||
|
public void setEmail(String email) { this.email = email; }
|
||||||
|
|
||||||
|
public String getPhone() { return phone; }
|
||||||
|
public void setPhone(String phone) { this.phone = phone; }
|
||||||
|
|
||||||
|
public String getSector() { return sector; }
|
||||||
|
public void setSector(String sector) { this.sector = sector; }
|
||||||
|
|
||||||
|
public Integer getEmployeeCount() { return employeeCount; }
|
||||||
|
public void setEmployeeCount(Integer employeeCount) { this.employeeCount = employeeCount; }
|
||||||
|
|
||||||
|
public Long getAuditId() { return auditId; }
|
||||||
|
public void setAuditId(Long auditId) { this.auditId = auditId; }
|
||||||
|
|
||||||
|
public Double getAuditScore() { return auditScore; }
|
||||||
|
public void setAuditScore(Double auditScore) { this.auditScore = auditScore; }
|
||||||
|
|
||||||
|
public List<QuoteModuleDTO> getModules() { return modules; }
|
||||||
|
public void setModules(List<QuoteModuleDTO> modules) { this.modules = modules; }
|
||||||
|
|
||||||
|
public Double getSubtotalHT() { return subtotalHT; }
|
||||||
|
public void setSubtotalHT(Double subtotalHT) { this.subtotalHT = subtotalHT; }
|
||||||
|
|
||||||
|
public Double getDiscountPercentage() { return discountPercentage; }
|
||||||
|
public void setDiscountPercentage(Double discountPercentage) { this.discountPercentage = discountPercentage; }
|
||||||
|
|
||||||
|
public Double getDiscountAmount() { return discountAmount; }
|
||||||
|
public void setDiscountAmount(Double discountAmount) { this.discountAmount = discountAmount; }
|
||||||
|
|
||||||
|
public Double getTotalHT() { return totalHT; }
|
||||||
|
public void setTotalHT(Double totalHT) { this.totalHT = totalHT; }
|
||||||
|
|
||||||
|
public Double getVatRate() { return vatRate; }
|
||||||
|
public void setVatRate(Double vatRate) { this.vatRate = vatRate; }
|
||||||
|
|
||||||
|
public Double getVatAmount() { return vatAmount; }
|
||||||
|
public void setVatAmount(Double vatAmount) { this.vatAmount = vatAmount; }
|
||||||
|
|
||||||
|
public Double getTotalTTC() { return totalTTC; }
|
||||||
|
public void setTotalTTC(Double totalTTC) { this.totalTTC = totalTTC; }
|
||||||
|
|
||||||
|
public Double getFormationHours() { return formationHours; }
|
||||||
|
public void setFormationHours(Double formationHours) { this.formationHours = formationHours; }
|
||||||
|
|
||||||
|
public Double getFormationRate() { return formationRate; }
|
||||||
|
public void setFormationRate(Double formationRate) { this.formationRate = formationRate; }
|
||||||
|
|
||||||
|
public Double getSupportMonths() { return supportMonths; }
|
||||||
|
public void setSupportMonths(Double supportMonths) { this.supportMonths = supportMonths; }
|
||||||
|
|
||||||
|
public Double getSupportRate() { return supportRate; }
|
||||||
|
public void setSupportRate(Double supportRate) { this.supportRate = supportRate; }
|
||||||
|
|
||||||
|
public String getPaymentTerms() { return paymentTerms; }
|
||||||
|
public void setPaymentTerms(String paymentTerms) { this.paymentTerms = paymentTerms; }
|
||||||
|
|
||||||
|
public String getDeliveryTerms() { return deliveryTerms; }
|
||||||
|
public void setDeliveryTerms(String deliveryTerms) { this.deliveryTerms = deliveryTerms; }
|
||||||
|
|
||||||
|
public Integer getValidityDays() { return validityDays; }
|
||||||
|
public void setValidityDays(Integer validityDays) { this.validityDays = validityDays; }
|
||||||
|
|
||||||
|
public QuoteStatus getStatus() { return status; }
|
||||||
|
public void setStatus(QuoteStatus status) { this.status = status; }
|
||||||
|
|
||||||
|
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||||
|
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||||
|
|
||||||
|
public LocalDateTime getSentAt() { return sentAt; }
|
||||||
|
public void setSentAt(LocalDateTime sentAt) { this.sentAt = sentAt; }
|
||||||
|
|
||||||
|
public LocalDateTime getExpiredAt() { return expiredAt; }
|
||||||
|
public void setExpiredAt(LocalDateTime expiredAt) { this.expiredAt = expiredAt; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
115
src/main/java/dev/lions/quote/QuoteModule.java
Normal file
115
src/main/java/dev/lions/quote/QuoteModule.java
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
package dev.lions.quote;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module/ligne d'un devis
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "quote_modules")
|
||||||
|
public class QuoteModule {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "quote_id", nullable = false)
|
||||||
|
private Quote quote;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String moduleCode; // CRM, STOCK, COMPTA, RH, INFRA
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String moduleName;
|
||||||
|
|
||||||
|
@Column(length = 1000)
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private Double unitPrice; // Prix unitaire HT
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private Integer quantity = 1;
|
||||||
|
|
||||||
|
private String unit = "licence"; // licence, heure, mois, etc.
|
||||||
|
|
||||||
|
// Détails techniques
|
||||||
|
@Column(length = 2000)
|
||||||
|
private String technicalSpecs;
|
||||||
|
|
||||||
|
@Column(length = 1000)
|
||||||
|
private String deliverables;
|
||||||
|
|
||||||
|
private Integer implementationDays; // Jours d'implémentation
|
||||||
|
|
||||||
|
// Niveau de complexité basé sur l'audit
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
private ComplexityLevel complexity = ComplexityLevel.STANDARD;
|
||||||
|
|
||||||
|
// Constructeurs
|
||||||
|
public QuoteModule() {}
|
||||||
|
|
||||||
|
public QuoteModule(String moduleCode, String moduleName, Double unitPrice) {
|
||||||
|
this.moduleCode = moduleCode;
|
||||||
|
this.moduleName = moduleName;
|
||||||
|
this.unitPrice = unitPrice;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Méthodes métier
|
||||||
|
public Double getTotalPrice() {
|
||||||
|
return unitPrice * quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Double getComplexityMultiplier() {
|
||||||
|
return switch (complexity) {
|
||||||
|
case BASIC -> 0.8;
|
||||||
|
case STANDARD -> 1.0;
|
||||||
|
case ADVANCED -> 1.3;
|
||||||
|
case ENTERPRISE -> 1.6;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public Double getAdjustedPrice() {
|
||||||
|
return unitPrice * getComplexityMultiplier();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters et Setters
|
||||||
|
public Long getId() { return id; }
|
||||||
|
public void setId(Long id) { this.id = id; }
|
||||||
|
|
||||||
|
public Quote getQuote() { return quote; }
|
||||||
|
public void setQuote(Quote quote) { this.quote = quote; }
|
||||||
|
|
||||||
|
public String getModuleCode() { return moduleCode; }
|
||||||
|
public void setModuleCode(String moduleCode) { this.moduleCode = moduleCode; }
|
||||||
|
|
||||||
|
public String getModuleName() { return moduleName; }
|
||||||
|
public void setModuleName(String moduleName) { this.moduleName = moduleName; }
|
||||||
|
|
||||||
|
public String getDescription() { return description; }
|
||||||
|
public void setDescription(String description) { this.description = description; }
|
||||||
|
|
||||||
|
public Double getUnitPrice() { return unitPrice; }
|
||||||
|
public void setUnitPrice(Double unitPrice) { this.unitPrice = unitPrice; }
|
||||||
|
|
||||||
|
public Integer getQuantity() { return quantity; }
|
||||||
|
public void setQuantity(Integer quantity) { this.quantity = quantity; }
|
||||||
|
|
||||||
|
public String getUnit() { return unit; }
|
||||||
|
public void setUnit(String unit) { this.unit = unit; }
|
||||||
|
|
||||||
|
public String getTechnicalSpecs() { return technicalSpecs; }
|
||||||
|
public void setTechnicalSpecs(String technicalSpecs) { this.technicalSpecs = technicalSpecs; }
|
||||||
|
|
||||||
|
public String getDeliverables() { return deliverables; }
|
||||||
|
public void setDeliverables(String deliverables) { this.deliverables = deliverables; }
|
||||||
|
|
||||||
|
public Integer getImplementationDays() { return implementationDays; }
|
||||||
|
public void setImplementationDays(Integer implementationDays) { this.implementationDays = implementationDays; }
|
||||||
|
|
||||||
|
public ComplexityLevel getComplexity() { return complexity; }
|
||||||
|
public void setComplexity(ComplexityLevel complexity) { this.complexity = complexity; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
70
src/main/java/dev/lions/quote/QuoteModuleDTO.java
Normal file
70
src/main/java/dev/lions/quote/QuoteModuleDTO.java
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
package dev.lions.quote;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO pour les modules de devis
|
||||||
|
*/
|
||||||
|
public class QuoteModuleDTO {
|
||||||
|
|
||||||
|
private Long id;
|
||||||
|
private String moduleCode;
|
||||||
|
private String moduleName;
|
||||||
|
private String description;
|
||||||
|
private Double unitPrice;
|
||||||
|
private Integer quantity;
|
||||||
|
private String unit;
|
||||||
|
private String technicalSpecs;
|
||||||
|
private String deliverables;
|
||||||
|
private Integer implementationDays;
|
||||||
|
private ComplexityLevel complexity;
|
||||||
|
|
||||||
|
// Constructeurs
|
||||||
|
public QuoteModuleDTO() {}
|
||||||
|
|
||||||
|
public QuoteModuleDTO(QuoteModule module) {
|
||||||
|
this.id = module.getId();
|
||||||
|
this.moduleCode = module.getModuleCode();
|
||||||
|
this.moduleName = module.getModuleName();
|
||||||
|
this.description = module.getDescription();
|
||||||
|
this.unitPrice = module.getUnitPrice();
|
||||||
|
this.quantity = module.getQuantity();
|
||||||
|
this.unit = module.getUnit();
|
||||||
|
this.technicalSpecs = module.getTechnicalSpecs();
|
||||||
|
this.deliverables = module.getDeliverables();
|
||||||
|
this.implementationDays = module.getImplementationDays();
|
||||||
|
this.complexity = module.getComplexity();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters et Setters
|
||||||
|
public Long getId() { return id; }
|
||||||
|
public void setId(Long id) { this.id = id; }
|
||||||
|
|
||||||
|
public String getModuleCode() { return moduleCode; }
|
||||||
|
public void setModuleCode(String moduleCode) { this.moduleCode = moduleCode; }
|
||||||
|
|
||||||
|
public String getModuleName() { return moduleName; }
|
||||||
|
public void setModuleName(String moduleName) { this.moduleName = moduleName; }
|
||||||
|
|
||||||
|
public String getDescription() { return description; }
|
||||||
|
public void setDescription(String description) { this.description = description; }
|
||||||
|
|
||||||
|
public Double getUnitPrice() { return unitPrice; }
|
||||||
|
public void setUnitPrice(Double unitPrice) { this.unitPrice = unitPrice; }
|
||||||
|
|
||||||
|
public Integer getQuantity() { return quantity; }
|
||||||
|
public void setQuantity(Integer quantity) { this.quantity = quantity; }
|
||||||
|
|
||||||
|
public String getUnit() { return unit; }
|
||||||
|
public void setUnit(String unit) { this.unit = unit; }
|
||||||
|
|
||||||
|
public String getTechnicalSpecs() { return technicalSpecs; }
|
||||||
|
public void setTechnicalSpecs(String technicalSpecs) { this.technicalSpecs = technicalSpecs; }
|
||||||
|
|
||||||
|
public String getDeliverables() { return deliverables; }
|
||||||
|
public void setDeliverables(String deliverables) { this.deliverables = deliverables; }
|
||||||
|
|
||||||
|
public Integer getImplementationDays() { return implementationDays; }
|
||||||
|
public void setImplementationDays(Integer implementationDays) { this.implementationDays = implementationDays; }
|
||||||
|
|
||||||
|
public ComplexityLevel getComplexity() { return complexity; }
|
||||||
|
public void setComplexity(ComplexityLevel complexity) { this.complexity = complexity; }
|
||||||
|
}
|
||||||
421
src/main/java/dev/lions/quote/QuoteReportService.java
Normal file
421
src/main/java/dev/lions/quote/QuoteReportService.java
Normal file
@@ -0,0 +1,421 @@
|
|||||||
|
package dev.lions.quote;
|
||||||
|
|
||||||
|
import com.itextpdf.text.*;
|
||||||
|
import com.itextpdf.text.pdf.*;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import io.quarkus.mailer.Mail;
|
||||||
|
import io.quarkus.mailer.Mailer;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.text.NumberFormat;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service de génération de rapports PDF pour les devis
|
||||||
|
*/
|
||||||
|
@ApplicationScoped
|
||||||
|
public class QuoteReportService {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
Mailer mailer;
|
||||||
|
|
||||||
|
private static final Font TITLE_FONT = new Font(Font.FontFamily.HELVETICA, 20, Font.BOLD, BaseColor.DARK_GRAY);
|
||||||
|
private static final Font HEADER_FONT = new Font(Font.FontFamily.HELVETICA, 14, Font.BOLD, BaseColor.BLACK);
|
||||||
|
private static final Font NORMAL_FONT = new Font(Font.FontFamily.HELVETICA, 11, Font.NORMAL, BaseColor.BLACK);
|
||||||
|
private static final Font SMALL_FONT = new Font(Font.FontFamily.HELVETICA, 9, Font.NORMAL, BaseColor.GRAY);
|
||||||
|
private static final Font PRICE_FONT = new Font(Font.FontFamily.HELVETICA, 12, Font.BOLD, BaseColor.BLUE);
|
||||||
|
|
||||||
|
private final NumberFormat currencyFormat = NumberFormat.getInstance(Locale.FRANCE);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère le PDF du devis
|
||||||
|
*/
|
||||||
|
public byte[] generateQuotePDF(Quote quote) throws Exception {
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
Document document = new Document(PageSize.A4, 50, 50, 50, 50);
|
||||||
|
PdfWriter writer = PdfWriter.getInstance(document, baos);
|
||||||
|
|
||||||
|
document.open();
|
||||||
|
|
||||||
|
// En-tête Lions Dev
|
||||||
|
addHeader(document, quote);
|
||||||
|
|
||||||
|
// Informations client
|
||||||
|
addClientInfo(document, quote);
|
||||||
|
|
||||||
|
// Détail des modules
|
||||||
|
addModulesDetail(document, quote);
|
||||||
|
|
||||||
|
// Services additionnels
|
||||||
|
addAdditionalServices(document, quote);
|
||||||
|
|
||||||
|
// Récapitulatif financier
|
||||||
|
addFinancialSummary(document, quote);
|
||||||
|
|
||||||
|
// Conditions commerciales
|
||||||
|
addCommercialTerms(document, quote);
|
||||||
|
|
||||||
|
// Pied de page
|
||||||
|
addFooter(document);
|
||||||
|
|
||||||
|
document.close();
|
||||||
|
return baos.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addHeader(Document document, Quote quote) throws DocumentException {
|
||||||
|
// Logo et titre Lions Dev
|
||||||
|
PdfPTable headerTable = new PdfPTable(2);
|
||||||
|
headerTable.setWidthPercentage(100);
|
||||||
|
headerTable.setWidths(new float[]{2, 1});
|
||||||
|
|
||||||
|
// Colonne gauche - Lions Dev
|
||||||
|
PdfPCell leftCell = new PdfPCell();
|
||||||
|
leftCell.setBorder(Rectangle.NO_BORDER);
|
||||||
|
|
||||||
|
Paragraph title = new Paragraph("LIONS DEV", TITLE_FONT);
|
||||||
|
title.setSpacingAfter(5);
|
||||||
|
leftCell.addElement(title);
|
||||||
|
|
||||||
|
Paragraph subtitle = new Paragraph("Solutions Digitales Innovantes", HEADER_FONT);
|
||||||
|
subtitle.setSpacingAfter(10);
|
||||||
|
leftCell.addElement(subtitle);
|
||||||
|
|
||||||
|
Paragraph address = new Paragraph("Abidjan, Côte d'Ivoire\n+225 01 01 75 95 25\ncontact@lions.dev", NORMAL_FONT);
|
||||||
|
leftCell.addElement(address);
|
||||||
|
|
||||||
|
headerTable.addCell(leftCell);
|
||||||
|
|
||||||
|
// Colonne droite - Numéro devis
|
||||||
|
PdfPCell rightCell = new PdfPCell();
|
||||||
|
rightCell.setBorder(Rectangle.NO_BORDER);
|
||||||
|
rightCell.setHorizontalAlignment(Element.ALIGN_RIGHT);
|
||||||
|
|
||||||
|
Paragraph quoteTitle = new Paragraph("DEVIS", new Font(Font.FontFamily.HELVETICA, 16, Font.BOLD));
|
||||||
|
quoteTitle.setAlignment(Element.ALIGN_RIGHT);
|
||||||
|
rightCell.addElement(quoteTitle);
|
||||||
|
|
||||||
|
Paragraph quoteNumber = new Paragraph(quote.getQuoteNumber(), HEADER_FONT);
|
||||||
|
quoteNumber.setAlignment(Element.ALIGN_RIGHT);
|
||||||
|
rightCell.addElement(quoteNumber);
|
||||||
|
|
||||||
|
Paragraph quoteDate = new Paragraph("Date: " + quote.getCreatedAt().format(DateTimeFormatter.ofPattern("dd/MM/yyyy")), NORMAL_FONT);
|
||||||
|
quoteDate.setAlignment(Element.ALIGN_RIGHT);
|
||||||
|
rightCell.addElement(quoteDate);
|
||||||
|
|
||||||
|
Paragraph validUntil = new Paragraph("Valide jusqu'au: " + quote.getExpiredAt().format(DateTimeFormatter.ofPattern("dd/MM/yyyy")), NORMAL_FONT);
|
||||||
|
validUntil.setAlignment(Element.ALIGN_RIGHT);
|
||||||
|
rightCell.addElement(validUntil);
|
||||||
|
|
||||||
|
headerTable.addCell(rightCell);
|
||||||
|
|
||||||
|
document.add(headerTable);
|
||||||
|
document.add(Chunk.NEWLINE);
|
||||||
|
|
||||||
|
// Ligne de séparation
|
||||||
|
com.itextpdf.text.pdf.draw.LineSeparator line = new com.itextpdf.text.pdf.draw.LineSeparator();
|
||||||
|
document.add(new Chunk(line));
|
||||||
|
document.add(Chunk.NEWLINE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addClientInfo(Document document, Quote quote) throws DocumentException {
|
||||||
|
Paragraph section = new Paragraph("INFORMATIONS CLIENT", HEADER_FONT);
|
||||||
|
section.setSpacingBefore(10);
|
||||||
|
section.setSpacingAfter(10);
|
||||||
|
document.add(section);
|
||||||
|
|
||||||
|
PdfPTable table = new PdfPTable(2);
|
||||||
|
table.setWidthPercentage(100);
|
||||||
|
table.setSpacingAfter(15);
|
||||||
|
|
||||||
|
addTableRow(table, "Entreprise:", quote.getCompanyName());
|
||||||
|
addTableRow(table, "Contact:", quote.getContactName());
|
||||||
|
addTableRow(table, "Email:", quote.getEmail());
|
||||||
|
addTableRow(table, "Téléphone:", quote.getPhone() != null ? quote.getPhone() : "-");
|
||||||
|
addTableRow(table, "Secteur:", quote.getSector() != null ? quote.getSector() : "-");
|
||||||
|
addTableRow(table, "Employés:", quote.getEmployeeCount() != null ? quote.getEmployeeCount() + " personnes" : "-");
|
||||||
|
|
||||||
|
if (quote.getAuditScore() != null) {
|
||||||
|
addTableRow(table, "Score audit:", String.format("%.1f%%", quote.getAuditScore()));
|
||||||
|
}
|
||||||
|
|
||||||
|
document.add(table);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addModulesDetail(Document document, Quote quote) throws DocumentException {
|
||||||
|
Paragraph section = new Paragraph("DÉTAIL DE LA SOLUTION", HEADER_FONT);
|
||||||
|
section.setSpacingBefore(15);
|
||||||
|
section.setSpacingAfter(10);
|
||||||
|
document.add(section);
|
||||||
|
|
||||||
|
if (quote.getModules().isEmpty()) {
|
||||||
|
Paragraph noModules = new Paragraph("Aucun module sélectionné", NORMAL_FONT);
|
||||||
|
document.add(noModules);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PdfPTable table = new PdfPTable(5);
|
||||||
|
table.setWidthPercentage(100);
|
||||||
|
table.setWidths(new float[]{3, 1, 1, 1, 1.5f});
|
||||||
|
|
||||||
|
// En-têtes
|
||||||
|
addTableHeader(table, "Module / Description");
|
||||||
|
addTableHeader(table, "Qté");
|
||||||
|
addTableHeader(table, "Prix Unit.");
|
||||||
|
addTableHeader(table, "Niveau");
|
||||||
|
addTableHeader(table, "Total");
|
||||||
|
|
||||||
|
double subtotal = 0;
|
||||||
|
|
||||||
|
for (QuoteModule module : quote.getModules()) {
|
||||||
|
// Nom du module
|
||||||
|
PdfPCell nameCell = new PdfPCell(new Phrase(module.getModuleName(), NORMAL_FONT));
|
||||||
|
nameCell.setVerticalAlignment(Element.ALIGN_TOP);
|
||||||
|
table.addCell(nameCell);
|
||||||
|
|
||||||
|
// Quantité
|
||||||
|
table.addCell(new PdfPCell(new Phrase(module.getQuantity().toString(), NORMAL_FONT)));
|
||||||
|
|
||||||
|
// Prix unitaire
|
||||||
|
table.addCell(new PdfPCell(new Phrase(formatCurrency(module.getUnitPrice()), NORMAL_FONT)));
|
||||||
|
|
||||||
|
// Niveau de complexité
|
||||||
|
String complexityText = switch (module.getComplexity()) {
|
||||||
|
case BASIC -> "Basique";
|
||||||
|
case STANDARD -> "Standard";
|
||||||
|
case ADVANCED -> "Avancé";
|
||||||
|
case ENTERPRISE -> "Enterprise";
|
||||||
|
};
|
||||||
|
table.addCell(new PdfPCell(new Phrase(complexityText, NORMAL_FONT)));
|
||||||
|
|
||||||
|
// Total
|
||||||
|
double moduleTotal = module.getUnitPrice() * module.getQuantity();
|
||||||
|
table.addCell(new PdfPCell(new Phrase(formatCurrency(moduleTotal), PRICE_FONT)));
|
||||||
|
subtotal += moduleTotal;
|
||||||
|
|
||||||
|
// Description détaillée (ligne suivante)
|
||||||
|
if (module.getDescription() != null && !module.getDescription().isEmpty()) {
|
||||||
|
PdfPCell descCell = new PdfPCell(new Phrase(module.getDescription(), SMALL_FONT));
|
||||||
|
descCell.setColspan(5);
|
||||||
|
descCell.setPaddingTop(5);
|
||||||
|
descCell.setPaddingBottom(10);
|
||||||
|
table.addCell(descCell);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.add(table);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addAdditionalServices(Document document, Quote quote) throws DocumentException {
|
||||||
|
if (quote.getFormationHours() > 0 || quote.getSupportMonths() > 0) {
|
||||||
|
Paragraph section = new Paragraph("SERVICES ADDITIONNELS", HEADER_FONT);
|
||||||
|
section.setSpacingBefore(15);
|
||||||
|
section.setSpacingAfter(10);
|
||||||
|
document.add(section);
|
||||||
|
|
||||||
|
PdfPTable table = new PdfPTable(4);
|
||||||
|
table.setWidthPercentage(100);
|
||||||
|
table.setWidths(new float[]{3, 1, 1, 1.5f});
|
||||||
|
|
||||||
|
// En-têtes
|
||||||
|
addTableHeader(table, "Service");
|
||||||
|
addTableHeader(table, "Quantité");
|
||||||
|
addTableHeader(table, "Prix Unit.");
|
||||||
|
addTableHeader(table, "Total");
|
||||||
|
|
||||||
|
// Formation
|
||||||
|
if (quote.getFormationHours() > 0) {
|
||||||
|
table.addCell(new PdfPCell(new Phrase("Formation utilisateurs", NORMAL_FONT)));
|
||||||
|
table.addCell(new PdfPCell(new Phrase(quote.getFormationHours() + " heures", NORMAL_FONT)));
|
||||||
|
table.addCell(new PdfPCell(new Phrase(formatCurrency(quote.getFormationRate()), NORMAL_FONT)));
|
||||||
|
table.addCell(new PdfPCell(new Phrase(formatCurrency(quote.getFormationHours() * quote.getFormationRate()), PRICE_FONT)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Support
|
||||||
|
if (quote.getSupportMonths() > 0) {
|
||||||
|
table.addCell(new PdfPCell(new Phrase("Support technique", NORMAL_FONT)));
|
||||||
|
table.addCell(new PdfPCell(new Phrase(quote.getSupportMonths() + " mois", NORMAL_FONT)));
|
||||||
|
table.addCell(new PdfPCell(new Phrase(formatCurrency(quote.getSupportRate()), NORMAL_FONT)));
|
||||||
|
table.addCell(new PdfPCell(new Phrase(formatCurrency(quote.getSupportMonths() * quote.getSupportRate()), PRICE_FONT)));
|
||||||
|
}
|
||||||
|
|
||||||
|
document.add(table);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addFinancialSummary(Document document, Quote quote) throws DocumentException {
|
||||||
|
Paragraph section = new Paragraph("RÉCAPITULATIF FINANCIER", HEADER_FONT);
|
||||||
|
section.setSpacingBefore(15);
|
||||||
|
section.setSpacingAfter(10);
|
||||||
|
document.add(section);
|
||||||
|
|
||||||
|
PdfPTable table = new PdfPTable(2);
|
||||||
|
table.setWidthPercentage(60);
|
||||||
|
table.setHorizontalAlignment(Element.ALIGN_RIGHT);
|
||||||
|
|
||||||
|
// Sous-total
|
||||||
|
addFinancialRow(table, "Sous-total HT:", formatCurrency(quote.getSubtotalHT()));
|
||||||
|
|
||||||
|
// Remise
|
||||||
|
if (quote.getDiscountPercentage() > 0) {
|
||||||
|
addFinancialRow(table, String.format("Remise (%.1f%%):", quote.getDiscountPercentage()),
|
||||||
|
"-" + formatCurrency(quote.getDiscountAmount()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Total HT
|
||||||
|
addFinancialRow(table, "Total HT:", formatCurrency(quote.getTotalHT()));
|
||||||
|
|
||||||
|
// TVA
|
||||||
|
addFinancialRow(table, String.format("TVA (%.0f%%):", quote.getVatRate()),
|
||||||
|
formatCurrency(quote.getVatAmount()));
|
||||||
|
|
||||||
|
// Total TTC
|
||||||
|
PdfPCell labelCell = new PdfPCell(new Phrase("TOTAL TTC:", new Font(Font.FontFamily.HELVETICA, 12, Font.BOLD)));
|
||||||
|
labelCell.setHorizontalAlignment(Element.ALIGN_RIGHT);
|
||||||
|
labelCell.setBorder(Rectangle.TOP);
|
||||||
|
table.addCell(labelCell);
|
||||||
|
|
||||||
|
PdfPCell valueCell = new PdfPCell(new Phrase(formatCurrency(quote.getTotalTTC()),
|
||||||
|
new Font(Font.FontFamily.HELVETICA, 14, Font.BOLD, BaseColor.BLUE)));
|
||||||
|
valueCell.setHorizontalAlignment(Element.ALIGN_RIGHT);
|
||||||
|
valueCell.setBorder(Rectangle.TOP);
|
||||||
|
table.addCell(valueCell);
|
||||||
|
|
||||||
|
document.add(table);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addCommercialTerms(Document document, Quote quote) throws DocumentException {
|
||||||
|
Paragraph section = new Paragraph("CONDITIONS COMMERCIALES", HEADER_FONT);
|
||||||
|
section.setSpacingBefore(20);
|
||||||
|
section.setSpacingAfter(10);
|
||||||
|
document.add(section);
|
||||||
|
|
||||||
|
// Conditions de paiement
|
||||||
|
Paragraph payment = new Paragraph("Conditions de paiement: " + quote.getPaymentTerms(), NORMAL_FONT);
|
||||||
|
payment.setSpacingAfter(5);
|
||||||
|
document.add(payment);
|
||||||
|
|
||||||
|
// Délais de livraison
|
||||||
|
Paragraph delivery = new Paragraph("Délais de livraison: " + quote.getDeliveryTerms(), NORMAL_FONT);
|
||||||
|
delivery.setSpacingAfter(5);
|
||||||
|
document.add(delivery);
|
||||||
|
|
||||||
|
// Validité
|
||||||
|
Paragraph validity = new Paragraph("Validité du devis: " + quote.getValidityDays() + " jours", NORMAL_FONT);
|
||||||
|
validity.setSpacingAfter(10);
|
||||||
|
document.add(validity);
|
||||||
|
|
||||||
|
// Notes importantes
|
||||||
|
Paragraph notes = new Paragraph("Notes importantes:", HEADER_FONT);
|
||||||
|
notes.setSpacingAfter(5);
|
||||||
|
document.add(notes);
|
||||||
|
|
||||||
|
List notesList = new List(List.UNORDERED);
|
||||||
|
notesList.add(new ListItem("Prix exprimés en FCFA, hors taxes", SMALL_FONT));
|
||||||
|
notesList.add(new ListItem("Formation incluse sur site client", SMALL_FONT));
|
||||||
|
notesList.add(new ListItem("Support technique pendant la période indiquée", SMALL_FONT));
|
||||||
|
notesList.add(new ListItem("Garantie logiciel 12 mois", SMALL_FONT));
|
||||||
|
notesList.add(new ListItem("Mises à jour incluses pendant 6 mois", SMALL_FONT));
|
||||||
|
|
||||||
|
document.add(notesList);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addFooter(Document document) throws DocumentException {
|
||||||
|
Paragraph footer = new Paragraph("\nPour accepter ce devis, contactez-nous au +225 01 01 75 95 25\n" +
|
||||||
|
"ou par email à contact@lions.dev\n\n" +
|
||||||
|
"Lions Dev - Votre partenaire digital en Côte d'Ivoire", SMALL_FONT);
|
||||||
|
footer.setAlignment(Element.ALIGN_CENTER);
|
||||||
|
footer.setSpacingBefore(20);
|
||||||
|
document.add(footer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Envoie le devis par email
|
||||||
|
*/
|
||||||
|
public void sendQuoteByEmail(Quote quote, String customMessage) {
|
||||||
|
try {
|
||||||
|
byte[] pdfBytes = generateQuotePDF(quote);
|
||||||
|
|
||||||
|
String emailContent = generateQuoteEmailContent(quote, customMessage);
|
||||||
|
|
||||||
|
Mail mail = Mail.withHtml(quote.getEmail(),
|
||||||
|
"Votre Devis Personnalisé - " + quote.getQuoteNumber(),
|
||||||
|
emailContent)
|
||||||
|
.addAttachment("devis-" + quote.getQuoteNumber() + ".pdf",
|
||||||
|
pdfBytes, "application/pdf");
|
||||||
|
|
||||||
|
mailer.send(mail);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Log l'erreur mais ne fait pas échouer le processus
|
||||||
|
System.err.println("Erreur envoi email devis: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String generateQuoteEmailContent(Quote quote, String customMessage) {
|
||||||
|
return String.format("""
|
||||||
|
<h2>Bonjour %s,</h2>
|
||||||
|
|
||||||
|
<p>Nous avons le plaisir de vous adresser votre devis personnalisé <strong>%s</strong>.</p>
|
||||||
|
|
||||||
|
%s
|
||||||
|
|
||||||
|
<h3>Récapitulatif:</h3>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Montant total:</strong> %s FCFA TTC</li>
|
||||||
|
<li><strong>Validité:</strong> %d jours</li>
|
||||||
|
<li><strong>Délais:</strong> %s</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p>Ce devis a été établi suite à votre audit de maturité digitale (score: %.1f%%).</p>
|
||||||
|
|
||||||
|
<p><strong>Pour accepter ce devis:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Répondez à cet email</li>
|
||||||
|
<li>Appelez-nous au +225 01 01 75 95 25</li>
|
||||||
|
<li>Ou planifiez un rendez-vous sur notre site</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p>Notre équipe reste à votre disposition pour toute question ou personnalisation.</p>
|
||||||
|
|
||||||
|
<p>Cordialement,<br>
|
||||||
|
L'équipe Lions Dev<br>
|
||||||
|
+225 01 01 75 95 25<br>
|
||||||
|
contact@lions.dev</p>
|
||||||
|
""",
|
||||||
|
quote.getContactName(),
|
||||||
|
quote.getQuoteNumber(),
|
||||||
|
customMessage != null ? "<p>" + customMessage + "</p>" : "",
|
||||||
|
formatCurrency(quote.getTotalTTC()),
|
||||||
|
quote.getValidityDays(),
|
||||||
|
quote.getDeliveryTerms(),
|
||||||
|
quote.getAuditScore() != null ? quote.getAuditScore() : 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Méthodes utilitaires
|
||||||
|
private void addTableRow(PdfPTable table, String label, String value) {
|
||||||
|
table.addCell(new PdfPCell(new Phrase(label, NORMAL_FONT)));
|
||||||
|
table.addCell(new PdfPCell(new Phrase(value != null ? value : "-", NORMAL_FONT)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addTableHeader(PdfPTable table, String header) {
|
||||||
|
PdfPCell cell = new PdfPCell(new Phrase(header, HEADER_FONT));
|
||||||
|
cell.setBackgroundColor(BaseColor.LIGHT_GRAY);
|
||||||
|
cell.setHorizontalAlignment(Element.ALIGN_CENTER);
|
||||||
|
table.addCell(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addFinancialRow(PdfPTable table, String label, String value) {
|
||||||
|
PdfPCell labelCell = new PdfPCell(new Phrase(label, NORMAL_FONT));
|
||||||
|
labelCell.setHorizontalAlignment(Element.ALIGN_RIGHT);
|
||||||
|
table.addCell(labelCell);
|
||||||
|
|
||||||
|
PdfPCell valueCell = new PdfPCell(new Phrase(value, PRICE_FONT));
|
||||||
|
valueCell.setHorizontalAlignment(Element.ALIGN_RIGHT);
|
||||||
|
table.addCell(valueCell);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatCurrency(Double amount) {
|
||||||
|
if (amount == null) return "0";
|
||||||
|
return String.format("%,.0f", amount) + " FCFA";
|
||||||
|
}
|
||||||
|
}
|
||||||
296
src/main/java/dev/lions/quote/QuoteResource.java
Normal file
296
src/main/java/dev/lions/quote/QuoteResource.java
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
package dev.lions.quote;
|
||||||
|
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.ws.rs.*;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
import jakarta.ws.rs.core.Response;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API REST pour la gestion des devis
|
||||||
|
*/
|
||||||
|
@Path("/api/quotes")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
public class QuoteResource {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
QuoteService quoteService;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
QuoteReportService quoteReportService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère un devis automatique à partir d'un audit
|
||||||
|
*/
|
||||||
|
@POST
|
||||||
|
@Path("/generate/{auditId}")
|
||||||
|
public Response generateQuoteFromAudit(@PathParam("auditId") Long auditId) {
|
||||||
|
try {
|
||||||
|
Quote quote = quoteService.generateQuoteFromAudit(auditId);
|
||||||
|
QuoteDTO quoteDTO = new QuoteDTO(quote);
|
||||||
|
|
||||||
|
return Response.ok(quoteDTO).build();
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return Response.status(Response.Status.BAD_REQUEST)
|
||||||
|
.entity(Map.of("error", e.getMessage()))
|
||||||
|
.build();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||||
|
.entity(Map.of("error", "Erreur lors de la génération du devis"))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère un devis par ID
|
||||||
|
*/
|
||||||
|
@GET
|
||||||
|
@Path("/{quoteId}")
|
||||||
|
public Response getQuote(@PathParam("quoteId") Long quoteId) {
|
||||||
|
Quote quote = quoteService.getQuoteById(quoteId);
|
||||||
|
if (quote == null) {
|
||||||
|
return Response.status(Response.Status.NOT_FOUND)
|
||||||
|
.entity(Map.of("error", "Devis non trouvé"))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
QuoteDTO quoteDTO = new QuoteDTO(quote);
|
||||||
|
return Response.ok(quoteDTO).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Personnalise un devis existant
|
||||||
|
*/
|
||||||
|
@PUT
|
||||||
|
@Path("/{quoteId}/customize")
|
||||||
|
public Response customizeQuote(@PathParam("quoteId") Long quoteId,
|
||||||
|
QuoteCustomizationDTO customization) {
|
||||||
|
try {
|
||||||
|
Quote quote = quoteService.customizeQuote(quoteId, customization);
|
||||||
|
QuoteDTO quoteDTO = new QuoteDTO(quote);
|
||||||
|
|
||||||
|
return Response.ok(quoteDTO).build();
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return Response.status(Response.Status.BAD_REQUEST)
|
||||||
|
.entity(Map.of("error", e.getMessage()))
|
||||||
|
.build();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||||
|
.entity(Map.of("error", "Erreur lors de la personnalisation"))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applique une remise à un devis
|
||||||
|
*/
|
||||||
|
@PUT
|
||||||
|
@Path("/{quoteId}/discount")
|
||||||
|
public Response applyDiscount(@PathParam("quoteId") Long quoteId,
|
||||||
|
Map<String, Object> discountData) {
|
||||||
|
try {
|
||||||
|
Double percentage = ((Number) discountData.get("percentage")).doubleValue();
|
||||||
|
String reason = (String) discountData.get("reason");
|
||||||
|
|
||||||
|
quoteService.applyDiscount(quoteId, percentage, reason);
|
||||||
|
|
||||||
|
Quote quote = quoteService.getQuoteById(quoteId);
|
||||||
|
QuoteDTO quoteDTO = new QuoteDTO(quote);
|
||||||
|
|
||||||
|
return Response.ok(quoteDTO).build();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return Response.status(Response.Status.BAD_REQUEST)
|
||||||
|
.entity(Map.of("error", "Erreur lors de l'application de la remise"))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Met à jour le statut d'un devis
|
||||||
|
*/
|
||||||
|
@PUT
|
||||||
|
@Path("/{quoteId}/status")
|
||||||
|
public Response updateStatus(@PathParam("quoteId") Long quoteId,
|
||||||
|
Map<String, String> statusData) {
|
||||||
|
try {
|
||||||
|
QuoteStatus status = QuoteStatus.valueOf(statusData.get("status"));
|
||||||
|
quoteService.updateQuoteStatus(quoteId, status);
|
||||||
|
|
||||||
|
return Response.ok(Map.of("message", "Statut mis à jour")).build();
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return Response.status(Response.Status.BAD_REQUEST)
|
||||||
|
.entity(Map.of("error", "Statut invalide"))
|
||||||
|
.build();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||||
|
.entity(Map.of("error", "Erreur lors de la mise à jour"))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère la liste des devis en attente
|
||||||
|
*/
|
||||||
|
@GET
|
||||||
|
@Path("/pending")
|
||||||
|
public Response getPendingQuotes() {
|
||||||
|
List<Quote> quotes = quoteService.getPendingQuotes();
|
||||||
|
List<QuoteDTO> quoteDTOs = quotes.stream()
|
||||||
|
.map(QuoteDTO::new)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
return Response.ok(quoteDTOs).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère le catalogue des modules
|
||||||
|
*/
|
||||||
|
@GET
|
||||||
|
@Path("/catalog")
|
||||||
|
public Response getModuleCatalog() {
|
||||||
|
List<ModuleCatalog> catalog = quoteService.getActiveCatalog();
|
||||||
|
return Response.ok(catalog).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère le PDF du devis
|
||||||
|
*/
|
||||||
|
@GET
|
||||||
|
@Path("/{quoteId}/pdf")
|
||||||
|
@Produces("application/pdf")
|
||||||
|
public Response generateQuotePDF(@PathParam("quoteId") Long quoteId) {
|
||||||
|
try {
|
||||||
|
Quote quote = quoteService.getQuoteById(quoteId);
|
||||||
|
if (quote == null) {
|
||||||
|
return Response.status(Response.Status.NOT_FOUND)
|
||||||
|
.entity(Map.of("error", "Devis non trouvé"))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] pdfBytes = quoteReportService.generateQuotePDF(quote);
|
||||||
|
|
||||||
|
return Response.ok(pdfBytes)
|
||||||
|
.header("Content-Disposition",
|
||||||
|
"attachment; filename=\"devis-" + quote.getQuoteNumber() + ".pdf\"")
|
||||||
|
.build();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||||
|
.entity(Map.of("error", "Erreur lors de la génération du PDF"))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Envoie le devis par email
|
||||||
|
*/
|
||||||
|
@POST
|
||||||
|
@Path("/{quoteId}/send")
|
||||||
|
public Response sendQuote(@PathParam("quoteId") Long quoteId,
|
||||||
|
Map<String, String> emailData) {
|
||||||
|
try {
|
||||||
|
Quote quote = quoteService.getQuoteById(quoteId);
|
||||||
|
if (quote == null) {
|
||||||
|
return Response.status(Response.Status.NOT_FOUND)
|
||||||
|
.entity(Map.of("error", "Devis non trouvé"))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
String message = emailData.get("message");
|
||||||
|
quoteReportService.sendQuoteByEmail(quote, message);
|
||||||
|
|
||||||
|
// Mise à jour du statut
|
||||||
|
quoteService.updateQuoteStatus(quoteId, QuoteStatus.SENT);
|
||||||
|
|
||||||
|
return Response.ok(Map.of("message", "Devis envoyé avec succès")).build();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||||
|
.entity(Map.of("error", "Erreur lors de l'envoi"))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Statistiques des devis
|
||||||
|
*/
|
||||||
|
@GET
|
||||||
|
@Path("/statistics")
|
||||||
|
public Response getStatistics() {
|
||||||
|
Map<String, Object> stats = quoteService.getQuoteStatistics();
|
||||||
|
return Response.ok(stats).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recherche de devis
|
||||||
|
*/
|
||||||
|
@GET
|
||||||
|
@Path("/search")
|
||||||
|
public Response searchQuotes(@QueryParam("company") String company,
|
||||||
|
@QueryParam("status") String status,
|
||||||
|
@QueryParam("from") String fromDate,
|
||||||
|
@QueryParam("to") String toDate) {
|
||||||
|
// Implémentation de la recherche
|
||||||
|
// Pour l'instant, retourne tous les devis en attente
|
||||||
|
List<Quote> quotes = quoteService.getPendingQuotes();
|
||||||
|
List<QuoteDTO> quoteDTOs = quotes.stream()
|
||||||
|
.map(QuoteDTO::new)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
return Response.ok(quoteDTOs).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Acceptation d'un devis par le client (lien public)
|
||||||
|
*/
|
||||||
|
@POST
|
||||||
|
@Path("/{quoteId}/accept")
|
||||||
|
public Response acceptQuote(@PathParam("quoteId") Long quoteId,
|
||||||
|
Map<String, String> acceptanceData) {
|
||||||
|
try {
|
||||||
|
Quote quote = quoteService.getQuoteById(quoteId);
|
||||||
|
if (quote == null || !quote.isValid()) {
|
||||||
|
return Response.status(Response.Status.BAD_REQUEST)
|
||||||
|
.entity(Map.of("error", "Devis non valide ou expiré"))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mise à jour du statut et feedback client
|
||||||
|
quoteService.updateQuoteStatus(quoteId, QuoteStatus.ACCEPTED);
|
||||||
|
|
||||||
|
String feedback = acceptanceData.get("feedback");
|
||||||
|
if (feedback != null) {
|
||||||
|
quote.setClientFeedback(feedback);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.ok(Map.of("message", "Devis accepté avec succès")).build();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||||
|
.entity(Map.of("error", "Erreur lors de l'acceptation"))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Consultation d'un devis par le client (lien public)
|
||||||
|
*/
|
||||||
|
@GET
|
||||||
|
@Path("/{quoteId}/view")
|
||||||
|
public Response viewQuote(@PathParam("quoteId") Long quoteId) {
|
||||||
|
Quote quote = quoteService.getQuoteById(quoteId);
|
||||||
|
if (quote == null) {
|
||||||
|
return Response.status(Response.Status.NOT_FOUND)
|
||||||
|
.entity(Map.of("error", "Devis non trouvé"))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marquer comme consulté
|
||||||
|
if (quote.getStatus() == QuoteStatus.SENT) {
|
||||||
|
quoteService.updateQuoteStatus(quoteId, QuoteStatus.VIEWED);
|
||||||
|
}
|
||||||
|
|
||||||
|
QuoteDTO quoteDTO = new QuoteDTO(quote);
|
||||||
|
return Response.ok(quoteDTO).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
356
src/main/java/dev/lions/quote/QuoteService.java
Normal file
356
src/main/java/dev/lions/quote/QuoteService.java
Normal file
@@ -0,0 +1,356 @@
|
|||||||
|
package dev.lions.quote;
|
||||||
|
|
||||||
|
import dev.lions.audit.AuditResponse;
|
||||||
|
import dev.lions.audit.AuditService;
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.persistence.EntityManager;
|
||||||
|
import jakarta.transaction.Transactional;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service de gestion des devis personnalisés
|
||||||
|
*/
|
||||||
|
@ApplicationScoped
|
||||||
|
public class QuoteService {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
EntityManager em;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
AuditService auditService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère un devis automatique basé sur un audit
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public Quote generateQuoteFromAudit(Long auditId) {
|
||||||
|
AuditResponse audit = auditService.getAuditById(auditId);
|
||||||
|
if (audit == null) {
|
||||||
|
throw new IllegalArgumentException("Audit non trouvé");
|
||||||
|
}
|
||||||
|
|
||||||
|
Quote quote = new Quote();
|
||||||
|
quote.setCompanyName(audit.getCompanyName());
|
||||||
|
quote.setContactName(audit.getContactName());
|
||||||
|
quote.setEmail(audit.getEmail());
|
||||||
|
quote.setPhone(audit.getPhone());
|
||||||
|
quote.setSector(audit.getSector());
|
||||||
|
quote.setEmployeeCount(audit.getEmployeeCount());
|
||||||
|
quote.setAuditId(auditId);
|
||||||
|
quote.setAuditScore(audit.getMaturityPercentage());
|
||||||
|
|
||||||
|
// Génération du numéro de devis
|
||||||
|
quote.setQuoteNumber(generateQuoteNumber());
|
||||||
|
|
||||||
|
// Sélection automatique des modules selon l'audit
|
||||||
|
List<QuoteModule> recommendedModules = recommendModulesFromAudit(audit);
|
||||||
|
|
||||||
|
for (QuoteModule module : recommendedModules) {
|
||||||
|
module.setQuote(quote);
|
||||||
|
quote.getModules().add(module);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calcul des services additionnels
|
||||||
|
calculateAdditionalServices(quote, audit);
|
||||||
|
|
||||||
|
// Calcul des totaux
|
||||||
|
quote.calculateTotals();
|
||||||
|
|
||||||
|
// Sauvegarde
|
||||||
|
em.persist(quote);
|
||||||
|
|
||||||
|
return quote;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recommande les modules selon les résultats d'audit
|
||||||
|
*/
|
||||||
|
private List<QuoteModule> recommendModulesFromAudit(AuditResponse audit) {
|
||||||
|
List<QuoteModule> modules = new ArrayList<>();
|
||||||
|
Map<String, Integer> categoryScores = audit.getCategoryScores();
|
||||||
|
|
||||||
|
// Récupération du catalogue
|
||||||
|
List<ModuleCatalog> catalog = getActiveCatalog();
|
||||||
|
|
||||||
|
for (Map.Entry<String, Integer> entry : categoryScores.entrySet()) {
|
||||||
|
String category = entry.getKey();
|
||||||
|
Integer score = entry.getValue();
|
||||||
|
|
||||||
|
// Si le score est faible, recommander le module
|
||||||
|
if (score < 60) { // Seuil configurable
|
||||||
|
ModuleCatalog catalogModule = findCatalogModule(catalog, category);
|
||||||
|
if (catalogModule != null) {
|
||||||
|
QuoteModule module = createModuleFromCatalog(catalogModule, audit);
|
||||||
|
modules.add(module);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return modules;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée un module de devis à partir du catalogue
|
||||||
|
*/
|
||||||
|
private QuoteModule createModuleFromCatalog(ModuleCatalog catalogModule, AuditResponse audit) {
|
||||||
|
QuoteModule module = new QuoteModule();
|
||||||
|
module.setModuleCode(catalogModule.getModuleCode());
|
||||||
|
module.setModuleName(catalogModule.getModuleName());
|
||||||
|
module.setDescription(catalogModule.getDescription());
|
||||||
|
module.setTechnicalSpecs(catalogModule.getTechnicalRequirements());
|
||||||
|
|
||||||
|
// Détermination du niveau de complexité
|
||||||
|
ComplexityLevel complexity = catalogModule.getRecommendedComplexity(
|
||||||
|
audit.getMaturityPercentage(),
|
||||||
|
audit.getEmployeeCount()
|
||||||
|
);
|
||||||
|
module.setComplexity(complexity);
|
||||||
|
|
||||||
|
// Prix selon la complexité
|
||||||
|
module.setUnitPrice(catalogModule.getPriceForComplexity(complexity));
|
||||||
|
module.setImplementationDays(catalogModule.getEstimatedImplementationDays(complexity));
|
||||||
|
|
||||||
|
// Génération des livrables
|
||||||
|
module.setDeliverables(generateDeliverables(catalogModule, complexity));
|
||||||
|
|
||||||
|
return module;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calcule les services additionnels (formation, support)
|
||||||
|
*/
|
||||||
|
private void calculateAdditionalServices(Quote quote, AuditResponse audit) {
|
||||||
|
// Formation : basée sur le nombre d'employés et la complexité
|
||||||
|
int employeeCount = audit.getEmployeeCount() != null ? audit.getEmployeeCount() : 5;
|
||||||
|
double formationHours = Math.max(8, employeeCount * 2); // Min 8h, 2h par employé
|
||||||
|
|
||||||
|
// Ajustement selon le score d'audit (plus le score est bas, plus de formation)
|
||||||
|
if (audit.getMaturityPercentage() < 30) {
|
||||||
|
formationHours *= 1.5;
|
||||||
|
} else if (audit.getMaturityPercentage() < 60) {
|
||||||
|
formationHours *= 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
quote.setFormationHours(formationHours);
|
||||||
|
|
||||||
|
// Support : 3 mois minimum, plus selon la complexité
|
||||||
|
double supportMonths = 3.0;
|
||||||
|
if (quote.getModules().size() > 2) {
|
||||||
|
supportMonths = 6.0;
|
||||||
|
}
|
||||||
|
if (quote.getModules().size() > 4) {
|
||||||
|
supportMonths = 12.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
quote.setSupportMonths(supportMonths);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère un numéro de devis unique
|
||||||
|
*/
|
||||||
|
private String generateQuoteNumber() {
|
||||||
|
int year = LocalDateTime.now().getYear();
|
||||||
|
|
||||||
|
// Récupération du dernier numéro de l'année
|
||||||
|
String lastNumber = em.createQuery(
|
||||||
|
"SELECT q.quoteNumber FROM Quote q WHERE q.quoteNumber LIKE :pattern ORDER BY q.quoteNumber DESC",
|
||||||
|
String.class)
|
||||||
|
.setParameter("pattern", "QUO-" + year + "-%")
|
||||||
|
.setMaxResults(1)
|
||||||
|
.getResultStream()
|
||||||
|
.findFirst()
|
||||||
|
.orElse("QUO-" + year + "-000");
|
||||||
|
|
||||||
|
// Extraction et incrémentation du numéro
|
||||||
|
String[] parts = lastNumber.split("-");
|
||||||
|
int nextNumber = Integer.parseInt(parts[2]) + 1;
|
||||||
|
|
||||||
|
return String.format("QUO-%d-%03d", year, nextNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère le catalogue actif
|
||||||
|
*/
|
||||||
|
public List<ModuleCatalog> getActiveCatalog() {
|
||||||
|
return em.createQuery(
|
||||||
|
"SELECT m FROM ModuleCatalog m WHERE m.active = true ORDER BY m.category, m.displayOrder",
|
||||||
|
ModuleCatalog.class
|
||||||
|
).getResultList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trouve un module du catalogue par catégorie
|
||||||
|
*/
|
||||||
|
private ModuleCatalog findCatalogModule(List<ModuleCatalog> catalog, String category) {
|
||||||
|
return catalog.stream()
|
||||||
|
.filter(m -> category.equals(m.getCategory()))
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère les livrables selon le module et la complexité
|
||||||
|
*/
|
||||||
|
private String generateDeliverables(ModuleCatalog catalogModule, ComplexityLevel complexity) {
|
||||||
|
StringBuilder deliverables = new StringBuilder();
|
||||||
|
|
||||||
|
deliverables.append("• Installation et configuration du module ").append(catalogModule.getModuleName()).append("\n");
|
||||||
|
deliverables.append("• Paramétrage selon vos processus métier\n");
|
||||||
|
deliverables.append("• Formation des utilisateurs\n");
|
||||||
|
deliverables.append("• Documentation utilisateur\n");
|
||||||
|
|
||||||
|
if (complexity == ComplexityLevel.ADVANCED || complexity == ComplexityLevel.ENTERPRISE) {
|
||||||
|
deliverables.append("• Intégrations avec systèmes existants\n");
|
||||||
|
deliverables.append("• Rapports personnalisés\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (complexity == ComplexityLevel.ENTERPRISE) {
|
||||||
|
deliverables.append("• Workflow avancés\n");
|
||||||
|
deliverables.append("• API personnalisées\n");
|
||||||
|
deliverables.append("• Support prioritaire\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
deliverables.append("• Garantie 12 mois\n");
|
||||||
|
|
||||||
|
return deliverables.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère un devis par ID
|
||||||
|
*/
|
||||||
|
public Quote getQuoteById(Long quoteId) {
|
||||||
|
return em.find(Quote.class, quoteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Met à jour le statut d'un devis
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public void updateQuoteStatus(Long quoteId, QuoteStatus status) {
|
||||||
|
Quote quote = em.find(Quote.class, quoteId);
|
||||||
|
if (quote != null) {
|
||||||
|
quote.setStatus(status);
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case SENT -> quote.setSentAt(LocalDateTime.now());
|
||||||
|
case VIEWED -> quote.setViewedAt(LocalDateTime.now());
|
||||||
|
case ACCEPTED -> quote.setAcceptedAt(LocalDateTime.now());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applique une remise à un devis
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public void applyDiscount(Long quoteId, Double discountPercentage, String reason) {
|
||||||
|
Quote quote = em.find(Quote.class, quoteId);
|
||||||
|
if (quote != null) {
|
||||||
|
quote.setDiscountPercentage(discountPercentage);
|
||||||
|
quote.calculateTotals();
|
||||||
|
|
||||||
|
String note = String.format("Remise appliquée: %.1f%% - Raison: %s",
|
||||||
|
discountPercentage, reason);
|
||||||
|
quote.setSalesNotes(quote.getSalesNotes() != null ?
|
||||||
|
quote.getSalesNotes() + "\n" + note : note);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les devis en attente
|
||||||
|
*/
|
||||||
|
public List<Quote> getPendingQuotes() {
|
||||||
|
return em.createQuery(
|
||||||
|
"SELECT q FROM Quote q WHERE q.status IN :statuses ORDER BY q.createdAt DESC",
|
||||||
|
Quote.class)
|
||||||
|
.setParameter("statuses", Arrays.asList(QuoteStatus.SENT, QuoteStatus.VIEWED))
|
||||||
|
.getResultList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Statistiques des devis
|
||||||
|
*/
|
||||||
|
public Map<String, Object> getQuoteStatistics() {
|
||||||
|
Map<String, Object> stats = new HashMap<>();
|
||||||
|
|
||||||
|
// Nombre total de devis
|
||||||
|
Long totalQuotes = em.createQuery("SELECT COUNT(q) FROM Quote q", Long.class)
|
||||||
|
.getSingleResult();
|
||||||
|
stats.put("totalQuotes", totalQuotes);
|
||||||
|
|
||||||
|
// Taux de conversion
|
||||||
|
Long acceptedQuotes = em.createQuery(
|
||||||
|
"SELECT COUNT(q) FROM Quote q WHERE q.status = :status", Long.class)
|
||||||
|
.setParameter("status", QuoteStatus.ACCEPTED)
|
||||||
|
.getSingleResult();
|
||||||
|
|
||||||
|
double conversionRate = totalQuotes > 0 ? (double) acceptedQuotes / totalQuotes * 100 : 0;
|
||||||
|
stats.put("conversionRate", conversionRate);
|
||||||
|
|
||||||
|
// Montant total des devis acceptés
|
||||||
|
Double totalRevenue = em.createQuery(
|
||||||
|
"SELECT SUM(q.totalTTC) FROM Quote q WHERE q.status = :status", Double.class)
|
||||||
|
.setParameter("status", QuoteStatus.ACCEPTED)
|
||||||
|
.getSingleResult();
|
||||||
|
stats.put("totalRevenue", totalRevenue != null ? totalRevenue : 0.0);
|
||||||
|
|
||||||
|
// Ticket moyen
|
||||||
|
double averageTicket = acceptedQuotes > 0 ? totalRevenue / acceptedQuotes : 0;
|
||||||
|
stats.put("averageTicket", averageTicket);
|
||||||
|
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Personnalise un devis existant
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public Quote customizeQuote(Long quoteId, QuoteCustomizationDTO customization) {
|
||||||
|
Quote quote = em.find(Quote.class, quoteId);
|
||||||
|
if (quote == null) {
|
||||||
|
throw new IllegalArgumentException("Devis non trouvé");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mise à jour des modules
|
||||||
|
if (customization.getModules() != null) {
|
||||||
|
quote.getModules().clear();
|
||||||
|
|
||||||
|
for (QuoteModuleDTO moduleDTO : customization.getModules()) {
|
||||||
|
QuoteModule module = new QuoteModule();
|
||||||
|
module.setQuote(quote);
|
||||||
|
module.setModuleCode(moduleDTO.getModuleCode());
|
||||||
|
module.setModuleName(moduleDTO.getModuleName());
|
||||||
|
module.setDescription(moduleDTO.getDescription());
|
||||||
|
module.setUnitPrice(moduleDTO.getUnitPrice());
|
||||||
|
module.setQuantity(moduleDTO.getQuantity());
|
||||||
|
module.setComplexity(moduleDTO.getComplexity());
|
||||||
|
|
||||||
|
quote.getModules().add(module);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mise à jour des services
|
||||||
|
if (customization.getFormationHours() != null) {
|
||||||
|
quote.setFormationHours(customization.getFormationHours());
|
||||||
|
}
|
||||||
|
if (customization.getSupportMonths() != null) {
|
||||||
|
quote.setSupportMonths(customization.getSupportMonths());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mise à jour des conditions
|
||||||
|
if (customization.getPaymentTerms() != null) {
|
||||||
|
quote.setPaymentTerms(customization.getPaymentTerms());
|
||||||
|
}
|
||||||
|
if (customization.getDeliveryTerms() != null) {
|
||||||
|
quote.setDeliveryTerms(customization.getDeliveryTerms());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recalcul des totaux
|
||||||
|
quote.calculateTotals();
|
||||||
|
|
||||||
|
return quote;
|
||||||
|
}
|
||||||
|
}
|
||||||
256
src/main/java/dev/lions/roi/ROICalculator.java
Normal file
256
src/main/java/dev/lions/roi/ROICalculator.java
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
package dev.lions.roi;
|
||||||
|
|
||||||
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.HashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculateur de ROI pour démontrer la valeur de la digitalisation
|
||||||
|
*/
|
||||||
|
@ApplicationScoped
|
||||||
|
public class ROICalculator {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calcule le ROI basé sur les gains de productivité
|
||||||
|
*/
|
||||||
|
public ROIResult calculateROI(ROIInput input) {
|
||||||
|
ROIResult result = new ROIResult();
|
||||||
|
|
||||||
|
// Calculs des gains annuels
|
||||||
|
double productivityGains = calculateProductivityGains(input);
|
||||||
|
double errorReduction = calculateErrorReduction(input);
|
||||||
|
double timesSavings = calculateTimeSavings(input);
|
||||||
|
double complianceGains = calculateComplianceGains(input);
|
||||||
|
|
||||||
|
double totalAnnualGains = productivityGains + errorReduction + timesSavings + complianceGains;
|
||||||
|
|
||||||
|
// Coûts
|
||||||
|
double implementationCost = input.getInvestmentAmount();
|
||||||
|
double annualMaintenanceCost = implementationCost * 0.15; // 15% par an
|
||||||
|
|
||||||
|
// ROI sur 3 ans
|
||||||
|
double totalGains3Years = totalAnnualGains * 3;
|
||||||
|
double totalCosts3Years = implementationCost + (annualMaintenanceCost * 3);
|
||||||
|
|
||||||
|
double roi3Years = ((totalGains3Years - totalCosts3Years) / totalCosts3Years) * 100;
|
||||||
|
|
||||||
|
// Période de retour sur investissement
|
||||||
|
double paybackPeriod = implementationCost / totalAnnualGains;
|
||||||
|
|
||||||
|
// Remplissage du résultat
|
||||||
|
result.setAnnualProductivityGains(productivityGains);
|
||||||
|
result.setAnnualErrorReduction(errorReduction);
|
||||||
|
result.setAnnualTimeSavings(timesSavings);
|
||||||
|
result.setAnnualComplianceGains(complianceGains);
|
||||||
|
result.setTotalAnnualGains(totalAnnualGains);
|
||||||
|
result.setImplementationCost(implementationCost);
|
||||||
|
result.setAnnualMaintenanceCost(annualMaintenanceCost);
|
||||||
|
result.setRoi3Years(roi3Years);
|
||||||
|
result.setPaybackPeriodMonths(paybackPeriod * 12);
|
||||||
|
result.setNetPresentValue3Years(totalGains3Years - totalCosts3Years);
|
||||||
|
|
||||||
|
// Détails par catégorie
|
||||||
|
result.setGainsByCategory(Map.of(
|
||||||
|
"Productivité", productivityGains,
|
||||||
|
"Réduction erreurs", errorReduction,
|
||||||
|
"Gain de temps", timesSavings,
|
||||||
|
"Conformité", complianceGains
|
||||||
|
));
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private double calculateProductivityGains(ROIInput input) {
|
||||||
|
// Gain de productivité basé sur l'automatisation
|
||||||
|
double baseProductivity = input.getEmployeeCount() * input.getAverageSalary() * 0.12; // 12% de gain
|
||||||
|
|
||||||
|
// Ajustement selon les modules
|
||||||
|
double multiplier = 1.0;
|
||||||
|
if (input.getSelectedModules().contains("CRM")) multiplier += 0.15;
|
||||||
|
if (input.getSelectedModules().contains("STOCK")) multiplier += 0.10;
|
||||||
|
if (input.getSelectedModules().contains("COMPTA")) multiplier += 0.08;
|
||||||
|
if (input.getSelectedModules().contains("RH")) multiplier += 0.05;
|
||||||
|
|
||||||
|
return baseProductivity * multiplier;
|
||||||
|
}
|
||||||
|
|
||||||
|
private double calculateErrorReduction(ROIInput input) {
|
||||||
|
// Réduction des erreurs et reprises
|
||||||
|
double currentErrorCost = input.getTurnover() * 0.02; // 2% du CA en erreurs
|
||||||
|
double reductionRate = 0.70; // 70% de réduction des erreurs
|
||||||
|
|
||||||
|
return currentErrorCost * reductionRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
private double calculateTimeSavings(ROIInput input) {
|
||||||
|
// Gain de temps sur les tâches administratives
|
||||||
|
double adminTimeHours = input.getEmployeeCount() * 2 * 250; // 2h/jour/employé, 250 jours/an
|
||||||
|
double hourlyRate = input.getAverageSalary() / (8 * 250); // Taux horaire
|
||||||
|
double timeSavingRate = 0.40; // 40% de gain de temps
|
||||||
|
|
||||||
|
return adminTimeHours * hourlyRate * timeSavingRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
private double calculateComplianceGains(ROIInput input) {
|
||||||
|
// Évitement des pénalités et amendes
|
||||||
|
double potentialFines = 500000; // 500K FCFA de pénalités potentielles par an
|
||||||
|
double complianceImprovement = 0.80; // 80% d'amélioration de la conformité
|
||||||
|
|
||||||
|
return potentialFines * complianceImprovement;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère des recommandations basées sur le ROI
|
||||||
|
*/
|
||||||
|
public String generateRecommendations(ROIResult result) {
|
||||||
|
StringBuilder recommendations = new StringBuilder();
|
||||||
|
|
||||||
|
if (result.getRoi3Years() > 200) {
|
||||||
|
recommendations.append("🚀 ROI EXCELLENT (>200%) : Investissement hautement recommandé !\n\n");
|
||||||
|
} else if (result.getRoi3Years() > 100) {
|
||||||
|
recommendations.append("✅ ROI TRÈS BON (>100%) : Investissement très rentable.\n\n");
|
||||||
|
} else if (result.getRoi3Years() > 50) {
|
||||||
|
recommendations.append("👍 ROI CORRECT (>50%) : Investissement rentable à moyen terme.\n\n");
|
||||||
|
} else {
|
||||||
|
recommendations.append("⚠️ ROI FAIBLE (<50%) : Revoir la configuration ou étaler l'investissement.\n\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
recommendations.append("POINTS CLÉS :\n");
|
||||||
|
recommendations.append(String.format("• Retour sur investissement en %.1f mois\n", result.getPaybackPeriodMonths()));
|
||||||
|
recommendations.append(String.format("• Gains annuels : %,.0f FCFA\n", result.getTotalAnnualGains()));
|
||||||
|
recommendations.append(String.format("• Bénéfice net sur 3 ans : %,.0f FCFA\n", result.getNetPresentValue3Years()));
|
||||||
|
|
||||||
|
recommendations.append("\nPRIORITÉS D'IMPLÉMENTATION :\n");
|
||||||
|
|
||||||
|
// Recommandations par gain le plus élevé
|
||||||
|
result.getGainsByCategory().entrySet().stream()
|
||||||
|
.sorted(Map.Entry.<String, Double>comparingByValue().reversed())
|
||||||
|
.forEach(entry -> {
|
||||||
|
recommendations.append(String.format("• %s : %,.0f FCFA/an\n",
|
||||||
|
entry.getKey(), entry.getValue()));
|
||||||
|
});
|
||||||
|
|
||||||
|
return recommendations.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calcule le ROI pour différents scénarios
|
||||||
|
*/
|
||||||
|
public Map<String, ROIResult> calculateScenarios(ROIInput baseInput) {
|
||||||
|
Map<String, ROIResult> scenarios = new HashMap<>();
|
||||||
|
|
||||||
|
// Scénario conservateur (gains -30%)
|
||||||
|
ROIInput conservative = baseInput.copy();
|
||||||
|
conservative.setAverageSalary(baseInput.getAverageSalary() * 0.7);
|
||||||
|
scenarios.put("Conservateur", calculateROI(conservative));
|
||||||
|
|
||||||
|
// Scénario réaliste (base)
|
||||||
|
scenarios.put("Réaliste", calculateROI(baseInput));
|
||||||
|
|
||||||
|
// Scénario optimiste (gains +50%)
|
||||||
|
ROIInput optimistic = baseInput.copy();
|
||||||
|
optimistic.setAverageSalary(baseInput.getAverageSalary() * 1.5);
|
||||||
|
scenarios.put("Optimiste", calculateROI(optimistic));
|
||||||
|
|
||||||
|
return scenarios;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Données d'entrée pour le calcul ROI
|
||||||
|
*/
|
||||||
|
class ROIInput {
|
||||||
|
private int employeeCount;
|
||||||
|
private double averageSalary; // Salaire moyen annuel
|
||||||
|
private double turnover; // CA annuel
|
||||||
|
private double investmentAmount; // Montant investissement
|
||||||
|
private java.util.List<String> selectedModules;
|
||||||
|
private String sector;
|
||||||
|
|
||||||
|
// Constructeurs
|
||||||
|
public ROIInput() {}
|
||||||
|
|
||||||
|
public ROIInput copy() {
|
||||||
|
ROIInput copy = new ROIInput();
|
||||||
|
copy.employeeCount = this.employeeCount;
|
||||||
|
copy.averageSalary = this.averageSalary;
|
||||||
|
copy.turnover = this.turnover;
|
||||||
|
copy.investmentAmount = this.investmentAmount;
|
||||||
|
copy.selectedModules = new java.util.ArrayList<>(this.selectedModules);
|
||||||
|
copy.sector = this.sector;
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters et Setters
|
||||||
|
public int getEmployeeCount() { return employeeCount; }
|
||||||
|
public void setEmployeeCount(int employeeCount) { this.employeeCount = employeeCount; }
|
||||||
|
|
||||||
|
public double getAverageSalary() { return averageSalary; }
|
||||||
|
public void setAverageSalary(double averageSalary) { this.averageSalary = averageSalary; }
|
||||||
|
|
||||||
|
public double getTurnover() { return turnover; }
|
||||||
|
public void setTurnover(double turnover) { this.turnover = turnover; }
|
||||||
|
|
||||||
|
public double getInvestmentAmount() { return investmentAmount; }
|
||||||
|
public void setInvestmentAmount(double investmentAmount) { this.investmentAmount = investmentAmount; }
|
||||||
|
|
||||||
|
public java.util.List<String> getSelectedModules() { return selectedModules; }
|
||||||
|
public void setSelectedModules(java.util.List<String> selectedModules) { this.selectedModules = selectedModules; }
|
||||||
|
|
||||||
|
public String getSector() { return sector; }
|
||||||
|
public void setSector(String sector) { this.sector = sector; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Résultat du calcul ROI
|
||||||
|
*/
|
||||||
|
class ROIResult {
|
||||||
|
private double annualProductivityGains;
|
||||||
|
private double annualErrorReduction;
|
||||||
|
private double annualTimeSavings;
|
||||||
|
private double annualComplianceGains;
|
||||||
|
private double totalAnnualGains;
|
||||||
|
private double implementationCost;
|
||||||
|
private double annualMaintenanceCost;
|
||||||
|
private double roi3Years;
|
||||||
|
private double paybackPeriodMonths;
|
||||||
|
private double netPresentValue3Years;
|
||||||
|
private Map<String, Double> gainsByCategory;
|
||||||
|
|
||||||
|
// Constructeurs
|
||||||
|
public ROIResult() {}
|
||||||
|
|
||||||
|
// Getters et Setters
|
||||||
|
public double getAnnualProductivityGains() { return annualProductivityGains; }
|
||||||
|
public void setAnnualProductivityGains(double annualProductivityGains) { this.annualProductivityGains = annualProductivityGains; }
|
||||||
|
|
||||||
|
public double getAnnualErrorReduction() { return annualErrorReduction; }
|
||||||
|
public void setAnnualErrorReduction(double annualErrorReduction) { this.annualErrorReduction = annualErrorReduction; }
|
||||||
|
|
||||||
|
public double getAnnualTimeSavings() { return annualTimeSavings; }
|
||||||
|
public void setAnnualTimeSavings(double annualTimeSavings) { this.annualTimeSavings = annualTimeSavings; }
|
||||||
|
|
||||||
|
public double getAnnualComplianceGains() { return annualComplianceGains; }
|
||||||
|
public void setAnnualComplianceGains(double annualComplianceGains) { this.annualComplianceGains = annualComplianceGains; }
|
||||||
|
|
||||||
|
public double getTotalAnnualGains() { return totalAnnualGains; }
|
||||||
|
public void setTotalAnnualGains(double totalAnnualGains) { this.totalAnnualGains = totalAnnualGains; }
|
||||||
|
|
||||||
|
public double getImplementationCost() { return implementationCost; }
|
||||||
|
public void setImplementationCost(double implementationCost) { this.implementationCost = implementationCost; }
|
||||||
|
|
||||||
|
public double getAnnualMaintenanceCost() { return annualMaintenanceCost; }
|
||||||
|
public void setAnnualMaintenanceCost(double annualMaintenanceCost) { this.annualMaintenanceCost = annualMaintenanceCost; }
|
||||||
|
|
||||||
|
public double getRoi3Years() { return roi3Years; }
|
||||||
|
public void setRoi3Years(double roi3Years) { this.roi3Years = roi3Years; }
|
||||||
|
|
||||||
|
public double getPaybackPeriodMonths() { return paybackPeriodMonths; }
|
||||||
|
public void setPaybackPeriodMonths(double paybackPeriodMonths) { this.paybackPeriodMonths = paybackPeriodMonths; }
|
||||||
|
|
||||||
|
public double getNetPresentValue3Years() { return netPresentValue3Years; }
|
||||||
|
public void setNetPresentValue3Years(double netPresentValue3Years) { this.netPresentValue3Years = netPresentValue3Years; }
|
||||||
|
|
||||||
|
public Map<String, Double> getGainsByCategory() { return gainsByCategory; }
|
||||||
|
public void setGainsByCategory(Map<String, Double> gainsByCategory) { this.gainsByCategory = gainsByCategory; }
|
||||||
|
}
|
||||||
170
src/main/java/dev/lions/roi/ROIResource.java
Normal file
170
src/main/java/dev/lions/roi/ROIResource.java
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
package dev.lions.roi;
|
||||||
|
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.ws.rs.*;
|
||||||
|
import jakarta.ws.rs.core.MediaType;
|
||||||
|
import jakarta.ws.rs.core.Response;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API REST pour le calculateur ROI
|
||||||
|
*/
|
||||||
|
@Path("/api/roi")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
public class ROIResource {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
ROICalculator roiCalculator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calcule le ROI pour une configuration donnée
|
||||||
|
*/
|
||||||
|
@POST
|
||||||
|
@Path("/calculate")
|
||||||
|
public Response calculateROI(ROIInput input) {
|
||||||
|
try {
|
||||||
|
// Validation des données
|
||||||
|
if (input.getEmployeeCount() <= 0 || input.getInvestmentAmount() <= 0) {
|
||||||
|
return Response.status(Response.Status.BAD_REQUEST)
|
||||||
|
.entity(Map.of("error", "Données invalides"))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
ROIResult result = roiCalculator.calculateROI(input);
|
||||||
|
String recommendations = roiCalculator.generateRecommendations(result);
|
||||||
|
|
||||||
|
Map<String, Object> response = Map.of(
|
||||||
|
"result", result,
|
||||||
|
"recommendations", recommendations
|
||||||
|
);
|
||||||
|
|
||||||
|
return Response.ok(response).build();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||||
|
.entity(Map.of("error", "Erreur lors du calcul ROI"))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calcule plusieurs scénarios ROI
|
||||||
|
*/
|
||||||
|
@POST
|
||||||
|
@Path("/scenarios")
|
||||||
|
public Response calculateScenarios(ROIInput input) {
|
||||||
|
try {
|
||||||
|
Map<String, ROIResult> scenarios = roiCalculator.calculateScenarios(input);
|
||||||
|
|
||||||
|
return Response.ok(scenarios).build();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||||
|
.entity(Map.of("error", "Erreur lors du calcul des scénarios"))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calcule le ROI rapide basé sur un audit
|
||||||
|
*/
|
||||||
|
@GET
|
||||||
|
@Path("/quick/{auditId}")
|
||||||
|
public Response quickROI(@PathParam("auditId") Long auditId) {
|
||||||
|
try {
|
||||||
|
// Récupération des données d'audit (simulation)
|
||||||
|
ROIInput input = new ROIInput();
|
||||||
|
input.setEmployeeCount(10); // Valeur par défaut
|
||||||
|
input.setAverageSalary(2400000); // 200K FCFA/mois
|
||||||
|
input.setTurnover(50000000); // 50M FCFA/an
|
||||||
|
input.setInvestmentAmount(500000); // 500K FCFA
|
||||||
|
input.setSelectedModules(Arrays.asList("CRM", "COMPTA"));
|
||||||
|
input.setSector("commerce");
|
||||||
|
|
||||||
|
ROIResult result = roiCalculator.calculateROI(input);
|
||||||
|
String recommendations = roiCalculator.generateRecommendations(result);
|
||||||
|
|
||||||
|
Map<String, Object> response = Map.of(
|
||||||
|
"result", result,
|
||||||
|
"recommendations", recommendations,
|
||||||
|
"input", input
|
||||||
|
);
|
||||||
|
|
||||||
|
return Response.ok(response).build();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||||
|
.entity(Map.of("error", "Erreur lors du calcul ROI rapide"))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retourne les paramètres par défaut selon le secteur
|
||||||
|
*/
|
||||||
|
@GET
|
||||||
|
@Path("/defaults/{sector}")
|
||||||
|
public Response getDefaults(@PathParam("sector") String sector) {
|
||||||
|
ROIInput defaults = new ROIInput();
|
||||||
|
|
||||||
|
switch (sector.toLowerCase()) {
|
||||||
|
case "commerce":
|
||||||
|
defaults.setAverageSalary(2400000); // 200K/mois
|
||||||
|
defaults.setSelectedModules(Arrays.asList("CRM", "STOCK", "COMPTA"));
|
||||||
|
break;
|
||||||
|
case "services":
|
||||||
|
defaults.setAverageSalary(3000000); // 250K/mois
|
||||||
|
defaults.setSelectedModules(Arrays.asList("CRM", "RH", "COMPTA"));
|
||||||
|
break;
|
||||||
|
case "industrie":
|
||||||
|
defaults.setAverageSalary(3600000); // 300K/mois
|
||||||
|
defaults.setSelectedModules(Arrays.asList("STOCK", "COMPTA", "RH"));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
defaults.setAverageSalary(2400000);
|
||||||
|
defaults.setSelectedModules(Arrays.asList("CRM", "COMPTA"));
|
||||||
|
}
|
||||||
|
|
||||||
|
defaults.setSector(sector);
|
||||||
|
|
||||||
|
return Response.ok(defaults).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère un rapport ROI détaillé
|
||||||
|
*/
|
||||||
|
@POST
|
||||||
|
@Path("/report")
|
||||||
|
public Response generateReport(ROIInput input) {
|
||||||
|
try {
|
||||||
|
ROIResult result = roiCalculator.calculateROI(input);
|
||||||
|
Map<String, ROIResult> scenarios = roiCalculator.calculateScenarios(input);
|
||||||
|
String recommendations = roiCalculator.generateRecommendations(result);
|
||||||
|
|
||||||
|
Map<String, Object> report = Map.of(
|
||||||
|
"input", input,
|
||||||
|
"baseResult", result,
|
||||||
|
"scenarios", scenarios,
|
||||||
|
"recommendations", recommendations,
|
||||||
|
"summary", generateSummary(result)
|
||||||
|
);
|
||||||
|
|
||||||
|
return Response.ok(report).build();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||||
|
.entity(Map.of("error", "Erreur lors de la génération du rapport"))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> generateSummary(ROIResult result) {
|
||||||
|
return Map.of(
|
||||||
|
"roiPercentage", Math.round(result.getRoi3Years()),
|
||||||
|
"paybackMonths", Math.round(result.getPaybackPeriodMonths()),
|
||||||
|
"annualSavings", Math.round(result.getTotalAnnualGains()),
|
||||||
|
"netBenefit", Math.round(result.getNetPresentValue3Years()),
|
||||||
|
"recommendation", result.getRoi3Years() > 100 ? "RECOMMANDÉ" :
|
||||||
|
result.getRoi3Years() > 50 ? "ACCEPTABLE" : "À REVOIR"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
637
src/main/resources/META-INF/resources/audit.html
Normal file
637
src/main/resources/META-INF/resources/audit.html
Normal file
@@ -0,0 +1,637 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Audit Gratuit de Maturité Digitale - Lions Dev</title>
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
<style>
|
||||||
|
.audit-container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
padding: 20px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-progress {
|
||||||
|
width: 100%;
|
||||||
|
height: 8px;
|
||||||
|
background: #e0e0e0;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin: 20px 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-progress-bar {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #4CAF50, #45a049);
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-section {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-section.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 1.5em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-icon {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
margin-right: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background: #f9f9f9;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-text {
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-options {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px;
|
||||||
|
background: white;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option:hover {
|
||||||
|
border-color: #667eea;
|
||||||
|
background: #f0f4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option.selected {
|
||||||
|
border-color: #667eea;
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option input[type="radio"] {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navigation {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 12px 24px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: #5a6fd8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.company-info {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input,
|
||||||
|
.form-group select {
|
||||||
|
padding: 10px;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus,
|
||||||
|
.form-group select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-container {
|
||||||
|
display: none;
|
||||||
|
text-align: center;
|
||||||
|
padding: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-display {
|
||||||
|
font-size: 3em;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-excellent { color: #4CAF50; }
|
||||||
|
.score-good { color: #2196F3; }
|
||||||
|
.score-average { color: #FF9800; }
|
||||||
|
.score-poor { color: #F44336; }
|
||||||
|
|
||||||
|
.recommendations {
|
||||||
|
text-align: left;
|
||||||
|
background: #f9f9f9;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.budget-estimation {
|
||||||
|
background: #e3f2fd;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.company-info {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audit-container {
|
||||||
|
margin: 10px;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="audit-container">
|
||||||
|
<!-- En-tête -->
|
||||||
|
<div class="audit-header">
|
||||||
|
<h1>🦁 Audit Gratuit de Maturité Digitale</h1>
|
||||||
|
<p>Évaluez le niveau de digitalisation de votre PME en 10 minutes</p>
|
||||||
|
<p><strong>100% Gratuit • Rapport PDF personnalisé • Recommandations d'experts</strong></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Barre de progression -->
|
||||||
|
<div class="audit-progress">
|
||||||
|
<div class="audit-progress-bar" id="progressBar" style="width: 0%"></div>
|
||||||
|
</div>
|
||||||
|
<div style="text-align: center; margin-bottom: 20px;">
|
||||||
|
<span id="progressText">Étape 1 sur 6</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Formulaire d'audit -->
|
||||||
|
<form id="auditForm">
|
||||||
|
<!-- Section 0: Informations entreprise -->
|
||||||
|
<div class="audit-section active" data-section="0">
|
||||||
|
<div class="section-title">
|
||||||
|
<div class="section-icon" style="background: #4CAF50; color: white;">🏢</div>
|
||||||
|
Informations sur votre entreprise
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="company-info">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="companyName">Nom de l'entreprise *</label>
|
||||||
|
<input type="text" id="companyName" name="companyName" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="contactName">Votre nom *</label>
|
||||||
|
<input type="text" id="contactName" name="contactName" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="email">Email professionnel *</label>
|
||||||
|
<input type="email" id="email" name="email" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="phone">Téléphone</label>
|
||||||
|
<input type="tel" id="phone" name="phone">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="sector">Secteur d'activité</label>
|
||||||
|
<select id="sector" name="sector">
|
||||||
|
<option value="">Sélectionnez...</option>
|
||||||
|
<option value="commerce">Commerce/Distribution</option>
|
||||||
|
<option value="services">Services</option>
|
||||||
|
<option value="industrie">Industrie/Manufacturing</option>
|
||||||
|
<option value="agriculture">Agriculture/Agro-alimentaire</option>
|
||||||
|
<option value="btp">BTP/Construction</option>
|
||||||
|
<option value="transport">Transport/Logistique</option>
|
||||||
|
<option value="sante">Santé</option>
|
||||||
|
<option value="education">Éducation/Formation</option>
|
||||||
|
<option value="autre">Autre</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="employeeCount">Nombre d'employés</label>
|
||||||
|
<select id="employeeCount" name="employeeCount">
|
||||||
|
<option value="">Sélectionnez...</option>
|
||||||
|
<option value="1">1-5 employés</option>
|
||||||
|
<option value="6">6-10 employés</option>
|
||||||
|
<option value="11">11-20 employés</option>
|
||||||
|
<option value="21">21-50 employés</option>
|
||||||
|
<option value="51">51-100 employés</option>
|
||||||
|
<option value="101">Plus de 100 employés</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="turnover">Chiffre d'affaires annuel</label>
|
||||||
|
<select id="turnover" name="turnover">
|
||||||
|
<option value="">Sélectionnez...</option>
|
||||||
|
<option value="< 10M">Moins de 10M FCFA</option>
|
||||||
|
<option value="10-50M">10-50M FCFA</option>
|
||||||
|
<option value="50-100M">50-100M FCFA</option>
|
||||||
|
<option value="100-500M">100-500M FCFA</option>
|
||||||
|
<option value="500M-1B">500M-1B FCFA</option>
|
||||||
|
<option value="> 1B">Plus de 1B FCFA</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Les sections de questions seront générées dynamiquement -->
|
||||||
|
<div id="questionsContainer"></div>
|
||||||
|
|
||||||
|
<!-- Section résultats -->
|
||||||
|
<div class="results-container" id="resultsContainer">
|
||||||
|
<h2>🎉 Votre Audit est Terminé !</h2>
|
||||||
|
<div class="score-display" id="scoreDisplay">---%</div>
|
||||||
|
<div id="maturityLevel"></div>
|
||||||
|
|
||||||
|
<div class="recommendations" id="recommendationsDisplay">
|
||||||
|
<!-- Recommandations générées -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="budget-estimation" id="budgetDisplay">
|
||||||
|
<!-- Estimation budgétaire -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p><strong>📧 Votre rapport détaillé vous a été envoyé par email</strong></p>
|
||||||
|
<p>Notre équipe vous contactera dans les 24h pour planifier un rendez-vous diagnostic gratuit.</p>
|
||||||
|
|
||||||
|
<button type="button" class="btn btn-primary" onclick="requestMeeting()">
|
||||||
|
📅 Demander un Rendez-vous Maintenant
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Navigation -->
|
||||||
|
<div class="navigation">
|
||||||
|
<button type="button" class="btn btn-secondary" id="prevBtn" onclick="previousSection()" style="display: none;">
|
||||||
|
← Précédent
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-primary" id="nextBtn" onclick="nextSection()">
|
||||||
|
Suivant →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Variables globales
|
||||||
|
let currentSection = 0;
|
||||||
|
let questions = {};
|
||||||
|
let answers = {};
|
||||||
|
let totalSections = 0;
|
||||||
|
|
||||||
|
// Initialisation
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
loadQuestions();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Chargement des questions depuis l'API
|
||||||
|
async function loadQuestions() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/audit/questions');
|
||||||
|
questions = await response.json();
|
||||||
|
generateQuestionSections();
|
||||||
|
updateProgress();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur chargement questions:', error);
|
||||||
|
alert('Erreur lors du chargement du questionnaire. Veuillez rafraîchir la page.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Génération des sections de questions
|
||||||
|
function generateQuestionSections() {
|
||||||
|
const container = document.getElementById('questionsContainer');
|
||||||
|
const categories = Object.keys(questions);
|
||||||
|
totalSections = categories.length + 1; // +1 pour les infos entreprise
|
||||||
|
|
||||||
|
categories.forEach((category, index) => {
|
||||||
|
const section = createQuestionSection(category, questions[category], index + 1);
|
||||||
|
container.appendChild(section);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Création d'une section de questions
|
||||||
|
function createQuestionSection(category, categoryQuestions, sectionIndex) {
|
||||||
|
const section = document.createElement('div');
|
||||||
|
section.className = 'audit-section';
|
||||||
|
section.setAttribute('data-section', sectionIndex);
|
||||||
|
|
||||||
|
const categoryIcons = {
|
||||||
|
'commercial': '💼',
|
||||||
|
'stock': '📦',
|
||||||
|
'comptabilite': '💰',
|
||||||
|
'rh': '👥',
|
||||||
|
'infrastructure': '🖥️'
|
||||||
|
};
|
||||||
|
|
||||||
|
const categoryNames = {
|
||||||
|
'commercial': 'Gestion Commerciale',
|
||||||
|
'stock': 'Gestion des Stocks',
|
||||||
|
'comptabilite': 'Comptabilité',
|
||||||
|
'rh': 'Ressources Humaines',
|
||||||
|
'infrastructure': 'Infrastructure IT'
|
||||||
|
};
|
||||||
|
|
||||||
|
section.innerHTML = `
|
||||||
|
<div class="section-title">
|
||||||
|
<div class="section-icon" style="background: #667eea; color: white;">
|
||||||
|
${categoryIcons[category] || '📋'}
|
||||||
|
</div>
|
||||||
|
${categoryNames[category] || category}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
categoryQuestions.forEach(question => {
|
||||||
|
const questionDiv = createQuestionElement(question);
|
||||||
|
section.appendChild(questionDiv);
|
||||||
|
});
|
||||||
|
|
||||||
|
return section;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Création d'un élément question
|
||||||
|
function createQuestionElement(question) {
|
||||||
|
const questionDiv = document.createElement('div');
|
||||||
|
questionDiv.className = 'question';
|
||||||
|
|
||||||
|
const optionsHtml = question.options.map((option, index) => `
|
||||||
|
<div class="option" onclick="selectOption(${question.id}, ${index}, this)">
|
||||||
|
<input type="radio" name="question_${question.id}" value="${index}" style="display: none;">
|
||||||
|
${option}
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
questionDiv.innerHTML = `
|
||||||
|
<div class="question-text">${question.question}</div>
|
||||||
|
<div class="question-options">
|
||||||
|
${optionsHtml}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
return questionDiv;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sélection d'une option
|
||||||
|
function selectOption(questionId, optionIndex, element) {
|
||||||
|
// Désélectionner les autres options de cette question
|
||||||
|
const questionDiv = element.closest('.question');
|
||||||
|
questionDiv.querySelectorAll('.option').forEach(opt => opt.classList.remove('selected'));
|
||||||
|
|
||||||
|
// Sélectionner l'option cliquée
|
||||||
|
element.classList.add('selected');
|
||||||
|
element.querySelector('input').checked = true;
|
||||||
|
|
||||||
|
// Enregistrer la réponse
|
||||||
|
answers[questionId] = optionIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
function nextSection() {
|
||||||
|
if (currentSection === 0) {
|
||||||
|
// Validation des informations entreprise
|
||||||
|
if (!validateCompanyInfo()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentSection < totalSections) {
|
||||||
|
document.querySelector(`[data-section="${currentSection}"]`).classList.remove('active');
|
||||||
|
currentSection++;
|
||||||
|
|
||||||
|
if (currentSection < totalSections) {
|
||||||
|
document.querySelector(`[data-section="${currentSection}"]`).classList.add('active');
|
||||||
|
updateProgress();
|
||||||
|
updateNavigation();
|
||||||
|
} else {
|
||||||
|
// Soumission de l'audit
|
||||||
|
submitAudit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function previousSection() {
|
||||||
|
if (currentSection > 0) {
|
||||||
|
document.querySelector(`[data-section="${currentSection}"]`).classList.remove('active');
|
||||||
|
currentSection--;
|
||||||
|
document.querySelector(`[data-section="${currentSection}"]`).classList.add('active');
|
||||||
|
updateProgress();
|
||||||
|
updateNavigation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mise à jour de la barre de progression
|
||||||
|
function updateProgress() {
|
||||||
|
const progress = (currentSection / totalSections) * 100;
|
||||||
|
document.getElementById('progressBar').style.width = progress + '%';
|
||||||
|
document.getElementById('progressText').textContent = `Étape ${currentSection + 1} sur ${totalSections + 1}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mise à jour de la navigation
|
||||||
|
function updateNavigation() {
|
||||||
|
const prevBtn = document.getElementById('prevBtn');
|
||||||
|
const nextBtn = document.getElementById('nextBtn');
|
||||||
|
|
||||||
|
prevBtn.style.display = currentSection > 0 ? 'block' : 'none';
|
||||||
|
nextBtn.textContent = currentSection === totalSections - 1 ? 'Terminer l\'Audit' : 'Suivant →';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation des informations entreprise
|
||||||
|
function validateCompanyInfo() {
|
||||||
|
const companyName = document.getElementById('companyName').value.trim();
|
||||||
|
const contactName = document.getElementById('contactName').value.trim();
|
||||||
|
const email = document.getElementById('email').value.trim();
|
||||||
|
|
||||||
|
if (!companyName || !contactName || !email) {
|
||||||
|
alert('Veuillez remplir tous les champs obligatoires (*)');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isValidEmail(email)) {
|
||||||
|
alert('Veuillez saisir un email valide');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation email
|
||||||
|
function isValidEmail(email) {
|
||||||
|
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Soumission de l'audit
|
||||||
|
async function submitAudit() {
|
||||||
|
try {
|
||||||
|
// Affichage du loading
|
||||||
|
document.getElementById('nextBtn').textContent = 'Traitement en cours...';
|
||||||
|
document.getElementById('nextBtn').disabled = true;
|
||||||
|
|
||||||
|
const submission = {
|
||||||
|
companyName: document.getElementById('companyName').value,
|
||||||
|
contactName: document.getElementById('contactName').value,
|
||||||
|
email: document.getElementById('email').value,
|
||||||
|
phone: document.getElementById('phone').value,
|
||||||
|
sector: document.getElementById('sector').value,
|
||||||
|
employeeCount: parseInt(document.getElementById('employeeCount').value) || null,
|
||||||
|
turnover: document.getElementById('turnover').value,
|
||||||
|
answers: answers
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch('/api/audit/submit', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(submission)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const result = await response.json();
|
||||||
|
displayResults(result);
|
||||||
|
} else {
|
||||||
|
const error = await response.json();
|
||||||
|
alert('Erreur: ' + (error.error || 'Erreur lors de la soumission'));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur soumission:', error);
|
||||||
|
alert('Erreur lors de la soumission. Veuillez réessayer.');
|
||||||
|
} finally {
|
||||||
|
document.getElementById('nextBtn').disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Affichage des résultats
|
||||||
|
function displayResults(result) {
|
||||||
|
// Masquer le formulaire
|
||||||
|
document.querySelector('.navigation').style.display = 'none';
|
||||||
|
document.querySelector(`[data-section="${currentSection}"]`).classList.remove('active');
|
||||||
|
|
||||||
|
// Afficher les résultats
|
||||||
|
const resultsContainer = document.getElementById('resultsContainer');
|
||||||
|
resultsContainer.style.display = 'block';
|
||||||
|
|
||||||
|
// Score
|
||||||
|
const scoreDisplay = document.getElementById('scoreDisplay');
|
||||||
|
scoreDisplay.textContent = result.maturityPercentage.toFixed(1) + '%';
|
||||||
|
|
||||||
|
// Couleur du score
|
||||||
|
if (result.maturityPercentage >= 80) {
|
||||||
|
scoreDisplay.className = 'score-display score-excellent';
|
||||||
|
document.getElementById('maturityLevel').textContent = '🏆 Niveau Expert';
|
||||||
|
} else if (result.maturityPercentage >= 60) {
|
||||||
|
scoreDisplay.className = 'score-display score-good';
|
||||||
|
document.getElementById('maturityLevel').textContent = '✅ Niveau Avancé';
|
||||||
|
} else if (result.maturityPercentage >= 30) {
|
||||||
|
scoreDisplay.className = 'score-display score-average';
|
||||||
|
document.getElementById('maturityLevel').textContent = '⚠️ Niveau Intermédiaire';
|
||||||
|
} else {
|
||||||
|
scoreDisplay.className = 'score-display score-poor';
|
||||||
|
document.getElementById('maturityLevel').textContent = '🚨 Niveau Débutant';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recommandations
|
||||||
|
document.getElementById('recommendationsDisplay').innerHTML = `
|
||||||
|
<h3>📋 Recommandations Personnalisées</h3>
|
||||||
|
<p>${result.recommendations.replace(/\n/g, '<br>')}</p>
|
||||||
|
<p><strong>Actions prioritaires:</strong> ${result.priorityActions}</p>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Budget
|
||||||
|
if (result.estimatedBudgetMin && result.estimatedBudgetMax) {
|
||||||
|
document.getElementById('budgetDisplay').innerHTML = `
|
||||||
|
<h3>💰 Estimation Budgétaire</h3>
|
||||||
|
<p>Investissement estimé pour votre digitalisation:</p>
|
||||||
|
<p><strong>${result.estimatedBudgetMin.toLocaleString()} - ${result.estimatedBudgetMax.toLocaleString()} FCFA</strong></p>
|
||||||
|
<p><small>* Estimation basée sur votre audit. Devis personnalisé après analyse détaillée.</small></p>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mise à jour de la progression
|
||||||
|
document.getElementById('progressBar').style.width = '100%';
|
||||||
|
document.getElementById('progressText').textContent = 'Audit Terminé ✅';
|
||||||
|
|
||||||
|
// Scroll vers les résultats
|
||||||
|
resultsContainer.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Demande de rendez-vous
|
||||||
|
function requestMeeting() {
|
||||||
|
// Redirection vers formulaire de RDV ou ouverture modal
|
||||||
|
window.location.href = '/contact?source=audit';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
419
src/main/resources/META-INF/resources/pme.html
Normal file
419
src/main/resources/META-INF/resources/pme.html
Normal file
@@ -0,0 +1,419 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Lions Dev - Solutions ERP pour PME Ivoiriennes</title>
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
<link rel="icon" href="favicon.ico">
|
||||||
|
<style>
|
||||||
|
/* PME-specific styles */
|
||||||
|
.hero-pme {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 80px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.hero-pme h1 {
|
||||||
|
font-size: 3rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.hero-pme .highlight {
|
||||||
|
color: #ffd700;
|
||||||
|
}
|
||||||
|
.hero-stats {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 3rem;
|
||||||
|
margin: 2rem 0;
|
||||||
|
}
|
||||||
|
.stat {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.stat-number {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #ffd700;
|
||||||
|
}
|
||||||
|
.stat-label {
|
||||||
|
font-size: 1rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
.solutions-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 2rem;
|
||||||
|
margin: 3rem 0;
|
||||||
|
}
|
||||||
|
.solution-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 2rem;
|
||||||
|
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.solution-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
.solution-card.popular::before {
|
||||||
|
content: "Plus populaire";
|
||||||
|
position: absolute;
|
||||||
|
top: -10px;
|
||||||
|
right: 20px;
|
||||||
|
background: #ff6b6b;
|
||||||
|
color: white;
|
||||||
|
padding: 5px 15px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
.solution-icon {
|
||||||
|
font-size: 3rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.solution-features {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
.solution-features li {
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
.solution-features li:before {
|
||||||
|
content: "✓";
|
||||||
|
color: #28a745;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
.solution-price {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #667eea;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 12px 24px;
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: background 0.3s ease;
|
||||||
|
}
|
||||||
|
.btn:hover {
|
||||||
|
background: #5a6fd8;
|
||||||
|
}
|
||||||
|
.btn-primary {
|
||||||
|
background: #667eea;
|
||||||
|
}
|
||||||
|
.btn-secondary {
|
||||||
|
background: #6c757d;
|
||||||
|
}
|
||||||
|
.btn-large {
|
||||||
|
padding: 16px 32px;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 2rem;
|
||||||
|
}
|
||||||
|
.problems-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 2rem;
|
||||||
|
margin: 3rem 0;
|
||||||
|
}
|
||||||
|
.problem-card {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 2rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.problem-icon {
|
||||||
|
font-size: 3rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.process-steps {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 2rem;
|
||||||
|
margin: 3rem 0;
|
||||||
|
}
|
||||||
|
.step {
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
.step-number {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0 auto 1rem;
|
||||||
|
}
|
||||||
|
.cta-section {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 4rem 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.navbar {
|
||||||
|
background: white;
|
||||||
|
padding: 1rem 0;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
.nav-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.nav-menu {
|
||||||
|
display: flex;
|
||||||
|
list-style: none;
|
||||||
|
gap: 2rem;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.nav-menu a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.logo {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #667eea;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
background: #333;
|
||||||
|
color: white;
|
||||||
|
padding: 3rem 0 1rem;
|
||||||
|
}
|
||||||
|
.footer-content {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
.footer-bottom {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 2rem;
|
||||||
|
padding-top: 2rem;
|
||||||
|
border-top: 1px solid #555;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav class="navbar">
|
||||||
|
<div class="container">
|
||||||
|
<div class="nav-content">
|
||||||
|
<a href="/" class="logo">🦁 Lions Dev</a>
|
||||||
|
<ul class="nav-menu">
|
||||||
|
<li><a href="#solutions">Solutions</a></li>
|
||||||
|
<li><a href="audit.html">Audit Gratuit</a></li>
|
||||||
|
<li><a href="roi-calculator.html">ROI</a></li>
|
||||||
|
<li><a href="#contact">Contact</a></li>
|
||||||
|
</ul>
|
||||||
|
<a href="audit.html" class="btn btn-primary">Audit Gratuit</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<!-- Hero Section -->
|
||||||
|
<section class="hero-pme">
|
||||||
|
<div class="container">
|
||||||
|
<h1>
|
||||||
|
Digitalisez votre PME avec des solutions ERP
|
||||||
|
<span class="highlight">100% adaptées à la Côte d'Ivoire</span>
|
||||||
|
</h1>
|
||||||
|
<p style="font-size: 1.2rem; margin: 1rem 0;">
|
||||||
|
Gestion intégrée • Conformité fiscale • ROI garanti • Support local
|
||||||
|
</p>
|
||||||
|
<div class="hero-stats">
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-number">50+</div>
|
||||||
|
<div class="stat-label">PME accompagnées</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-number">300%</div>
|
||||||
|
<div class="stat-label">ROI moyen</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-number">6 mois</div>
|
||||||
|
<div class="stat-label">Retour sur investissement</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 2rem;">
|
||||||
|
<a href="audit.html" class="btn btn-large" style="background: #ffd700; color: #333; margin-right: 1rem;">
|
||||||
|
🎯 Audit Gratuit de Maturité Digitale
|
||||||
|
</a>
|
||||||
|
<a href="roi-calculator.html" class="btn btn-large btn-secondary">
|
||||||
|
💰 Calculer mon ROI
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Problems Section -->
|
||||||
|
<section style="padding: 4rem 0;">
|
||||||
|
<div class="container">
|
||||||
|
<h2 style="text-align: center; margin-bottom: 3rem;">Les défis des PME ivoiriennes</h2>
|
||||||
|
<div class="problems-grid">
|
||||||
|
<div class="problem-card">
|
||||||
|
<div class="problem-icon">📊</div>
|
||||||
|
<h3>Gestion manuelle</h3>
|
||||||
|
<p>Excel, papier, calculs manuels... Perte de temps et erreurs fréquentes</p>
|
||||||
|
</div>
|
||||||
|
<div class="problem-card">
|
||||||
|
<div class="problem-icon">⚖️</div>
|
||||||
|
<h3>Conformité fiscale</h3>
|
||||||
|
<p>TVA, IS, CNPS... Déclarations complexes et risques de pénalités</p>
|
||||||
|
</div>
|
||||||
|
<div class="problem-card">
|
||||||
|
<div class="problem-icon">📈</div>
|
||||||
|
<h3>Manque de visibilité</h3>
|
||||||
|
<p>Pas de tableaux de bord, décisions prises "au feeling"</p>
|
||||||
|
</div>
|
||||||
|
<div class="problem-card">
|
||||||
|
<div class="problem-icon">💸</div>
|
||||||
|
<h3>Coûts cachés</h3>
|
||||||
|
<p>Erreurs, retards, temps perdu... Impact sur la rentabilité</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Solutions Section -->
|
||||||
|
<section id="solutions" style="padding: 4rem 0; background: #f8f9fa;">
|
||||||
|
<div class="container">
|
||||||
|
<h2 style="text-align: center; margin-bottom: 3rem;">Nos Solutions ERP Modulaires</h2>
|
||||||
|
<div class="solutions-grid">
|
||||||
|
<div class="solution-card popular">
|
||||||
|
<div class="solution-icon">🤝</div>
|
||||||
|
<h3>CRM Commercial</h3>
|
||||||
|
<p>Gestion complète de la relation client</p>
|
||||||
|
<ul class="solution-features">
|
||||||
|
<li>Pipeline de ventes</li>
|
||||||
|
<li>Suivi des prospects</li>
|
||||||
|
<li>Facturation intégrée</li>
|
||||||
|
<li>Rapports commerciaux</li>
|
||||||
|
</ul>
|
||||||
|
<div class="solution-price">À partir de 150K FCFA</div>
|
||||||
|
<a href="quote-configurator.html?module=CRM" class="btn">Configurer</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="solution-card">
|
||||||
|
<div class="solution-icon">📦</div>
|
||||||
|
<h3>Gestion des Stocks</h3>
|
||||||
|
<p>Inventaire automatisé avec codes-barres</p>
|
||||||
|
<ul class="solution-features">
|
||||||
|
<li>Multi-entrepôts</li>
|
||||||
|
<li>Codes-barres/QR codes</li>
|
||||||
|
<li>Alertes stock minimum</li>
|
||||||
|
<li>Valorisation FIFO/LIFO</li>
|
||||||
|
</ul>
|
||||||
|
<div class="solution-price">À partir de 120K FCFA</div>
|
||||||
|
<a href="quote-configurator.html?module=STOCK" class="btn">Configurer</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="solution-card popular">
|
||||||
|
<div class="solution-icon">💼</div>
|
||||||
|
<h3>Comptabilité Intégrée</h3>
|
||||||
|
<p>Conforme aux normes ivoiriennes</p>
|
||||||
|
<ul class="solution-features">
|
||||||
|
<li>Plan comptable SYSCOHADA</li>
|
||||||
|
<li>Déclarations TVA/IS automatiques</li>
|
||||||
|
<li>Intégration bancaire</li>
|
||||||
|
<li>Rapports DGI</li>
|
||||||
|
</ul>
|
||||||
|
<div class="solution-price">À partir de 180K FCFA</div>
|
||||||
|
<a href="quote-configurator.html?module=COMPTA" class="btn">Configurer</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Process Section -->
|
||||||
|
<section style="padding: 4rem 0;">
|
||||||
|
<div class="container">
|
||||||
|
<h2 style="text-align: center; margin-bottom: 3rem;">Notre Approche en 4 Étapes</h2>
|
||||||
|
<div class="process-steps">
|
||||||
|
<div class="step">
|
||||||
|
<div class="step-number">1</div>
|
||||||
|
<h3>Audit Gratuit</h3>
|
||||||
|
<p>Évaluation de votre maturité digitale en 15 minutes</p>
|
||||||
|
<a href="audit.html" class="btn">Commencer l'audit</a>
|
||||||
|
</div>
|
||||||
|
<div class="step">
|
||||||
|
<div class="step-number">2</div>
|
||||||
|
<h3>Devis Personnalisé</h3>
|
||||||
|
<p>Configuration sur mesure selon vos besoins</p>
|
||||||
|
<a href="quote-configurator.html" class="btn">Configurer</a>
|
||||||
|
</div>
|
||||||
|
<div class="step">
|
||||||
|
<div class="step-number">3</div>
|
||||||
|
<h3>Implémentation</h3>
|
||||||
|
<p>Déploiement et formation de vos équipes</p>
|
||||||
|
</div>
|
||||||
|
<div class="step">
|
||||||
|
<div class="step-number">4</div>
|
||||||
|
<h3>Support Continu</h3>
|
||||||
|
<p>Accompagnement et évolutions</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- CTA Section -->
|
||||||
|
<section class="cta-section">
|
||||||
|
<div class="container">
|
||||||
|
<h2>Prêt à digitaliser votre PME ?</h2>
|
||||||
|
<p>Rejoignez les 50+ entreprises ivoiriennes qui nous font confiance</p>
|
||||||
|
<div style="margin-top: 2rem;">
|
||||||
|
<a href="audit.html" class="btn btn-large" style="background: #667eea; margin-right: 1rem;">
|
||||||
|
🎯 Audit Gratuit (15 min)
|
||||||
|
</a>
|
||||||
|
<a href="tel:+2250123456789" class="btn btn-large btn-secondary">
|
||||||
|
📞 Appeler maintenant
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="footer">
|
||||||
|
<div class="container">
|
||||||
|
<div class="footer-content">
|
||||||
|
<div>
|
||||||
|
<h4>Lions Dev</h4>
|
||||||
|
<p>Solutions ERP pour PME ivoiriennes</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4>Solutions</h4>
|
||||||
|
<ul style="list-style: none; padding: 0;">
|
||||||
|
<li><a href="#solutions" style="color: #ccc;">CRM Commercial</a></li>
|
||||||
|
<li><a href="#solutions" style="color: #ccc;">Gestion Stocks</a></li>
|
||||||
|
<li><a href="#solutions" style="color: #ccc;">Comptabilité</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4>Contact</h4>
|
||||||
|
<p>📍 Abidjan, Côte d'Ivoire</p>
|
||||||
|
<p>📞 +225 01 23 45 67 89</p>
|
||||||
|
<p>✉️ contact@lionsdev.ci</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="footer-bottom">
|
||||||
|
<p>© 2024 Lions Dev. Tous droits réservés.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
569
src/main/resources/META-INF/resources/quote-configurator.html
Normal file
569
src/main/resources/META-INF/resources/quote-configurator.html
Normal file
@@ -0,0 +1,569 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Configurateur de Devis - Lions Dev</title>
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
<style>
|
||||||
|
.configurator-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.configurator-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
padding: 30px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modules-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-card {
|
||||||
|
background: white;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-card:hover {
|
||||||
|
border-color: #667eea;
|
||||||
|
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-card.selected {
|
||||||
|
border-color: #667eea;
|
||||||
|
background: #f0f4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-title {
|
||||||
|
font-size: 1.3em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-price {
|
||||||
|
font-size: 1.1em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-description {
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-features {
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #555;
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
|
||||||
|
.complexity-selector {
|
||||||
|
margin-top: 15px;
|
||||||
|
padding: 10px;
|
||||||
|
background: #f9f9f9;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-card.selected .complexity-selector {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.complexity-options {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.complexity-option {
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.complexity-option:hover {
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.complexity-option.selected {
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.services-section {
|
||||||
|
background: white;
|
||||||
|
padding: 25px;
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.services-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 20px;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quote-summary {
|
||||||
|
background: white;
|
||||||
|
padding: 25px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
position: sticky;
|
||||||
|
top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-line {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-line.total {
|
||||||
|
border-top: 2px solid #667eea;
|
||||||
|
padding-top: 15px;
|
||||||
|
margin-top: 15px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.2em;
|
||||||
|
color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: #5a6fd8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #f8f9fa;
|
||||||
|
color: #333;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popular-badge {
|
||||||
|
background: #ff6b6b;
|
||||||
|
color: white;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.8em;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.modules-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.services-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.complexity-options {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="configurator-container">
|
||||||
|
<!-- En-tête -->
|
||||||
|
<div class="configurator-header">
|
||||||
|
<h1>🛠️ Configurateur de Devis Personnalisé</h1>
|
||||||
|
<p>Sélectionnez les modules qui correspondent à vos besoins</p>
|
||||||
|
<p><strong>Devis gratuit • Personnalisation complète • Support inclus</strong></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: grid; grid-template-columns: 2fr 1fr; gap: 30px;">
|
||||||
|
<!-- Configuration des modules -->
|
||||||
|
<div>
|
||||||
|
<h2>📦 Modules Disponibles</h2>
|
||||||
|
<div class="modules-grid" id="modulesGrid">
|
||||||
|
<!-- Les modules seront chargés dynamiquement -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Services additionnels -->
|
||||||
|
<div class="services-section">
|
||||||
|
<h3>🎓 Services Additionnels</h3>
|
||||||
|
<div class="services-grid">
|
||||||
|
<div class="service-item">
|
||||||
|
<label>Formation (heures):</label>
|
||||||
|
<input type="number" id="formationHours" class="service-input"
|
||||||
|
value="8" min="0" max="100" onchange="updateSummary()">
|
||||||
|
<span>15 000 FCFA/h</span>
|
||||||
|
</div>
|
||||||
|
<div class="service-item">
|
||||||
|
<label>Support (mois):</label>
|
||||||
|
<input type="number" id="supportMonths" class="service-input"
|
||||||
|
value="3" min="0" max="24" onchange="updateSummary()">
|
||||||
|
<span>25 000 FCFA/mois</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Résumé du devis -->
|
||||||
|
<div>
|
||||||
|
<div class="quote-summary">
|
||||||
|
<h3>💰 Résumé du Devis</h3>
|
||||||
|
|
||||||
|
<div id="selectedModules">
|
||||||
|
<!-- Modules sélectionnés -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="summary-line">
|
||||||
|
<span>Sous-total modules:</span>
|
||||||
|
<span id="subtotalModules">0 FCFA</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="summary-line">
|
||||||
|
<span>Formation:</span>
|
||||||
|
<span id="formationCost">120 000 FCFA</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="summary-line">
|
||||||
|
<span>Support:</span>
|
||||||
|
<span id="supportCost">75 000 FCFA</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="summary-line">
|
||||||
|
<span>Total HT:</span>
|
||||||
|
<span id="totalHT">0 FCFA</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="summary-line">
|
||||||
|
<span>TVA (18%):</span>
|
||||||
|
<span id="vatAmount">0 FCFA</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="summary-line total">
|
||||||
|
<span>TOTAL TTC:</span>
|
||||||
|
<span id="totalTTC">0 FCFA</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="action-buttons">
|
||||||
|
<button class="btn btn-secondary" onclick="resetConfiguration()">
|
||||||
|
🔄 Réinitialiser
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary" onclick="generateQuote()">
|
||||||
|
📄 Générer Devis
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 15px; text-align: center;">
|
||||||
|
<small style="color: #666;">
|
||||||
|
💡 Devis personnalisé gratuit<br>
|
||||||
|
📞 Conseil expert inclus<br>
|
||||||
|
⚡ Réponse sous 24h
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Variables globales
|
||||||
|
let catalog = [];
|
||||||
|
let selectedModules = new Map();
|
||||||
|
let auditId = null;
|
||||||
|
|
||||||
|
// Initialisation
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Récupérer l'ID d'audit depuis l'URL si présent
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
auditId = urlParams.get('audit');
|
||||||
|
|
||||||
|
loadModuleCatalog();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Chargement du catalogue
|
||||||
|
async function loadModuleCatalog() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/quotes/catalog');
|
||||||
|
catalog = await response.json();
|
||||||
|
renderModules();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur chargement catalogue:', error);
|
||||||
|
alert('Erreur lors du chargement du catalogue');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Affichage des modules
|
||||||
|
function renderModules() {
|
||||||
|
const grid = document.getElementById('modulesGrid');
|
||||||
|
grid.innerHTML = '';
|
||||||
|
|
||||||
|
catalog.forEach(module => {
|
||||||
|
const moduleCard = createModuleCard(module);
|
||||||
|
grid.appendChild(moduleCard);
|
||||||
|
});
|
||||||
|
|
||||||
|
updateSummary();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Création d'une carte module
|
||||||
|
function createModuleCard(module) {
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = 'module-card';
|
||||||
|
card.onclick = () => toggleModule(module.moduleCode);
|
||||||
|
|
||||||
|
const popularBadge = module.popular ? '<span class="popular-badge">Populaire</span>' : '';
|
||||||
|
|
||||||
|
card.innerHTML = `
|
||||||
|
<div class="module-header">
|
||||||
|
<div class="module-title">${module.moduleName}${popularBadge}</div>
|
||||||
|
<div class="module-price">À partir de ${formatCurrency(module.basicPrice)}</div>
|
||||||
|
</div>
|
||||||
|
<div class="module-description">${module.description}</div>
|
||||||
|
<div class="module-features">${module.features}</div>
|
||||||
|
|
||||||
|
<div class="complexity-selector">
|
||||||
|
<label><strong>Niveau de complexité:</strong></label>
|
||||||
|
<div class="complexity-options">
|
||||||
|
<div class="complexity-option" data-level="BASIC" onclick="selectComplexity('${module.moduleCode}', 'BASIC', ${module.basicPrice}, event)">
|
||||||
|
Basique<br><small>${formatCurrency(module.basicPrice)}</small>
|
||||||
|
</div>
|
||||||
|
<div class="complexity-option selected" data-level="STANDARD" onclick="selectComplexity('${module.moduleCode}', 'STANDARD', ${module.standardPrice}, event)">
|
||||||
|
Standard<br><small>${formatCurrency(module.standardPrice)}</small>
|
||||||
|
</div>
|
||||||
|
<div class="complexity-option" data-level="ADVANCED" onclick="selectComplexity('${module.moduleCode}', 'ADVANCED', ${module.advancedPrice}, event)">
|
||||||
|
Avancé<br><small>${formatCurrency(module.advancedPrice)}</small>
|
||||||
|
</div>
|
||||||
|
<div class="complexity-option" data-level="ENTERPRISE" onclick="selectComplexity('${module.moduleCode}', 'ENTERPRISE', ${module.enterprisePrice}, event)">
|
||||||
|
Enterprise<br><small>${formatCurrency(module.enterprisePrice)}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sélection/désélection d'un module
|
||||||
|
function toggleModule(moduleCode) {
|
||||||
|
const card = event.currentTarget;
|
||||||
|
|
||||||
|
if (selectedModules.has(moduleCode)) {
|
||||||
|
// Désélectionner
|
||||||
|
selectedModules.delete(moduleCode);
|
||||||
|
card.classList.remove('selected');
|
||||||
|
} else {
|
||||||
|
// Sélectionner avec niveau standard par défaut
|
||||||
|
const module = catalog.find(m => m.moduleCode === moduleCode);
|
||||||
|
selectedModules.set(moduleCode, {
|
||||||
|
module: module,
|
||||||
|
complexity: 'STANDARD',
|
||||||
|
price: module.standardPrice
|
||||||
|
});
|
||||||
|
card.classList.add('selected');
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSummary();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sélection du niveau de complexité
|
||||||
|
function selectComplexity(moduleCode, level, price, event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
// Mise à jour visuelle
|
||||||
|
const card = event.target.closest('.module-card');
|
||||||
|
card.querySelectorAll('.complexity-option').forEach(opt => opt.classList.remove('selected'));
|
||||||
|
event.target.classList.add('selected');
|
||||||
|
|
||||||
|
// Mise à jour des données
|
||||||
|
if (selectedModules.has(moduleCode)) {
|
||||||
|
const selection = selectedModules.get(moduleCode);
|
||||||
|
selection.complexity = level;
|
||||||
|
selection.price = price;
|
||||||
|
updateSummary();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mise à jour du résumé
|
||||||
|
function updateSummary() {
|
||||||
|
const selectedDiv = document.getElementById('selectedModules');
|
||||||
|
selectedDiv.innerHTML = '';
|
||||||
|
|
||||||
|
let subtotalModules = 0;
|
||||||
|
|
||||||
|
// Affichage des modules sélectionnés
|
||||||
|
selectedModules.forEach((selection, moduleCode) => {
|
||||||
|
const moduleDiv = document.createElement('div');
|
||||||
|
moduleDiv.className = 'summary-line';
|
||||||
|
moduleDiv.innerHTML = `
|
||||||
|
<span>${selection.module.moduleName} (${selection.complexity})</span>
|
||||||
|
<span>${formatCurrency(selection.price)}</span>
|
||||||
|
`;
|
||||||
|
selectedDiv.appendChild(moduleDiv);
|
||||||
|
subtotalModules += selection.price;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calculs
|
||||||
|
const formationHours = parseFloat(document.getElementById('formationHours').value) || 0;
|
||||||
|
const supportMonths = parseFloat(document.getElementById('supportMonths').value) || 0;
|
||||||
|
|
||||||
|
const formationCost = formationHours * 15000;
|
||||||
|
const supportCost = supportMonths * 25000;
|
||||||
|
const totalHT = subtotalModules + formationCost + supportCost;
|
||||||
|
const vatAmount = totalHT * 0.18;
|
||||||
|
const totalTTC = totalHT + vatAmount;
|
||||||
|
|
||||||
|
// Mise à jour de l'affichage
|
||||||
|
document.getElementById('subtotalModules').textContent = formatCurrency(subtotalModules);
|
||||||
|
document.getElementById('formationCost').textContent = formatCurrency(formationCost);
|
||||||
|
document.getElementById('supportCost').textContent = formatCurrency(supportCost);
|
||||||
|
document.getElementById('totalHT').textContent = formatCurrency(totalHT);
|
||||||
|
document.getElementById('vatAmount').textContent = formatCurrency(vatAmount);
|
||||||
|
document.getElementById('totalTTC').textContent = formatCurrency(totalTTC);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Réinitialisation
|
||||||
|
function resetConfiguration() {
|
||||||
|
selectedModules.clear();
|
||||||
|
document.querySelectorAll('.module-card').forEach(card => {
|
||||||
|
card.classList.remove('selected');
|
||||||
|
});
|
||||||
|
document.getElementById('formationHours').value = 8;
|
||||||
|
document.getElementById('supportMonths').value = 3;
|
||||||
|
updateSummary();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Génération du devis
|
||||||
|
async function generateQuote() {
|
||||||
|
if (selectedModules.size === 0) {
|
||||||
|
alert('Veuillez sélectionner au moins un module');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collecte des données
|
||||||
|
const modules = Array.from(selectedModules.values()).map(selection => ({
|
||||||
|
moduleCode: selection.module.moduleCode,
|
||||||
|
moduleName: selection.module.moduleName,
|
||||||
|
description: selection.module.description,
|
||||||
|
unitPrice: selection.price,
|
||||||
|
quantity: 1,
|
||||||
|
complexity: selection.complexity
|
||||||
|
}));
|
||||||
|
|
||||||
|
const customization = {
|
||||||
|
modules: modules,
|
||||||
|
formationHours: parseFloat(document.getElementById('formationHours').value) || 0,
|
||||||
|
supportMonths: parseFloat(document.getElementById('supportMonths').value) || 0
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
let response;
|
||||||
|
|
||||||
|
if (auditId) {
|
||||||
|
// Génération depuis un audit
|
||||||
|
response = await fetch(`/api/quotes/generate/${auditId}`, {
|
||||||
|
method: 'POST'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Redirection vers formulaire client
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
config: JSON.stringify(customization)
|
||||||
|
});
|
||||||
|
window.location.href = `/quote-form.html?${params}`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const quote = await response.json();
|
||||||
|
|
||||||
|
// Personnalisation du devis
|
||||||
|
const customizeResponse = await fetch(`/api/quotes/${quote.id}/customize`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(customization)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (customizeResponse.ok) {
|
||||||
|
alert('Devis généré avec succès !');
|
||||||
|
window.location.href = `/quote-view.html?id=${quote.id}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert('Erreur lors de la génération du devis');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur:', error);
|
||||||
|
alert('Erreur lors de la génération du devis');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Formatage des montants
|
||||||
|
function formatCurrency(amount) {
|
||||||
|
return new Intl.NumberFormat('fr-FR').format(amount) + ' FCFA';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
558
src/main/resources/META-INF/resources/roi-calculator.html
Normal file
558
src/main/resources/META-INF/resources/roi-calculator.html
Normal file
@@ -0,0 +1,558 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Calculateur ROI - Lions Dev</title>
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
<style>
|
||||||
|
.roi-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.roi-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
padding: 30px;
|
||||||
|
background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%);
|
||||||
|
color: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.roi-form {
|
||||||
|
background: white;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0,0,0,0.1);
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input,
|
||||||
|
.form-group select {
|
||||||
|
padding: 12px;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus,
|
||||||
|
.form-group select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #4CAF50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modules-selection {
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modules-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 15px;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-checkbox {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 15px;
|
||||||
|
background: #f9f9f9;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-checkbox:hover {
|
||||||
|
border-color: #4CAF50;
|
||||||
|
background: #f0fff0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-checkbox.selected {
|
||||||
|
border-color: #4CAF50;
|
||||||
|
background: #e8f5e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-checkbox input {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calculate-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 15px;
|
||||||
|
background: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calculate-btn:hover {
|
||||||
|
background: #45a049;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-container {
|
||||||
|
display: none;
|
||||||
|
background: white;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.roi-metrics {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-card {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
border-left: 4px solid #4CAF50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-value {
|
||||||
|
font-size: 2em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #4CAF50;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-label {
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gains-breakdown {
|
||||||
|
margin: 30px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gains-chart {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gain-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: #f9f9f9;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gain-label {
|
||||||
|
min-width: 150px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gain-progress {
|
||||||
|
flex: 1;
|
||||||
|
height: 20px;
|
||||||
|
background: #e0e0e0;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin: 0 15px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gain-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #4CAF50, #45a049);
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: width 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gain-amount {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #4CAF50;
|
||||||
|
min-width: 120px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scenarios-section {
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scenarios-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 20px;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scenario-card {
|
||||||
|
padding: 20px;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scenario-card.best {
|
||||||
|
border-color: #4CAF50;
|
||||||
|
background: #f0fff0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recommendations {
|
||||||
|
background: #e3f2fd;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recommendations h3 {
|
||||||
|
color: #1976d2;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recommendations pre {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
font-family: inherit;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.form-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.roi-metrics {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scenarios-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="roi-container">
|
||||||
|
<!-- En-tête -->
|
||||||
|
<div class="roi-header">
|
||||||
|
<h1>📊 Calculateur de Retour sur Investissement</h1>
|
||||||
|
<p>Découvrez les bénéfices financiers de votre digitalisation</p>
|
||||||
|
<p><strong>Calcul personnalisé • Scénarios multiples • Recommandations d'experts</strong></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Formulaire -->
|
||||||
|
<div class="roi-form">
|
||||||
|
<h2>🏢 Informations sur votre entreprise</h2>
|
||||||
|
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="sector">Secteur d'activité</label>
|
||||||
|
<select id="sector" onchange="loadDefaults()">
|
||||||
|
<option value="">Sélectionnez...</option>
|
||||||
|
<option value="commerce">Commerce/Distribution</option>
|
||||||
|
<option value="services">Services</option>
|
||||||
|
<option value="industrie">Industrie/Manufacturing</option>
|
||||||
|
<option value="agriculture">Agriculture/Agro-alimentaire</option>
|
||||||
|
<option value="btp">BTP/Construction</option>
|
||||||
|
<option value="transport">Transport/Logistique</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="employeeCount">Nombre d'employés</label>
|
||||||
|
<input type="number" id="employeeCount" min="1" max="1000" value="10">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="averageSalary">Salaire moyen annuel (FCFA)</label>
|
||||||
|
<input type="number" id="averageSalary" min="1000000" max="10000000" value="2400000" step="100000">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="turnover">Chiffre d'affaires annuel (FCFA)</label>
|
||||||
|
<input type="number" id="turnover" min="1000000" max="1000000000" value="50000000" step="1000000">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="investmentAmount">Montant d'investissement (FCFA)</label>
|
||||||
|
<input type="number" id="investmentAmount" min="100000" max="10000000" value="500000" step="50000">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modules-selection">
|
||||||
|
<h3>📦 Modules à implémenter</h3>
|
||||||
|
<div class="modules-grid">
|
||||||
|
<div class="module-checkbox" onclick="toggleModule('CRM')">
|
||||||
|
<input type="checkbox" id="module-CRM" value="CRM">
|
||||||
|
<label for="module-CRM">💼 CRM Commercial</label>
|
||||||
|
</div>
|
||||||
|
<div class="module-checkbox" onclick="toggleModule('STOCK')">
|
||||||
|
<input type="checkbox" id="module-STOCK" value="STOCK">
|
||||||
|
<label for="module-STOCK">📦 Gestion Stock</label>
|
||||||
|
</div>
|
||||||
|
<div class="module-checkbox" onclick="toggleModule('COMPTA')">
|
||||||
|
<input type="checkbox" id="module-COMPTA" value="COMPTA">
|
||||||
|
<label for="module-COMPTA">💰 Comptabilité</label>
|
||||||
|
</div>
|
||||||
|
<div class="module-checkbox" onclick="toggleModule('RH')">
|
||||||
|
<input type="checkbox" id="module-RH" value="RH">
|
||||||
|
<label for="module-RH">👥 Ressources Humaines</label>
|
||||||
|
</div>
|
||||||
|
<div class="module-checkbox" onclick="toggleModule('INFRA')">
|
||||||
|
<input type="checkbox" id="module-INFRA" value="INFRA">
|
||||||
|
<label for="module-INFRA">🖥️ Infrastructure IT</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="calculate-btn" onclick="calculateROI()">
|
||||||
|
🚀 Calculer le ROI
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Résultats -->
|
||||||
|
<div class="results-container" id="resultsContainer">
|
||||||
|
<h2>📈 Résultats du Calcul ROI</h2>
|
||||||
|
|
||||||
|
<!-- Métriques principales -->
|
||||||
|
<div class="roi-metrics">
|
||||||
|
<div class="metric-card">
|
||||||
|
<div class="metric-value" id="roiPercentage">0%</div>
|
||||||
|
<div class="metric-label">ROI sur 3 ans</div>
|
||||||
|
</div>
|
||||||
|
<div class="metric-card">
|
||||||
|
<div class="metric-value" id="paybackPeriod">0</div>
|
||||||
|
<div class="metric-label">Retour investissement (mois)</div>
|
||||||
|
</div>
|
||||||
|
<div class="metric-card">
|
||||||
|
<div class="metric-value" id="annualGains">0</div>
|
||||||
|
<div class="metric-label">Gains annuels (FCFA)</div>
|
||||||
|
</div>
|
||||||
|
<div class="metric-card">
|
||||||
|
<div class="metric-value" id="netBenefit">0</div>
|
||||||
|
<div class="metric-label">Bénéfice net 3 ans (FCFA)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Répartition des gains -->
|
||||||
|
<div class="gains-breakdown">
|
||||||
|
<h3>💡 Répartition des Gains Annuels</h3>
|
||||||
|
<div class="gains-chart" id="gainsChart">
|
||||||
|
<!-- Généré dynamiquement -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Scénarios -->
|
||||||
|
<div class="scenarios-section">
|
||||||
|
<h3>🎯 Analyse par Scénarios</h3>
|
||||||
|
<div class="scenarios-grid" id="scenariosGrid">
|
||||||
|
<!-- Généré dynamiquement -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Recommandations -->
|
||||||
|
<div class="recommendations" id="recommendationsSection">
|
||||||
|
<h3>🎯 Recommandations Personnalisées</h3>
|
||||||
|
<pre id="recommendationsText"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Variables globales
|
||||||
|
let selectedModules = new Set();
|
||||||
|
|
||||||
|
// Chargement des valeurs par défaut selon le secteur
|
||||||
|
async function loadDefaults() {
|
||||||
|
const sector = document.getElementById('sector').value;
|
||||||
|
if (!sector) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/roi/defaults/${sector}`);
|
||||||
|
const defaults = await response.json();
|
||||||
|
|
||||||
|
document.getElementById('averageSalary').value = defaults.averageSalary;
|
||||||
|
|
||||||
|
// Sélection des modules recommandés
|
||||||
|
selectedModules.clear();
|
||||||
|
document.querySelectorAll('.module-checkbox').forEach(cb => cb.classList.remove('selected'));
|
||||||
|
document.querySelectorAll('input[type="checkbox"]').forEach(cb => cb.checked = false);
|
||||||
|
|
||||||
|
defaults.selectedModules.forEach(moduleCode => {
|
||||||
|
toggleModule(moduleCode, true);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur chargement defaults:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sélection/désélection des modules
|
||||||
|
function toggleModule(moduleCode, force = false) {
|
||||||
|
const checkbox = document.getElementById(`module-${moduleCode}`);
|
||||||
|
const container = checkbox.closest('.module-checkbox');
|
||||||
|
|
||||||
|
if (force || !selectedModules.has(moduleCode)) {
|
||||||
|
selectedModules.add(moduleCode);
|
||||||
|
container.classList.add('selected');
|
||||||
|
checkbox.checked = true;
|
||||||
|
} else {
|
||||||
|
selectedModules.delete(moduleCode);
|
||||||
|
container.classList.remove('selected');
|
||||||
|
checkbox.checked = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calcul du ROI
|
||||||
|
async function calculateROI() {
|
||||||
|
if (selectedModules.size === 0) {
|
||||||
|
alert('Veuillez sélectionner au moins un module');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const input = {
|
||||||
|
employeeCount: parseInt(document.getElementById('employeeCount').value),
|
||||||
|
averageSalary: parseFloat(document.getElementById('averageSalary').value),
|
||||||
|
turnover: parseFloat(document.getElementById('turnover').value),
|
||||||
|
investmentAmount: parseFloat(document.getElementById('investmentAmount').value),
|
||||||
|
selectedModules: Array.from(selectedModules),
|
||||||
|
sector: document.getElementById('sector').value
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Calcul du ROI principal
|
||||||
|
const response = await fetch('/api/roi/calculate', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(input)
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Calcul des scénarios
|
||||||
|
const scenariosResponse = await fetch('/api/roi/scenarios', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(input)
|
||||||
|
});
|
||||||
|
|
||||||
|
const scenarios = await scenariosResponse.json();
|
||||||
|
|
||||||
|
// Affichage des résultats
|
||||||
|
displayResults(data.result, data.recommendations, scenarios);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur calcul ROI:', error);
|
||||||
|
alert('Erreur lors du calcul du ROI');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Affichage des résultats
|
||||||
|
function displayResults(result, recommendations, scenarios) {
|
||||||
|
// Affichage du conteneur
|
||||||
|
document.getElementById('resultsContainer').style.display = 'block';
|
||||||
|
|
||||||
|
// Métriques principales
|
||||||
|
document.getElementById('roiPercentage').textContent = Math.round(result.roi3Years) + '%';
|
||||||
|
document.getElementById('paybackPeriod').textContent = Math.round(result.paybackPeriodMonths);
|
||||||
|
document.getElementById('annualGains').textContent = formatCurrency(result.totalAnnualGains);
|
||||||
|
document.getElementById('netBenefit').textContent = formatCurrency(result.netPresentValue3Years);
|
||||||
|
|
||||||
|
// Répartition des gains
|
||||||
|
displayGainsBreakdown(result.gainsByCategory);
|
||||||
|
|
||||||
|
// Scénarios
|
||||||
|
displayScenarios(scenarios);
|
||||||
|
|
||||||
|
// Recommandations
|
||||||
|
document.getElementById('recommendationsText').textContent = recommendations;
|
||||||
|
|
||||||
|
// Scroll vers les résultats
|
||||||
|
document.getElementById('resultsContainer').scrollIntoView({ behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Affichage de la répartition des gains
|
||||||
|
function displayGainsBreakdown(gainsByCategory) {
|
||||||
|
const chart = document.getElementById('gainsChart');
|
||||||
|
chart.innerHTML = '';
|
||||||
|
|
||||||
|
const maxGain = Math.max(...Object.values(gainsByCategory));
|
||||||
|
|
||||||
|
Object.entries(gainsByCategory).forEach(([category, amount]) => {
|
||||||
|
const percentage = (amount / maxGain) * 100;
|
||||||
|
|
||||||
|
const bar = document.createElement('div');
|
||||||
|
bar.className = 'gain-bar';
|
||||||
|
bar.innerHTML = `
|
||||||
|
<div class="gain-label">${category}</div>
|
||||||
|
<div class="gain-progress">
|
||||||
|
<div class="gain-fill" style="width: ${percentage}%"></div>
|
||||||
|
</div>
|
||||||
|
<div class="gain-amount">${formatCurrency(amount)}</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
chart.appendChild(bar);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Affichage des scénarios
|
||||||
|
function displayScenarios(scenarios) {
|
||||||
|
const grid = document.getElementById('scenariosGrid');
|
||||||
|
grid.innerHTML = '';
|
||||||
|
|
||||||
|
Object.entries(scenarios).forEach(([name, result]) => {
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = 'scenario-card';
|
||||||
|
if (name === 'Réaliste') card.classList.add('best');
|
||||||
|
|
||||||
|
card.innerHTML = `
|
||||||
|
<h4>${name}</h4>
|
||||||
|
<div style="font-size: 1.5em; font-weight: bold; color: #4CAF50; margin: 10px 0;">
|
||||||
|
${Math.round(result.roi3Years)}%
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 0.9em; color: #666;">
|
||||||
|
ROI sur 3 ans
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 10px; font-size: 0.8em;">
|
||||||
|
Retour: ${Math.round(result.paybackPeriodMonths)} mois<br>
|
||||||
|
Gains: ${formatCurrency(result.totalAnnualGains)}/an
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
grid.appendChild(card);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Formatage des montants
|
||||||
|
function formatCurrency(amount) {
|
||||||
|
return new Intl.NumberFormat('fr-FR').format(Math.round(amount)) + ' FCFA';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -24,7 +24,7 @@ app.default-language=fr
|
|||||||
# Configuration proxy et CORS
|
# 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=true
|
quarkus.http.cors.enabled=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<EFBFBD>s JSF
|
# Chemins d'accè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.enable=true
|
quarkus.swagger-ui.enabled=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}
|
||||||
|
|||||||
99
src/test/java/dev/lions/audit/AuditServiceTest.java
Normal file
99
src/test/java/dev/lions/audit/AuditServiceTest.java
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
package dev.lions.audit;
|
||||||
|
|
||||||
|
import io.quarkus.test.junit.QuarkusTest;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.HashMap;
|
||||||
|
|
||||||
|
@QuarkusTest
|
||||||
|
public class AuditServiceTest {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
AuditService auditService;
|
||||||
|
|
||||||
|
private AuditResponse sampleResponse;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
sampleResponse = new AuditResponse();
|
||||||
|
sampleResponse.setCompanyName("Test Company");
|
||||||
|
sampleResponse.setContactName("John Doe");
|
||||||
|
sampleResponse.setEmail("test@company.com");
|
||||||
|
sampleResponse.setPhone("0123456789");
|
||||||
|
sampleResponse.setEmployeeCount(25);
|
||||||
|
sampleResponse.setTurnover("500000000");
|
||||||
|
sampleResponse.setSector("Commerce");
|
||||||
|
|
||||||
|
// Réponses aux questions (Map questionId -> answer index)
|
||||||
|
Map<Long, Integer> answers = new HashMap<>();
|
||||||
|
for (long i = 1; i <= 16; i++) {
|
||||||
|
answers.put(i, (int) (2 + (i % 4))); // Scores entre 2 et 5
|
||||||
|
}
|
||||||
|
sampleResponse.setAnswers(answers);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testProcessAuditResponse() {
|
||||||
|
AuditResponse result = auditService.processAuditResponse(sampleResponse);
|
||||||
|
|
||||||
|
assertNotNull(result);
|
||||||
|
assertEquals("Test Company", result.getCompanyName());
|
||||||
|
assertEquals("test@company.com", result.getEmail());
|
||||||
|
assertNotNull(result.getTotalScore());
|
||||||
|
assertTrue(result.getTotalScore() > 0);
|
||||||
|
assertNotNull(result.getMaturityPercentage());
|
||||||
|
assertTrue(result.getMaturityPercentage() > 0);
|
||||||
|
assertTrue(result.getMaturityPercentage() <= 100);
|
||||||
|
assertNotNull(result.getCategoryScores());
|
||||||
|
assertFalse(result.getCategoryScores().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testBasicFunctionality() {
|
||||||
|
// Test que le service peut traiter une réponse d'audit basique
|
||||||
|
AuditResponse result = auditService.processAuditResponse(sampleResponse);
|
||||||
|
|
||||||
|
assertNotNull(result);
|
||||||
|
assertEquals("Test Company", result.getCompanyName());
|
||||||
|
assertEquals("test@company.com", result.getEmail());
|
||||||
|
|
||||||
|
// Vérifier que les scores ont été calculés
|
||||||
|
assertNotNull(result.getTotalScore());
|
||||||
|
assertNotNull(result.getMaturityPercentage());
|
||||||
|
assertNotNull(result.getCategoryScores());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testScoreCalculation() {
|
||||||
|
AuditResponse result = auditService.processAuditResponse(sampleResponse);
|
||||||
|
|
||||||
|
// Vérifier que les scores sont dans des plages valides
|
||||||
|
assertTrue(result.getTotalScore() > 0);
|
||||||
|
assertTrue(result.getMaturityPercentage() >= 0);
|
||||||
|
assertTrue(result.getMaturityPercentage() <= 100);
|
||||||
|
|
||||||
|
// Vérifier que les scores de catégorie existent
|
||||||
|
assertNotNull(result.getCategoryScores());
|
||||||
|
assertFalse(result.getCategoryScores().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testInputValidation() {
|
||||||
|
// Test avec nom d'entreprise null
|
||||||
|
sampleResponse.setCompanyName(null);
|
||||||
|
assertThrows(Exception.class, () -> {
|
||||||
|
auditService.processAuditResponse(sampleResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test avec email null
|
||||||
|
sampleResponse.setCompanyName("Test Company");
|
||||||
|
sampleResponse.setEmail(null);
|
||||||
|
assertThrows(Exception.class, () -> {
|
||||||
|
auditService.processAuditResponse(sampleResponse);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
124
src/test/java/dev/lions/compliance/IvorianTaxServiceTest.java
Normal file
124
src/test/java/dev/lions/compliance/IvorianTaxServiceTest.java
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
package dev.lions.compliance;
|
||||||
|
|
||||||
|
import io.quarkus.test.junit.QuarkusTest;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@QuarkusTest
|
||||||
|
public class IvorianTaxServiceTest {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
IvorianTaxService taxService;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCalculateTVAStandard() {
|
||||||
|
double amountHT = 1000000.0; // 1M FCFA
|
||||||
|
TaxCalculation result = taxService.calculateTVA(amountHT, false);
|
||||||
|
|
||||||
|
assertNotNull(result);
|
||||||
|
assertEquals(amountHT, result.getAmountHT());
|
||||||
|
assertEquals(18.0, result.getTaxRate()); // Taux standard 18%
|
||||||
|
assertEquals(180000.0, result.getTaxAmount(), 0.01); // 18% de 1M
|
||||||
|
assertEquals(1180000.0, result.getAmountTTC(), 0.01); // HT + TVA
|
||||||
|
assertEquals("TVA", result.getTaxType());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCalculateTVAReduced() {
|
||||||
|
double amountHT = 1000000.0; // 1M FCFA
|
||||||
|
TaxCalculation result = taxService.calculateTVA(amountHT, true);
|
||||||
|
|
||||||
|
assertNotNull(result);
|
||||||
|
assertEquals(amountHT, result.getAmountHT());
|
||||||
|
assertEquals(9.0, result.getTaxRate()); // Taux réduit 9%
|
||||||
|
assertEquals(90000.0, result.getTaxAmount(), 0.01); // 9% de 1M
|
||||||
|
assertEquals(1090000.0, result.getAmountTTC(), 0.01); // HT + TVA
|
||||||
|
assertEquals("TVA", result.getTaxType());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCalculateISStandard() {
|
||||||
|
double profit = 10000000.0; // 10M FCFA
|
||||||
|
TaxCalculation result = taxService.calculateIS(profit, false);
|
||||||
|
|
||||||
|
assertNotNull(result);
|
||||||
|
assertEquals(25.0, result.getTaxRate()); // Taux standard 25%
|
||||||
|
assertEquals(2500000.0, result.getTaxAmount(), 0.01); // 25% de 10M
|
||||||
|
assertEquals("IS", result.getTaxType());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCalculateISPME() {
|
||||||
|
double profit = 10000000.0; // 10M FCFA
|
||||||
|
TaxCalculation result = taxService.calculateIS(profit, true);
|
||||||
|
|
||||||
|
assertNotNull(result);
|
||||||
|
assertEquals(20.0, result.getTaxRate()); // Taux PME 20%
|
||||||
|
assertEquals(2000000.0, result.getTaxAmount(), 0.01); // 20% de 10M
|
||||||
|
assertEquals("IS", result.getTaxType());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testTaxRateConstants() {
|
||||||
|
// Vérifier les constantes de taux
|
||||||
|
assertEquals(18.0, IvorianTaxService.TVA_RATE);
|
||||||
|
assertEquals(9.0, IvorianTaxService.TVA_REDUCED_RATE);
|
||||||
|
assertEquals(25.0, IvorianTaxService.IS_RATE);
|
||||||
|
assertEquals(20.0, IvorianTaxService.IS_REDUCED_RATE);
|
||||||
|
assertEquals(200_000_000.0, IvorianTaxService.PME_TURNOVER_THRESHOLD);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testZeroAmounts() {
|
||||||
|
// Test avec montant zéro
|
||||||
|
TaxCalculation tvaResult = taxService.calculateTVA(0.0, false);
|
||||||
|
assertEquals(0.0, tvaResult.getTaxAmount());
|
||||||
|
assertEquals(0.0, tvaResult.getAmountTTC());
|
||||||
|
|
||||||
|
TaxCalculation isResult = taxService.calculateIS(0.0, false);
|
||||||
|
assertEquals(0.0, isResult.getTaxAmount());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testLargeAmounts() {
|
||||||
|
// Test avec gros montants
|
||||||
|
double largeAmount = 1_000_000_000.0; // 1 milliard FCFA
|
||||||
|
|
||||||
|
TaxCalculation tvaResult = taxService.calculateTVA(largeAmount, false);
|
||||||
|
assertEquals(180_000_000.0, tvaResult.getTaxAmount(), 0.01); // 18% de 1B
|
||||||
|
|
||||||
|
TaxCalculation isResult = taxService.calculateIS(largeAmount, false);
|
||||||
|
assertEquals(250_000_000.0, isResult.getTaxAmount(), 0.01); // 25% de 1B
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testNegativeAmounts() {
|
||||||
|
// Les montants négatifs devraient être traités correctement
|
||||||
|
TaxCalculation tvaResult = taxService.calculateTVA(-1000000.0, false);
|
||||||
|
assertTrue(tvaResult.getTaxAmount() <= 0); // TVA négative ou nulle
|
||||||
|
|
||||||
|
TaxCalculation isResult = taxService.calculateIS(-1000000.0, false);
|
||||||
|
assertTrue(isResult.getTaxAmount() <= 0); // IS négative ou nulle
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testPMEThreshold() {
|
||||||
|
// Vérifier le seuil PME
|
||||||
|
double threshold = IvorianTaxService.PME_TURNOVER_THRESHOLD;
|
||||||
|
assertEquals(200_000_000.0, threshold); // 200M FCFA
|
||||||
|
|
||||||
|
// Test avec montant juste en dessous du seuil PME
|
||||||
|
double belowThreshold = threshold - 1000000.0;
|
||||||
|
TaxCalculation pmeResult = taxService.calculateIS(belowThreshold * 0.1, true); // 10% de marge
|
||||||
|
assertEquals(20.0, pmeResult.getTaxRate()); // Taux PME
|
||||||
|
|
||||||
|
// Test avec montant au-dessus du seuil
|
||||||
|
double aboveThreshold = threshold + 1000000.0;
|
||||||
|
TaxCalculation standardResult = taxService.calculateIS(aboveThreshold * 0.1, false); // 10% de marge
|
||||||
|
assertEquals(25.0, standardResult.getTaxRate()); // Taux standard
|
||||||
|
}
|
||||||
|
}
|
||||||
169
src/test/java/dev/lions/integration/AuditIntegrationTest.java
Normal file
169
src/test/java/dev/lions/integration/AuditIntegrationTest.java
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
package dev.lions.integration;
|
||||||
|
|
||||||
|
import io.quarkus.test.junit.QuarkusTest;
|
||||||
|
import io.restassured.http.ContentType;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static io.restassured.RestAssured.given;
|
||||||
|
import static org.hamcrest.CoreMatchers.*;
|
||||||
|
import static org.hamcrest.Matchers.greaterThan;
|
||||||
|
|
||||||
|
@QuarkusTest
|
||||||
|
public class AuditIntegrationTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetAuditQuestions() {
|
||||||
|
// Endpoint retourne Map<String, List<AuditQuestion>> (catégorie → questions),
|
||||||
|
// pas une liste plate. Donc on vérifie qu'il y a au moins une catégorie
|
||||||
|
// et que chaque question dans la 1ère catégorie a les champs attendus.
|
||||||
|
given()
|
||||||
|
.when().get("/api/audit/questions")
|
||||||
|
.then()
|
||||||
|
.statusCode(200)
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body("size()", greaterThan(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSubmitAudit() {
|
||||||
|
String auditSubmission = """
|
||||||
|
{
|
||||||
|
"companyName": "Test Company",
|
||||||
|
"email": "test@company.com",
|
||||||
|
"phone": "0123456789",
|
||||||
|
"employeeCount": 25,
|
||||||
|
"annualRevenue": 500000000,
|
||||||
|
"sector": "Commerce",
|
||||||
|
"responses": [3, 4, 2, 3, 4, 3, 2, 3, 4, 2, 3, 2, 3, 4, 2, 3]
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
given()
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body(auditSubmission)
|
||||||
|
.when().post("/api/audit/submit")
|
||||||
|
.then()
|
||||||
|
.statusCode(200)
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body("companyName", equalTo("Test Company"))
|
||||||
|
.body("email", equalTo("test@company.com"))
|
||||||
|
.body("overallMaturity", notNullValue())
|
||||||
|
.body("categoryScores", notNullValue())
|
||||||
|
.body("recommendations", notNullValue())
|
||||||
|
.body("maturityLevel", notNullValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSubmitAuditInvalidData() {
|
||||||
|
String invalidSubmission = """
|
||||||
|
{
|
||||||
|
"companyName": "",
|
||||||
|
"email": "invalid-email",
|
||||||
|
"responses": []
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
given()
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body(invalidSubmission)
|
||||||
|
.when().post("/api/audit/submit")
|
||||||
|
.then()
|
||||||
|
.statusCode(400);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGenerateAuditReport() {
|
||||||
|
String auditSubmission = """
|
||||||
|
{
|
||||||
|
"companyName": "Test Company",
|
||||||
|
"email": "test@company.com",
|
||||||
|
"phone": "0123456789",
|
||||||
|
"employeeCount": 25,
|
||||||
|
"annualRevenue": 500000000,
|
||||||
|
"sector": "Commerce",
|
||||||
|
"responses": [3, 4, 2, 3, 4, 3, 2, 3, 4, 2, 3, 2, 3, 4, 2, 3]
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
given()
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body(auditSubmission)
|
||||||
|
.when().post("/api/audit/report")
|
||||||
|
.then()
|
||||||
|
.statusCode(200)
|
||||||
|
.contentType("application/pdf");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetQuoteFromAudit() {
|
||||||
|
String auditSubmission = """
|
||||||
|
{
|
||||||
|
"companyName": "Test Company",
|
||||||
|
"email": "test@company.com",
|
||||||
|
"phone": "0123456789",
|
||||||
|
"employeeCount": 25,
|
||||||
|
"annualRevenue": 500000000,
|
||||||
|
"sector": "Commerce",
|
||||||
|
"responses": [3, 4, 2, 3, 4, 3, 2, 3, 4, 2, 3, 2, 3, 4, 2, 3]
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
given()
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body(auditSubmission)
|
||||||
|
.when().post("/api/quotes/from-audit")
|
||||||
|
.then()
|
||||||
|
.statusCode(200)
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body("companyName", equalTo("Test Company"))
|
||||||
|
.body("modules", notNullValue())
|
||||||
|
.body("totalPrice", notNullValue())
|
||||||
|
.body("implementationDays", notNullValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testROICalculation() {
|
||||||
|
String roiRequest = """
|
||||||
|
{
|
||||||
|
"annualRevenue": 500000000,
|
||||||
|
"employeeCount": 25,
|
||||||
|
"currentMaturity": 45.0,
|
||||||
|
"targetMaturity": 85.0,
|
||||||
|
"investmentAmount": 2000000.0,
|
||||||
|
"scenario": "realistic"
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
given()
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body(roiRequest)
|
||||||
|
.when().post("/api/roi/calculate")
|
||||||
|
.then()
|
||||||
|
.statusCode(200)
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body("annualSavings", notNullValue())
|
||||||
|
.body("roiPercentage", notNullValue())
|
||||||
|
.body("paybackMonths", notNullValue())
|
||||||
|
.body("breakdownByCategory", notNullValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testHealthCheck() {
|
||||||
|
given()
|
||||||
|
.when().get("/q/health")
|
||||||
|
.then()
|
||||||
|
.statusCode(200)
|
||||||
|
.body("status", equalTo("UP"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOpenAPISpec() {
|
||||||
|
given()
|
||||||
|
.when().get("/q/openapi")
|
||||||
|
.then()
|
||||||
|
.statusCode(200)
|
||||||
|
.contentType("application/yaml")
|
||||||
|
.body(containsString("openapi"))
|
||||||
|
.body(containsString("info"));
|
||||||
|
}
|
||||||
|
}
|
||||||
170
src/test/java/dev/lions/roi/ROICalculatorTest.java
Normal file
170
src/test/java/dev/lions/roi/ROICalculatorTest.java
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
package dev.lions.roi;
|
||||||
|
|
||||||
|
import io.quarkus.test.junit.QuarkusTest;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@QuarkusTest
|
||||||
|
public class ROICalculatorTest {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
ROICalculator roiCalculator;
|
||||||
|
|
||||||
|
private ROIInput sampleRequest;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
sampleRequest = new ROIInput();
|
||||||
|
sampleRequest.setTurnover(500000000.0); // 500M FCFA
|
||||||
|
sampleRequest.setEmployeeCount(25);
|
||||||
|
sampleRequest.setAverageSalary(3000000.0); // 3M FCFA par an
|
||||||
|
sampleRequest.setInvestmentAmount(2000000.0); // 2M FCFA
|
||||||
|
sampleRequest.setSelectedModules(java.util.Arrays.asList("CRM", "STOCK", "COMPTA"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCalculateROI() {
|
||||||
|
ROIResult result = roiCalculator.calculateROI(sampleRequest);
|
||||||
|
|
||||||
|
assertNotNull(result);
|
||||||
|
assertTrue(result.getTotalAnnualGains() > 0);
|
||||||
|
assertTrue(result.getAnnualProductivityGains() > 0);
|
||||||
|
assertTrue(result.getAnnualErrorReduction() > 0);
|
||||||
|
assertTrue(result.getAnnualTimeSavings() > 0);
|
||||||
|
assertTrue(result.getAnnualComplianceGains() > 0);
|
||||||
|
assertTrue(result.getPaybackPeriodMonths() > 0);
|
||||||
|
assertNotNull(result.getRoi3Years());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testBasicCalculation() {
|
||||||
|
ROIResult result = roiCalculator.calculateROI(sampleRequest);
|
||||||
|
|
||||||
|
assertNotNull(result);
|
||||||
|
assertTrue(result.getTotalAnnualGains() > 0);
|
||||||
|
assertTrue(result.getRoi3Years() != 0); // Peut être négatif ou positif
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDifferentInvestmentAmounts() {
|
||||||
|
// Test avec investissement faible
|
||||||
|
sampleRequest.setInvestmentAmount(1000000.0); // 1M FCFA
|
||||||
|
ROIResult lowInvestment = roiCalculator.calculateROI(sampleRequest);
|
||||||
|
|
||||||
|
// Test avec investissement élevé
|
||||||
|
sampleRequest.setInvestmentAmount(5000000.0); // 5M FCFA
|
||||||
|
ROIResult highInvestment = roiCalculator.calculateROI(sampleRequest);
|
||||||
|
|
||||||
|
assertNotNull(lowInvestment);
|
||||||
|
assertNotNull(highInvestment);
|
||||||
|
// L'investissement plus faible devrait avoir un meilleur ROI
|
||||||
|
assertTrue(lowInvestment.getPaybackPeriodMonths() < highInvestment.getPaybackPeriodMonths());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDifferentModules() {
|
||||||
|
// Test avec un seul module
|
||||||
|
sampleRequest.setSelectedModules(java.util.Arrays.asList("CRM"));
|
||||||
|
ROIResult singleModule = roiCalculator.calculateROI(sampleRequest);
|
||||||
|
|
||||||
|
// Test avec plusieurs modules
|
||||||
|
sampleRequest.setSelectedModules(java.util.Arrays.asList("CRM", "STOCK", "COMPTA", "RH"));
|
||||||
|
ROIResult multipleModules = roiCalculator.calculateROI(sampleRequest);
|
||||||
|
|
||||||
|
// Plus de modules devraient générer plus de gains
|
||||||
|
assertTrue(multipleModules.getTotalAnnualGains() > singleModule.getTotalAnnualGains());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDifferentSectors() {
|
||||||
|
// Test secteur commerce
|
||||||
|
sampleRequest.setSector("Commerce");
|
||||||
|
ROIResult commerce = roiCalculator.calculateROI(sampleRequest);
|
||||||
|
|
||||||
|
// Test secteur services
|
||||||
|
sampleRequest.setSector("Services");
|
||||||
|
ROIResult services = roiCalculator.calculateROI(sampleRequest);
|
||||||
|
|
||||||
|
assertNotNull(commerce);
|
||||||
|
assertNotNull(services);
|
||||||
|
assertTrue(commerce.getTotalAnnualGains() > 0);
|
||||||
|
assertTrue(services.getTotalAnnualGains() > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testEmployeeCountImpact() {
|
||||||
|
// Test avec peu d'employés
|
||||||
|
sampleRequest.setEmployeeCount(5);
|
||||||
|
ROIResult smallTeam = roiCalculator.calculateROI(sampleRequest);
|
||||||
|
|
||||||
|
// Test avec beaucoup d'employés
|
||||||
|
sampleRequest.setEmployeeCount(50);
|
||||||
|
ROIResult largeTeam = roiCalculator.calculateROI(sampleRequest);
|
||||||
|
|
||||||
|
// Les gains devraient être plus importants avec plus d'employés
|
||||||
|
assertTrue(largeTeam.getTotalAnnualGains() > smallTeam.getTotalAnnualGains());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testRevenueImpact() {
|
||||||
|
// Test avec faible chiffre d'affaires
|
||||||
|
sampleRequest.setTurnover(100000000.0); // 100M FCFA
|
||||||
|
ROIResult lowRevenue = roiCalculator.calculateROI(sampleRequest);
|
||||||
|
|
||||||
|
// Test avec chiffre d'affaires élevé
|
||||||
|
sampleRequest.setTurnover(1000000000.0); // 1B FCFA
|
||||||
|
ROIResult highRevenue = roiCalculator.calculateROI(sampleRequest);
|
||||||
|
|
||||||
|
// Les gains devraient être plus importants avec un CA plus élevé
|
||||||
|
assertTrue(highRevenue.getTotalAnnualGains() > lowRevenue.getTotalAnnualGains());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testResultComponents() {
|
||||||
|
ROIResult result = roiCalculator.calculateROI(sampleRequest);
|
||||||
|
|
||||||
|
// Vérifier que tous les composants sont présents et positifs
|
||||||
|
assertTrue(result.getAnnualProductivityGains() >= 0);
|
||||||
|
assertTrue(result.getAnnualErrorReduction() >= 0);
|
||||||
|
assertTrue(result.getAnnualTimeSavings() >= 0);
|
||||||
|
assertTrue(result.getAnnualComplianceGains() >= 0);
|
||||||
|
|
||||||
|
// Le total devrait être la somme des composants
|
||||||
|
double expectedTotal = result.getAnnualProductivityGains() +
|
||||||
|
result.getAnnualErrorReduction() +
|
||||||
|
result.getAnnualTimeSavings() +
|
||||||
|
result.getAnnualComplianceGains();
|
||||||
|
assertEquals(expectedTotal, result.getTotalAnnualGains(), 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testPaybackPeriod() {
|
||||||
|
ROIResult result = roiCalculator.calculateROI(sampleRequest);
|
||||||
|
|
||||||
|
assertTrue(result.getPaybackPeriodMonths() > 0);
|
||||||
|
assertTrue(result.getPaybackPeriodMonths() <= 60); // Maximum 5 ans de retour sur investissement
|
||||||
|
|
||||||
|
// Vérifier la cohérence : payback = investissement / (gains annuels / 12)
|
||||||
|
double expectedPayback = sampleRequest.getInvestmentAmount() / (result.getTotalAnnualGains() / 12);
|
||||||
|
assertEquals(expectedPayback, result.getPaybackPeriodMonths(), 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testInvalidInputHandling() {
|
||||||
|
// Test avec chiffre d'affaires négatif
|
||||||
|
sampleRequest.setTurnover(-1000000.0);
|
||||||
|
// Note: Le calculateur pourrait ne pas valider les entrées négatives
|
||||||
|
ROIResult result = roiCalculator.calculateROI(sampleRequest);
|
||||||
|
assertNotNull(result); // Juste vérifier qu'il ne plante pas
|
||||||
|
|
||||||
|
// Test avec nombre d'employés négatif
|
||||||
|
sampleRequest.setTurnover(500000000.0);
|
||||||
|
sampleRequest.setEmployeeCount(-5);
|
||||||
|
result = roiCalculator.calculateROI(sampleRequest);
|
||||||
|
assertNotNull(result); // Juste vérifier qu'il ne plante pas
|
||||||
|
}
|
||||||
|
}
|
||||||
14
src/test/resources/application.properties
Normal file
14
src/test/resources/application.properties
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# Configuration pour les tests
|
||||||
|
app.base-url=http://localhost:8080
|
||||||
|
app.storage.base-path=/tmp/lionsdev-test
|
||||||
|
|
||||||
|
# Configuration de la base de données pour les tests
|
||||||
|
quarkus.datasource.db-kind=postgresql
|
||||||
|
quarkus.hibernate-orm.schema-management.strategy=drop-and-create
|
||||||
|
|
||||||
|
# Configuration des logs pour les tests
|
||||||
|
quarkus.log.level=INFO
|
||||||
|
quarkus.log.category."dev.lions".level=DEBUG
|
||||||
|
|
||||||
|
# Configuration des tests
|
||||||
|
quarkus.test.profile=test
|
||||||
Reference in New Issue
Block a user