Nouveau composant de simulation qui compare des situations

barème-continu
Mael 2018-12-11 13:50:53 +01:00
parent 568768eaa8
commit 3f9b06cd0a
10 changed files with 195 additions and 43 deletions

View File

@ -15,9 +15,6 @@ rules:
react/no-unescaped-entities: 0
react/display-name: 1
parser: babel-eslint
parserOptions:
ecmaFeatures:
legacyDecorators: true
plugins:
- react

View File

@ -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 (
<div id="GenericSimulation">
<header>
<img src="https://images.unsplash.com/photo-1488722796624-0aa6f1bb6399?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1350&q=80" />
<h1>Quel revenu au régime des indépendants ?</h1>
<PeriodSwitch />
</header>
<div className="ui__ container" id="simulationContent">
{!isEmpty(previousAnswers) && (
<button
style={{
background: colours.colour,
color: colours.textColour
}}
onClick={() => this.setState({ displayAnswers: true })}>
Mes réponses
</button>
)}
{this.state.displayAnswers && (
<Answers
onClose={() => this.setState({ displayAnswers: false })}
/>
)}
<Conversation
textColourOnWhite={this.props.colours.textColourOnWhite}
/>
<Controls {...{ controls }} />
{noNextSteps && (
<>
<h2>Plus de questions ! </h2>
<p>Vous avez atteint l'estimation la plus précise.</p>
</>
)}
<ComparativeTargets />
</div>
</div>
)
}
}
)

View File

@ -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 (
<div id="targets">
<div
className="content"
style={{ color: this.props.colours.textColour }}>
<ul>
{this.props.analyses.map((analysis, i) => {
let { title, nodeValue, dottedName } = analysis.targets[0]
return (
<li>
{branches[i].nom}
<span className="figure">
<span className="value">{nodeValue?.toFixed(1)}</span>{' '}
</span>
<Link
title="Quel est calcul ?"
style={{ color: this.props.colours.colour }}
to={'/règle/' + dottedName}
className="explanation">
{emoji('📖')}
</Link>
</li>
)
})}
</ul>
</div>
</div>
)
}
}
)

View File

@ -0,0 +1,7 @@
- nom: Revenu en indépendant
situation:
indépendant: oui
- situation:
micro entreprise: oui
nom: revenu en micro-entreprise

View File

@ -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]

View File

@ -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

View File

@ -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

View File

@ -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 => {

View File

@ -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(

View File

@ -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={[]}>
<Switch>
<Route exact path="/" component={GenericSimulation} />
<Route exact path="/" component={ComparativeSimulation} />
<Route path="/règle/:name" component={RulePage} />
<Route path="/règles" component={RulesList} />
<Route component={Route404} />