Créer un simulateur
Dans ce tutoriel, nous allons créer un simulateur de TJM (Tarif Journalier Moyen) pour freelances en utilisant Publicodes et React.
Vous découvrirez comment Publicodes simplifie la création d’outils de simulation en gardant une logique métier claire et maintenable.
Ce que nous allons construire
Notre simulateur permettra à l’utilisateur de :
- Saisir son TJM (Tarif Journalier Moyen)
- Indiquer le nombre de jours facturés par mois
- Spécifier ses charges fixes annuelles
- Voir automatiquement le calcul de son chiffre d’affaires et sa rémunération nette
Prérequis
Pour suivre ce tutoriel, vous aurez besoin de :
- Node.js (version 18 ou supérieure)
- npm, yarn ou pnpm
- Des connaissances de base en JavaScript et React
Étape 1 : Initialiser le projet
Commençons par créer un nouveau projet React.
# Créer un nouveau projet React avec Vite
npm create vite@latest simulateur-tjm -- --template react
# Naviguer dans le dossier du projet
cd simulateur-tjm
# Installer les dépendances
npm install
Ensuite, installons les dépendances nécessaires pour notre simulateur :
npm install publicodes @publicodes/forms yaml
Étape 2 : Définir les règles Publicodes
Créons maintenant un fichier pour définir nos règles de calcul avec Publicodes. C’est ici que réside toute la puissance de Publicodes : nous allons définir notre logique métier de manière déclarative, ce qui la rendra facile à comprendre, à maintenir et à faire évoluer.
Créez un fichier src/rules.js
avec le contenu suivant :
// src/rules.js
import { parse } from 'yaml';
// Définition des règles en YAML dans un template string
const rulesText = `
nb jours:
titre: Nombre de jours facturés par mois
question: Combien de jours facturez-vous par mois en moyenne ?
unité: jour facturé/mois
TJM:
titre: Tarif journalier moyen
question: Quel est votre tarif journalier ?
description: Tarif journalier hors taxe facturé à vos clients
unité: €/jour facturé
CA:
titre: Chiffre d'affaires mensuel
valeur: TJM * nb jours
unité: €/mois
charges fixes:
question: Quelles sont vos charges fixes ?
description: Incluez tous vos frais fixes - comptabilité, assurances, locaux, etc.
par défaut: 10 % * CA
unité: €/mois
rémunération brute: CA - charges fixes
rémunération nette: rémunération brute - cotisations sociales
cotisations sociales: rémunération brute * 30%
`;
// Parsing des règles
const rules = parse(rulesText);
export default rules;
A retenir :
- Définition des règles métier avec des titres, questions, unité et valeurs par défaut
- Modèle complètement découplé de l’interface utilisateur
Étape 3 : Créer un formulaire simple avec FormBuilder
Maintenant, utilisons la bibliothèque @publicodes/forms
pour créer notre formulaire interactif. Cette bibliothèque nous permet de générer automatiquement un formulaire basé sur nos règles Publicodes, en gérant pour nous la logique d’affichage, et les dépendances entre les champs.
Initialiser l’Engine
Commençons par configurer le moteur Publicodes qui effectuera les calculs. Dans le fichier src/App.jsx
:
import Engine, { formatValue } from 'publicodes';
import rules from './rules';
// Initialiser le moteur Publicodes
const engine = new Engine(rules);
// La règle cible pour le calcul
const TARGET = 'rémunération nette';
export default function App() {
return (
<>
<h1>Simulateur de TJM pour freelance</h1>
<section>
<h2>Résultats</h2>
Rémunération nette : {formatValue(engine.evaluate(TARGET))}
</section>
</>
);
}
À noter :
- L’Engine est initialisé avec les règles définies dans
rules.js
(un simple objet JavaScript) ; - Pour calculer la rémunération nette, nous utilisons la méthode
evaluate
; - Nous utilisons la fonction
formatValue
pour afficher les résultats de manière lisible avec les unités ; - L’
Engine
et la règle cible (TARGET
) sont définis en dehors du composant pour éviter de les recréer à chaque rendu.
Pour modifier la situation utilisée pour le calcul, vous pouvez utiliser la méthode setSituation
:
// src/App.jsx
engine.setSituation({
'nb jours': 15,
TJM: 500,
'charges fixes': 500
});
Testez que la page affiche correctement rémunération nette: 4 900 €/mois
en lançant le serveur de développement :
npm run dev
Initialiser l’état du formulaire
Ajoutons maintenant l’état initial du formulaire pour commencer à construire notre interface interactive.
// src/App.jsx
import Engine, { formatValue } from 'publicodes';
import { FormBuilder } from '@publicodes/forms';
import { useState } from 'react';
import rules from './rules';
// Initialiser le moteur Publicodes
const engine = new Engine(rules);
// La règle cible pour le calcul
const TARGET = 'rémunération nette';
// Form Builder pour gérer le formulaire
const formBuilder = new FormBuilder({ engine });
// Initialiser l'état du formulaire
const initialState = formBuilder.start(FormBuilder.newState(), TARGET);
export default function App() {
const [formState, setFormState] = useState(initialState);
return (
<>
<h1>Simulateur de TJM pour freelance</h1>
<h2>Valeurs à renseigner</h2>
<ul>
{formBuilder.currentPage(formState).map((element) => (
<li key={element.id}>{element.label}</li>
))}
</ul>
<section>
<h2>Résultats</h2>
Rémunération nette : {formatValue(engine.evaluate(TARGET))}
</section>
</>
);
}
A noter
- FormBuilder analyse automatiquement les règles pour déterminer quelles questions poser
- L’état du formulaire est géré via React useState
formBuilder.currentPage(formState)
renvoie les éléments à afficher sur la page actuelle (pour l’instant, il n’y a qu’une seule page)
Créer un composant pour l’affichage des champs de saisie
Créons maintenant un composant réutilisable pour afficher un champs de saisie de notre formulaire. Il devra afficher le label, l’unité et permettre à l’utilisateur de saisir une valeur.
// src/Input.jsx
export default function Input({ element, onChange }) {
return (
<div>
<label htmlFor={element.id}>{element.label}</label>
<br />
<input
id={element.id}
type="number"
value={element.value ?? ''}
onChange={(e) => onChange(element.id, e.target.value)}
/>
{element.unit && <span>{element.unit}</span>}
</div>
);
}
Intégrez ce composant dans le composant principal App
:
// src/App.jsx
export default function App() {
const [formState, setFormState] = useState(initialState);
const handleChange = (id, value) => {
const newState = formBuilder.handleInputChange(formState, id, value);
setFormState(newState);
};
return (
<>
<h1>Simulateur de TJM pour freelance</h1>
<form>
{formBuilder.currentPage(formState).map((element) => (
<Input
key={element.id}
element={element}
onChange={handleChange}
/>
))}
</form>
<section>
<h2>Résultats</h2>
Rémunération nette : {formatValue(engine.evaluate(TARGET))}
</section>
</>
);
}
A noter
- Le changement des valeurs est géré avec
formBuilder.handleInputChange
, qui met à jour la situation de l’Engine et l’état du formulaire lorsque l’utilisateur saisit une valeur. - Le composant
Input
affiche le label, l’unité et permet à l’utilisateur de saisir une valeur numérique.
Étape 4 : Gérer les questions conditionnelles
Améliorons maintenant notre simulateur en ajoutant des questions conditionnelles. Cette fonctionnalité est particulièrement utile pour créer des formulaires dynamiques qui s’adaptent aux réponses de l’utilisateur. Dans notre cas, nous allons demander le type de statut du freelance pour calculer les cotisations sociales, avec l’option auto-entrepreneur disponible uniquement si le chiffre d’affaires est inférieur à 70 000 €.
Modifions nos règles pour ajouter cette logique conditionnelle :
# Ajoutez ces règles à votre fichier rules.js
cotisations sociales:
valeur: rémunération brute * taux
unité: €/mois
avec:
taux:
variations:
- si: type de statut = 'auto-entrepreneur'
alors: 24%
- si: type de statut = 'indépendant'
alors: 30%
- si: type de statut = 'sasu'
alors: 50%
type de statut:
question: Quel est votre type de statut ?
une possibilité:
- auto-entrepreneur:
applicable si: CA <= 70000 €/an
- indépendant:
- sasu:
`;
Pour pouvoir utiliser la désactivation de possibilité non applicable, il faut instancier l’engine avec un flag spécifique :
// App.jsx
const engine = new Engine(rules, {
flag: {
filterNotApplicablePossibilities: true
}
});
Ensuite, pour gérer les questions de type possibilités
, nous devons ajouter un composant RadioGroup
:
// src/RadioGroup.jsx
export default function RadioGroup({ element, onChange }) {
return (
<fieldset>
<legend>{element.label}</legend>
{element.options.map((option) => (
<label key={option.value}>
<input
type="radio"
name={element.id}
value={option.value}
checked={element.value === option.value}
onChange={() => onChange(element.id, option.value)}
/>
{option.label}
</label>
))}
</fieldset>
);
}
Intégrez ce composant dans le composant principal App
:
// src/App.jsx
export default function App() {
const [formState, setFormState] = useState(initialState);
const handleChange = (id, value) => {
const newState = formBuilder.handleInputChange(formState, id, value);
setFormState(newState);
};
return (
<>
<h1>Simulateur de TJM pour freelance</h1>
<form>
{formBuilder.currentPage(formState).map((element) => (
<FormElement
key={element.id}
element={element}
onChange={handleChange}
/>
))}
</form>
<section>
<h2>Résultats</h2>
Rémunération nette : {formatValue(engine.evaluate(TARGET))}
</section>
</>
);
}
function FormElement({ element, onChange }) {
if (element.element === 'RadioGroup') {
return <RadioGroup element={element} onChange={onChange} />;
}
return <Input element={element} onChange={onChange} />;
}
Si vous saisissez un TJM et un nombre de jour élévé (par exemple 15 jours à 500€/jour), vous devriez voir disparaître l’option auto-entrepreneur.
Points clés
- Il est possible de spécifier des règles conditionnelles avec la clause
applicable si
- Les règles avec plusieurs possibilités peuvent être saisie avec un élément de formulaire spécial « RadioGroup » pour gérer les choix multiples
- Le formulaire s’adapte automatiquement aux réponses de l’utilisateur, en fonction de la logique des règles publicodes
Naviguer entre les pages du formulaire
Pour les formulaires plus complexes, @publicodes/forms gère automatiquement la pagination. Ajoutons cette fonctionnalité à notre simulateur pour améliorer l’expérience utilisateur, particulièrement utile lorsque le nombre de questions augmente.
Pour l’instant toutes les questions sont sur une seule page. En effet, par défaut, FormBuilder
regroupe les questions en fonction de leur espace de nom.
Nous allons grouper les questions deux par deux pour créer une pagination.
// src/App.jsx
function groupByTwo(array) {
return array.reduce(
(result, item, index) =>
index % 2 === 0 ? [...result, [item, array[index + 1]]] : result,
[]
);
}
const formBuilder = new FormBuilder({ engine, pageBuilder: groupByTwo });
Nous allons ensuite ajouter des boutons “Suivant” et “Précédent” et un indicateur de page courante :
// src/App.jsx
export default function App() {
const [formState, setFormState] = useState(initialState);
const handleChange = (id, value) => {
const newState = formBuilder.handleInputChange(formState, id, value);
setFormState(newState);
};
const { current, pageCount, hasNextPage, hasPreviousPage } =
formBuilder.pagination(formState);
return (
<>
<h1>Simulateur de TJM pour freelance</h1>
<small>
Page {current} sur {pageCount}
</small>
<form>
{formBuilder.currentPage(formState).map((element) => (
<FormElement
key={element.id}
element={element}
onChange={handleChange}
/>
))}
</form>
<div>
{hasPreviousPage && (
<button
onClick={() => {
setFormState(
formBuilder.goToPreviousPage(formState)
);
}}>
Précédent
</button>
)}
{hasNextPage && (
<button
onClick={() =>
setFormState(formBuilder.goToNextPage(formState))
}>
Suivant
</button>
)}
</div>
<section>
<h2>Résultats</h2>
Rémunération nette : {formatValue(engine.evaluate(TARGET))}
</section>
</>
);
}
Points clés
- La pagination est gérée automatiquement par
@publicodes/forms
- Les méthodes
goToNextPage
etgoToPreviousPage
permettent de naviguer entre les pages du formulaire - Pour personnaliser la pagination, vous pouvez passer une fonction
pageBuilder
àFormBuilder
Aller plus loin
Félicitations ! Vous avez créé un simulateur de TJM interactif avec Publicodes et React. Vous pouvez maintenant améliorer le style (qui est un peu brut), ou continuer dans une direction plus avancée :
Personnaliser l’interface utilisateur
Il est possible de personnaliser l’interface utilisateur directement depuis les règles Publicodes. Il est possible de modifier le type de saisie, les labels, ou encore l’ordre des questions.
Pour cela, vous pouvez ajouter des métadonnées avec la clé form
.
TJM:
description: |
Tarif journalier hors taxe facturé aux clients.
Généralement entre 300 et 800 €/jour.
unité: €/jour facturé
form:
position: 1
label: Tarif journalier
description: Indiquez votre tarif journalier hors taxe
Prendre en compte les propriétés additionnelles
Les éléments de formulaires retournés par formBuilder.currentPage
contiennent de nombreuses propriétés utiles pour personnaliser l’interface utilisateur. Par exemple, vous pouvez utiliser element.description
pour afficher des informations supplémentaires.
Par ailleurs, certaines propriétés permettent de fournir une experience utilisateur de meilleure qualité, en cachant ou désactivant des éléments en fonction de la situation.
Pour en savoir plus, consultez la documentation de l’API
Ajouter des pages d’explications
La bibliothèque @publicodes/react-ui
fournit des composants prêts à l’emploi pour afficher des explications de calculs. Pour savoir comment l’utiliser dans le cadre d’une application React, vous pouvez :
- Lire le guide NextJS - Pour expliquer les résultats aux utilisateurs
- Consulter l’exemple sur le repo GitHub
Déplacer le modèle dans un paquet séparé
Pour bénéficier de la meilleure expérience de développement, il est recommandé d’utiliser la CLI Publicodes pour gérer les règles et les modèles. Vous pouvez ensuite les publier sur npm pour les réutiliser dans d’autres projets.