From 3f9b06cd0a47302957aa00e43ac8ee34a6e3a105 Mon Sep 17 00:00:00 2001 From: Mael Date: Tue, 11 Dec 2018 13:50:53 +0100 Subject: [PATCH] Nouveau composant de simulation qui compare des situations --- .eslintrc.yaml | 3 - source/components/ComparativeSimulation.js | 77 +++++++++++++++++++ source/components/ComparativeTargets.js | 49 ++++++++++++ .../simulateur-rémunération-dirigeant.yaml | 7 ++ source/config.js | 2 +- source/engine/rules.js | 5 +- source/règles/base.yaml | 35 +++++---- source/selectors/analyseSelectors.js | 46 ++++++----- source/selectors/regleSelectors.js | 10 ++- source/sites/publi.codes/App.js | 4 +- 10 files changed, 195 insertions(+), 43 deletions(-) create mode 100644 source/components/ComparativeSimulation.js create mode 100644 source/components/ComparativeTargets.js create mode 100644 source/components/simulateur-rémunération-dirigeant.yaml diff --git a/.eslintrc.yaml b/.eslintrc.yaml index c758bafb9..b9f09191e 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -15,9 +15,6 @@ rules: react/no-unescaped-entities: 0 react/display-name: 1 parser: babel-eslint -parserOptions: - ecmaFeatures: - legacyDecorators: true plugins: - react diff --git a/source/components/ComparativeSimulation.js b/source/components/ComparativeSimulation.js new file mode 100644 index 000000000..df55e897e --- /dev/null +++ b/source/components/ComparativeSimulation.js @@ -0,0 +1,77 @@ +import React from 'react' +import { connect } from 'react-redux' +import { isEmpty, compose } from 'ramda' +import Answers from 'Components/AnswerList' +import Conversation from 'Components/conversation/Conversation' +import withColours from 'Components/utils/withColours' +import ComparativeTargets from 'Components/ComparativeTargets' +import './GenericSimulation.css' +import { + nextStepsSelector, + analysisWithDefaultsSelector +} from 'Selectors/analyseSelectors' +import { reduxForm } from 'redux-form' +import PeriodSwitch from 'Components/PeriodSwitch' +import Controls from './Controls' +import situations from './simulateur-rémunération-dirigeant.yaml' + +export default compose( + withColours, + connect(state => ({ + previousAnswers: state.conversationSteps.foldedSteps, + noNextSteps: nextStepsSelector(state).length == 0, + analysis: analysisWithDefaultsSelector(state) + })) +)( + class extends React.Component { + state = { + displayAnswers: false + } + render() { + let { + colours, + noNextSteps, + previousAnswers, + analysis: { controls } + } = this.props + return ( +
+
+ +

Quel revenu au régime des indépendants ?

+ +
+
+ {!isEmpty(previousAnswers) && ( + + )} + + {this.state.displayAnswers && ( + this.setState({ displayAnswers: false })} + /> + )} + + + {noNextSteps && ( + <> +

Plus de questions !

+

Vous avez atteint l'estimation la plus précise.

+ + )} + +
+
+ ) + } + } +) diff --git a/source/components/ComparativeTargets.js b/source/components/ComparativeTargets.js new file mode 100644 index 000000000..51037cc4c --- /dev/null +++ b/source/components/ComparativeTargets.js @@ -0,0 +1,49 @@ +import React from 'react' +import { analysisWithDefaultsSelector } from 'Selectors/analyseSelectors' +import { connect } from 'react-redux' +import './Targets.css' +import withColours from 'Components/utils/withColours' +import { Link } from 'react-router-dom' +import emoji from 'react-easy-emoji' +import { compose } from 'ramda' +import branches from './simulateur-rémunération-dirigeant.yaml' + +export default compose( + connect(state => ({ + analyses: analysisWithDefaultsSelector(state, { branches }) + })), + withColours +)( + class Targets extends React.Component { + render() { + return ( +
+
+
    + {this.props.analyses.map((analysis, i) => { + let { title, nodeValue, dottedName } = analysis.targets[0] + return ( +
  • + {branches[i].nom} + + {nodeValue?.toFixed(1)}{' '} + + + {emoji('📖')} + +
  • + ) + })} +
+
+
+ ) + } + } +) diff --git a/source/components/simulateur-rémunération-dirigeant.yaml b/source/components/simulateur-rémunération-dirigeant.yaml new file mode 100644 index 000000000..d14ee94ce --- /dev/null +++ b/source/components/simulateur-rémunération-dirigeant.yaml @@ -0,0 +1,7 @@ +- nom: Revenu en indépendant + situation: + indépendant: oui +- situation: + micro entreprise: oui + nom: revenu en micro-entreprise + diff --git a/source/config.js b/source/config.js index 63603920c..4b956f3f9 100644 --- a/source/config.js +++ b/source/config.js @@ -1,5 +1,5 @@ // Each one will be a line in the simulation box -export let mainTargetNames = ["micro entreprise . revenu net d'impôt"] +export let mainTargetNames = ["revenu net d'impôt"] // Some others will be displayed too so need to be computed export let simulationTargetNames = [...mainTargetNames] diff --git a/source/engine/rules.js b/source/engine/rules.js index 15b789786..c82e89b4d 100644 --- a/source/engine/rules.js +++ b/source/engine/rules.js @@ -131,7 +131,10 @@ export let disambiguateRuleReference = ( found = reduce( (res, path) => when(is(Object), reduced)( - findRuleByDottedName(allRules, [...path, partialName].join(' . ')) + do { + let dottedNameToCheck = [...path, partialName].join(' . ') + findRuleByDottedName(allRules, dottedNameToCheck) + } ), null, pathPossibilities diff --git a/source/règles/base.yaml b/source/règles/base.yaml index 327614efd..b55e93b84 100644 --- a/source/règles/base.yaml +++ b/source/règles/base.yaml @@ -835,7 +835,7 @@ description: Notion mal définie mais reconnue par les conventions collectives et déterminant l'appartenance à une caise de retraite de base spécifique par défaut: non -- nom: plafond sécurité sociale +- nom: plafond sécurité sociale temps plein description: Le plafond de Sécurité sociale est le montant maximum des rémunérations à prendre en compte pour le calcul de certaines cotisations. période: mois formule: 3377 @@ -847,7 +847,7 @@ - espace: contrat salarié nom: plafond sécurité sociale période: flexible - formule: plafond sécurité sociale * quotité de travail + formule: plafond sécurité sociale temps plein * quotité de travail - espace: contrat salarié nom: SMIC temps plein @@ -2501,6 +2501,7 @@ - espace: impôt nom: revenu abattu par défaut + période: flexible description: Dans le cas général, l'impôt est calculé après l'application d'un abattement forfaitaire fixe. Chacun peut néanmoins opter pour la déclaration de ses *frais réels*, qui viendront remplacer ce forfait par défaut. formule: allègement: @@ -2546,6 +2547,17 @@ contrat salarié . rémunération . net imposable: 4000 valeur attendue: 7253.26 +- nom: revenu net de cotisations + période: flexible + formule: + somme: + - contrat salarié . salaire . net + - indépendant . revenu net de cotisations + - micro entreprise . revenu net de cotisations + +- nom: revenu net d'impôt + période: flexible + formule: revenu net de cotisations - impôt . impôt sur le revenu à payer - espace: entreprise nom: chiffre d'affaires @@ -2625,7 +2637,7 @@ alors: barème continu: assiette: base des cotisations - multiplicateur: plafond sécurité sociale + multiplicateur: plafond sécurité sociale temps plein points: 0: 1.5% 1.1: 6.5% @@ -2639,7 +2651,7 @@ multiplication: assiette: base des cotisations taux: 0.85% - plafond: 5 * plafond sécurité sociale + plafond: 5 * plafond sécurité sociale temps plein - espace: indépendant . cotisations . maladie nom: artisans commerçants @@ -2652,7 +2664,7 @@ formule: barème continu: assiette: base des cotisations - multiplicateur: plafond sécurité sociale + multiplicateur: plafond sécurité sociale temps plein points: 0: 0% 0.4: 3.16% @@ -2670,7 +2682,7 @@ - espace: indépendant . cotisations . maladie . artisans commerçants . part revenus élevés nom: seuil période: flexible - formule: 5 * plafond sécurité sociale + formule: 5 * plafond sécurité sociale temps plein - espace: indépendant . cotisations @@ -2753,7 +2765,7 @@ formule: multiplication: assiette: base des cotisations - plafond: plafond sécurité sociale + plafond: plafond sécurité sociale temps plein taux: 1.3% # TODO invalidité décès pour les libéraux @@ -2790,7 +2802,7 @@ multiplication: assiette: base des cotisations taux: 0.29% - plafond: plafond sécurité sociale + plafond: plafond sécurité sociale temps plein # C'est le taux pour les artisans. # TODO Pour les commerçants, le taux est de 0.25% # @@ -2800,18 +2812,13 @@ formule: barème continu: assiette: base des cotisations - multiplicateur: plafond sécurité sociale + multiplicateur: plafond sécurité sociale temps plein points: 0: 0% 1.1: 0% 1.4: 3.1% -- espace: indépendant - nom: revenu net de cotisations - période: flexible - formule: base des cotisations - prélèvements - - espace: indépendant nom: revenu net de cotisations période: flexible diff --git a/source/selectors/analyseSelectors.js b/source/selectors/analyseSelectors.js index 9df9fda2f..49d4f8962 100644 --- a/source/selectors/analyseSelectors.js +++ b/source/selectors/analyseSelectors.js @@ -12,10 +12,9 @@ import { rulesFr as baseRulesFr } from 'Engine/rules' import { analyse, analyseMany, parseAll } from 'Engine/traverse' -import { contains, equals, head, isEmpty, isNil, path, pick } from 'ramda' +import { contains, equals, head, isEmpty, pick } from 'ramda' import { getFormValues } from 'redux-form' import { createSelector, createSelectorCreator, defaultMemoize } from 'reselect' -import { mainTargetNames } from '../config' // create a "selector creator" that uses deep equal instead of === const createDeepEqualSelector = createSelectorCreator(defaultMemoize, equals) @@ -52,7 +51,7 @@ export let situationSelector = createDeepEqualSelector( export let noUserInputSelector = createSelector( [situationSelector], - situation => console.log({ situation }) || !situation || isEmpty(situation) + situation => !situation || isEmpty(situation) ) export let formattedSituationSelector = createSelector( @@ -68,14 +67,20 @@ let validatedStepsSelector = createSelector( (foldedSteps, target) => [...foldedSteps, target] ) -export let validatedSituationSelector = createSelector( - [formattedSituationSelector, validatedStepsSelector], - (situation, validatedSteps) => pick(validatedSteps, situation) +let situationBranchesSelector = createSelector( + [formattedSituationSelector, (state, props) => props?.branches || [{}]], + (situation, branches) => + branches.map(branchPatch => ({ ...situation, ...branchPatch })) +) +export let validatedSituationsSelector = createSelector( + [situationBranchesSelector, validatedStepsSelector], + (situations, validatedSteps) => situations.map(s => pick(validatedSteps, s)) ) -let situationWithDefaultsSelector = createSelector( - [ruleDefaultsSelector, formattedSituationSelector], - (defaults, situation) => ({ ...defaults, ...situation }) +let situationsWithDefaultsSelector = createSelector( + [ruleDefaultsSelector, situationBranchesSelector], + (defaults, situations) => + situations.map(situation => ({ ...defaults, ...situation })) ) let analyseRule = (parsedRules, ruleDottedName, situation) => @@ -87,20 +92,21 @@ export let ruleAnalysisSelector = createSelector( [ parsedRulesSelector, (_, { dottedName }) => dottedName, - situationWithDefaultsSelector + situationsWithDefaultsSelector ], - analyseRule + (rules, dottedName, situations) => + analyseRule(rules, dottedName, situations[0]) ) let exampleSituationSelector = createSelector( [ parsedRulesSelector, - situationWithDefaultsSelector, + situationsWithDefaultsSelector, ({ currentExample }) => currentExample ], - (rules, situation, example) => + (rules, situations, example) => example && { - ...situation, + ...situations[0], ...disambiguateExampleSituation( rules, findRuleByDottedName(rules, example.dottedName) @@ -119,15 +125,19 @@ export let exampleAnalysisSelector = createSelector( let makeAnalysisSelector = situationSelector => createDeepEqualSelector( [parsedRulesSelector, targetNamesSelector, situationSelector], - (parsedRules, targetNames, situation) => - analyseMany(parsedRules, targetNames)(dottedName => situation[dottedName]) + (parsedRules, targetNames, situations) => { + let analyses = situations.map(s => + analyseMany(parsedRules, targetNames)(dottedName => s[dottedName]) + ) + return situations.length === 1 ? analyses[0] : analyses + } ) export let analysisWithDefaultsSelector = makeAnalysisSelector( - situationWithDefaultsSelector + situationsWithDefaultsSelector ) let analysisValidatedOnlySelector = makeAnalysisSelector( - validatedSituationSelector + validatedSituationsSelector ) export let blockingInputControlsSelector = state => { diff --git a/source/selectors/regleSelectors.js b/source/selectors/regleSelectors.js index 1963d3162..b98b7b6c0 100644 --- a/source/selectors/regleSelectors.js +++ b/source/selectors/regleSelectors.js @@ -7,7 +7,7 @@ import { createSelector } from 'reselect' import { analysisWithDefaultsSelector, flatRulesSelector, - validatedSituationSelector + validatedSituationsSelector } from './analyseSelectors' import type { FlatRules } from 'Types/State' import type { @@ -43,9 +43,9 @@ export const règleLocaliséeSelector = createSelector( export const règleValeurSelector = createSelector( analysisWithDefaultsSelector, - validatedSituationSelector, + validatedSituationsSelector, règleLocaliséeSelector, - (analysis: Analysis, situation, règleLocalisée: string => Règle) => ( + (analysis: Analysis, situations, règleLocalisée: string => Règle) => ( dottedName: string ): RègleValeur => { if (!analysis) { @@ -58,7 +58,9 @@ export const règleValeurSelector = createSelector( analysis.targets.find(target => target.dottedName === dottedName) let valeur = - rule && !isNil(rule.nodeValue) ? rule.nodeValue : situation[dottedName] + rule && !isNil(rule.nodeValue) + ? rule.nodeValue + : situations[0][dottedName] if (isNil(valeur)) { console.warn( diff --git a/source/sites/publi.codes/App.js b/source/sites/publi.codes/App.js index 9723505fd..2df391b8a 100644 --- a/source/sites/publi.codes/App.js +++ b/source/sites/publi.codes/App.js @@ -5,7 +5,7 @@ import 'Ui/index.css' import Provider from '../../Provider' import Route404 from '../embauche.gouv.fr/pages/Route404' import RulesList from '../embauche.gouv.fr/pages/RulesList' -import GenericSimulation from 'Components/GenericSimulation' +import ComparativeSimulation from 'Components/ComparativeSimulation' import { simulationTargetNames } from '../../config' class App extends Component { @@ -18,7 +18,7 @@ class App extends Component { }} reduxMiddlewares={[]}> - +