Merge pull request #2 from sgmap/toute-regle-se-simule

Toute règle se simule; exemples sur /règle; résolutions de bugs
pull/6/head
Mael 2017-05-11 11:12:23 +02:00 committed by GitHub
commit 5d7654fca8
39 changed files with 738 additions and 370 deletions

View File

@ -22,7 +22,6 @@
"ramda": "^0.23.0",
"react": "^15.5.4",
"react-dom": "^15.5.4",
"react-json-tree": "^0.10.0",
"react-redux": "^5.0.3",
"react-router-dom": "^4.1.1",
"reduce-reducers": "^0.1.2",

View File

@ -0,0 +1,43 @@
Etant donné que pour des objectifs choisis (ex. surcoût CDD), on construit automatiquement le formulaire, il faut déterminer l'ordre des questions.
Plusieurs implémentations sont possibles. On va les illustrer justement avec cet exemple du surcoût CDD.
Objectifs :
- CIF
- majoration chômage
- prime précarité
- compensation congés payés (CP).
Variables manquantes :
- salaire de base
- cdd . événements de contrat (groupe de questions)
- cdd . motifs CDD (groupe de questions)
- cdd . durée contrat
- cdd . contrat jeune vacances
- cdd . jours de congés non pris
### Par occurence
On peut simplement compter, pour chaque variable manquante, le nombre de fois où sa valeur est demandée.
On remarquera alors que `salaire de base` et `événements de contrat` sont à égalité, devant `motif` et les autres.
### Par profondeur dans le modèle
Cette égalité dans la méthode par occurences pose problème : les tests utilisateurs montrent que mettre la question `événements` en première position suprend, et ce malgé son omniprésence dans le calcul.
On peut imaginer, avec un raisonnement pas très précis certes, que ce caractère surprenant peut en partie être expliqué par sa profondeur dans le modèle de donnée : elle est plus spécialisée que `salaire de base`.
### Par impact sur le calcul
On peut aussi raisonner en impact sur le calcul : quelle est l'influence d'une variable sur la valeur cumulée de nos objectifs (sommés ici).
L'objectif alors est de rapidement donner une estimation des résultats de la simulation à l'utilisateur, en €.
La variable `salaire de base` devient donc totalement prioritaire, car c'est la seule qui manque pour donner une première fourchette de résultats : les objectifs sont en simplifiant une fonction affine du `salaire de base`.
Autrement dit, c'est la seule variable non bornée de la simulation.
#TODO
La variable `contrat jeune vacances` est utilisée comme condition d'applicabilité par deux variables.

View File

@ -0,0 +1,23 @@
# AFAIRE :
# - gérer l'aspect temporel : indemnité par jour, convertible proportionnellement.
# - les réductions de cotisations / impôts
# - créer et documenter les entités accord employeur, distance minimale domicile travail
. : ikv
nom: indemnité kilométrique vélo
description: Indemnité Kilométrique Vélo pour les salariés qui pédalent entre leur domicile et leur lieu de travail
formule:
applicable si: accord employeur ikv
multiplication:
assiette: 0.25
facteur: distance minimale domicile travail
# par jour travaillé
réduction:
cibles:
- cotisations sociales #pour l'employeur
- salaire net imposable #pour le salarié
plafond: 200
références:
Indemnité kilométrique: https://www.service-public.fr/professionnels-entreprises/vosdroits/F33808

View File

@ -19,3 +19,21 @@
références:
Code du travail - Article L6322-37 : https://www.legifrance.gouv.fr/affichCodeArticle.do?idArticle=LEGIARTI000022234996&cidTexte=LEGITEXT000006072050
exemples:
- nom: SMIC
situation:
assiette cotisations sociales: 1480
valeur attendue: 14.8
- nom: salaire médian
situation:
motif . classique . usage: oui
assiette cotisations sociales: 2300
valeur attendue: 23
- nom: motif saisonnier -> non applicable
situation:
motif . classique . saisonnier: oui
assiette cotisations sociales: 2300
valeur attendue: null

View File

@ -16,7 +16,8 @@
# TODO aspect temporel
# L'indemnité est versée à la fin du contrat, sauf si le CDD se poursuit par un CDI.
#TODO avoir un vrai mécanisme de calcul temporel pour faire les conversions
#TODO cette formule pourrait être clarifiée,
# probablement grâce à un vrai mécanisme de calcul temporel pour faire les conversions
formule:
le maximum de:
- description: Méthode "du dixième"
@ -46,6 +47,30 @@
facteur: 1 / 21
exemples:
- nom: pas de congés non pris
situation:
salaire brut: 2300
prime fin de contrat: 0
congés non pris: 0
durée contrat: 12
valeur attendue: 0
- nom: 10 jours non pris
situation:
salaire brut: 2300
prime fin de contrat: 0
congés non pris: 10
durée contrat: 12
valeur attendue: 92
- nom: 3 jours non pris
situation:
salaire brut: 2300
prime fin de contrat: 0
congés non pris: 3
durée contrat: 6
valeur attendue: 54.76
notes: |
À noter, la loi El Khomri modifie l'article L3141-12:

View File

@ -10,7 +10,6 @@
- Dans les faits, les CDD Senior perçoivent une indemnité dun montant équivalent à lindemnité de précarité : [line](https://www.easycdd.com/LEGISLATION-CDD/Fin-ou-rupture-du-contrat-CDD/La-prime-de-precarite/La-prime-de-precarite-n-est-pas-due-si)
non applicable si:
une de ces conditions:
# Evènements particuliers
@ -37,6 +36,19 @@
assiette: salaire brut
taux: 10%
exemples:
- nom: salaire médian
situation:
salaire brut: 2300
valeur attendue: 230
- nom: CDD d'usage -> non applicable
situation:
motif . classique . usage: oui
événement . refus CDI avantageux: oui
salaire brut: 2300
valeur attendue: null
références:
Code du travail - Article L1243-8: https://www.legifrance.gouv.fr/affichCode.do?idSectionTA=LEGISCTA000006189459&cidTexte=LEGITEXT000006072050

View File

@ -25,6 +25,25 @@
durée contrat <= 3: 0.5%
exemples:
- nom: salaire médian, CDD d'usage, contrat de 2 mois
situation:
assiette cotisations sociales: 2300
motif . classique . accroissement activité: oui
durée contrat: 1
valeur attendue: 69
#TODO on ne peut aujourd'hui tester 'classique . usage' : l'évaluation n'aura pas la valeur de la première branche de la logique numérique, motif . classique . accroissement activité, et va donc s'arrêter en renvoyant un 'null'
# solution possible : un mode d'évaluation 'shallow' ou non renseigné = faux (null -> false)
- nom: durée de contrat de 4 mois -> non applicable
situation:
assiette cotisations sociales: 2300
durée contrat: 4
valeur attendue: null
références:
La mojoration de la contribution chômage: https://www.urssaf.fr/portail/home/employeur/calculer-les-cotisations/les-taux-de-cotisations/lassurance-chomage-et-lags/la-majoration-de-la-contribution.html

View File

@ -6,14 +6,12 @@
destinataire: AGIRC
description: |
Cotisation de retraite complémentaire cadre. Complète le régime ARRCO, gérée par l'AGIRC (Association Générale des Institutions de Retraite des Cadres)
références:
calcul des cotisations: http://www.agirc-arrco.fr/entreprises/gerer-les-salaries/calcul-des-cotisations/
garantie minimale de points: http://www.journaldunet.com/management/pratique/primes-et-avantages/5079/gmp-2016-la-garantie-minimale-de-points-calcul-et-montant.html
non applicable si: statut cadre # TODO l'inverse ! Vérifier le fonctionnnement de la négation !statut cadre OU statut cadre = non OU applicable si: statut cadre
#TODO double négation en attendant d'ajouter 'applicable si'
non applicable si: ≠ statut cadre
formule:
barème:
assiette: salaire de base #TODO assiette cotisations sociales
assiette: salaire de base #TODO devrait être assiette cotisations sociales. Mais elle contient les primes CDD
multiplicateur des tranches: plafond sécurité sociale
composantes:
- attributs:
@ -62,4 +60,8 @@
taux: 0%
notes: |
Il éxiste une tranche C, de 4 à 8 fois la base, sur laquelle la répartition des cotisations est décidée au sein de lentreprise jusquà 20 %. De 20 % à 20,30 %, la répartition est la suivante : 66,67 % à la charge du salarié et 33,33 % pour lemployeur.
Il existe une tranche C, de 4 à 8 fois la base, sur laquelle la répartition des cotisations est décidée au sein de lentreprise jusquà 20 %. De 20 % à 20,30 %, la répartition est la suivante : 66,67 % à la charge du salarié et 33,33 % pour lemployeur.
références:
calcul des cotisations: http://www.agirc-arrco.fr/entreprises/gerer-les-salaries/calcul-des-cotisations/
garantie minimale de points: http://www.journaldunet.com/management/pratique/primes-et-avantages/5079/gmp-2016-la-garantie-minimale-de-points-calcul-et-montant.html

View File

@ -12,7 +12,8 @@
notes: |
Avant 2011, il y avait une cotisation forfaitaire au lieu de la tranche A
non applicable si: statut cadre # TODO l'inverse ! Vérifier le fonctionnnement de la négation !statut cadre OU statut cadre = non OU applicable si: statut cadre
#TODO double négation en attendant d'ajouter 'applicable si'
non applicable si: ≠ statut cadre
formule:
barème:

View File

@ -32,6 +32,7 @@
références:
Code du travail - Article L1242-2: https://www.legifrance.gouv.fr/affichCodeArticle.do;jsessionid=714D2E2B814371F4F1D5AA88472CD621.tpdila20v_1?idArticle=LEGIARTI000033024658&cidTexte=LEGITEXT000006072050&dateTexte=20170420
- espace: contrat salarié . CDD . motif . classique
nom: saisonnier
titre: Saisonnier

View File

@ -63,3 +63,16 @@
- majoration chômage #cotisation
- prime fin de contrat #indemnité
- compensation congés payés #indemnité
simulateur:
titre: Simulateur CDD
sous-titre: Découvrir le surcoût employeur du CDD par rapport au CDI
introduction:
notes:
- icône: fa-handshake-o
texte: Vous avez embauché ou vous réfléchissez à l'embauche d'un salarié en CDD.
titre: Votre situation
- icône: fa-balance-scale
texte: Votre contrat ne peut donc avoir ni pour objet ni pour effet de pourvoir durablement un emploi lié à l'activité normale et permanente de l'entreprise.
titre: Votre obligation
motivation: Découvrez en quelques clics le montant des 4 obligations du CDD.

View File

@ -54,6 +54,8 @@
- espace: contrat salarié
nom: statut cadre
titre: Statut cadre
question: Le salarié a-t-il le statut cadre ?
description: Notion mal définie mais reconnue par les conventions collectives et déterminant l'appartenance à une caise de retraite de base spécifique

View File

@ -1,31 +0,0 @@
# Les simulateurs sont définis ici. Ils ont une variable comme objectif de calcul. Le moteur va analyser cette variable et son arbre pour produire un formulaire. Il sera exposé sur /simu/:id
#TODO pourquoi ne pas définir les simulateurs directement dans leurs variables d'objectif ?
- titre: Simulateur CDD
sous-titre: Découvrir le surcoût employeur du CDD par rapport au CDI
id: cdd
introduction:
- icône: fa-handshake-o
texte: Vous avez embauché ou vous réfléchissez à l'embauche d'un salarié en CDD.
titre: Votre situation
- icône: fa-balance-scale
texte: Votre contrat ne peut donc avoir ni pour objet ni pour effet de pourvoir durablement un emploi lié à l'activité normale et permanente de l'entreprise.
titre: Votre obligation
action:
texte: Découvrez en quelques clics le montant des 4 obligations du CDD.
bouton: C'est parti !
objectif: surcoût CDD
conclusion: |
Nous n'avons plus de questions : votre simulation est terminée.
Cliquez sur les obligations en bas pour comprendre vos résultats.
Une remarque ? [Écrivez-nous](/contact) !
# <i
# style={{cursor: 'pointer'}}
# className="fa fa-envelope-o"
# />
# - titre: Du brut au net

View File

@ -3,7 +3,7 @@ import {connect} from 'react-redux'
import {findRuleByDottedName} from '../engine/rules'
import './Aide.css'
import {EXPLAIN_VARIABLE} from '../actions'
import References from './References'
import References from './rule/References'
import marked from '../engine/marked'
@connect(

View File

@ -25,7 +25,7 @@ export let AttachDictionary = dictionary => Decorated =>
render(){
let {explanation, term} = this.state
return (
<div className="dictionaryWrapper">
<div style={{display: 'inline-block'}} className="dictionaryWrapper">
<Decorated ref={decorated => this.decorated = decorated} {...this.props} explain={this.explain}/>
{explanation &&
<div className="dictionaryPanelWrapper" onClick={() => this.setState({term: null, explanation: null})}>

View File

@ -30,7 +30,7 @@ export default class HomeEmbauche extends Component {
</div>
<div>
<span>Nouveau</span>
<a href="/simu/cdd/intro" target="_blank">Simuler le surcoût CDD (beta) <i className="fa fa-hand-o-right" aria-hidden="true"></i></a>
<a href="/simu/surcoût-CDD/intro">Simuler le surcoût CDD (beta) <i className="fa fa-hand-o-right" aria-hidden="true"></i></a>
</div>
</div>
</div>

View File

@ -1,8 +1,7 @@
import React, {Component} from 'react'
import './HomeSyso.css'
import {searchRules} from '../engine/rules.js'
import {searchRules, encodeRuleName} from '../engine/rules.js'
import {Link} from 'react-router-dom'
import '../components/Rule.css'
import R from 'ramda'
export default class Home extends Component {
@ -48,7 +47,7 @@ export default class Home extends Component {
{type}
</span>
<span className="rule-name">
<Link to={`/regle/${name}`}>{name}</Link>
<Link to={`/regle/${encodeRuleName(name)}`}>{name}</Link>
</span>
</li>
))}
@ -60,7 +59,7 @@ export default class Home extends Component {
<ul>
<li key="cdd">
<span className="simulateur">Surcoût du CDD</span>
<Link to="/simu/cdd/intro"><button>Essayer</button></Link>
<Link to="/simu/surcoût-CDD/intro"><button>Essayer</button></Link>
</li>
<li key="embauche">
<span className="simulateur">Prix global de l'embauche</span>

View File

@ -4,10 +4,12 @@ import {Link} from 'react-router-dom'
import {connect} from 'react-redux'
import R from 'ramda'
import './Results.css'
import {capitalise0} from '../utils'
import {computeRuleValue} from '../engine/traverse'
import {encodeRuleName, getObjectives} from '../engine/rules'
let fmt = new Intl.NumberFormat('fr-FR').format
let humanFigure = decimalDigits => value => fmt(value.toFixed(decimalDigits))
let capitalize = name => R.head(name).toUpperCase() + R.tail(name)
@connect(
state => ({
pointedOutObjectives: state.pointedOutObjectives,
@ -17,9 +19,10 @@ let capitalize = name => R.head(name).toUpperCase() + R.tail(name)
)
export default class Results extends Component {
render() {
let {analysedSituation, pointedOutObjectives, conversationStarted} = this.props
// On travaille pour l'instant sur un objectif qui est une somme de plusieurs variables, et c'est ces variables que nous affichons comme résultats. D'où ce chemin :
let explanation = R.path(['formule', 'explanation', 'explanation'])(analysedSituation)
let {analysedSituation, pointedOutObjectives, conversationStarted} = this.props,
explanation = getObjectives(analysedSituation)
if (!explanation) return null
return (
@ -31,23 +34,25 @@ export default class Results extends Component {
</div>
<ul>
{explanation.map(
({variableName, nodeValue, explanation: {name, type, 'non applicable si': nonApplicable, formule: {nodeValue: computedValue}}}) =>
({name, dottedName, type, 'non applicable si': nonApplicable, formule: {nodeValue: formuleValue}}) =>
do {
//TODO quel bordel, à revoir
let
unsatisfied = nodeValue == null,
ruleValue = computeRuleValue(formuleValue, nonApplicable && nonApplicable.nodeValue),
unsatisfied = ruleValue == null,
nonApplicableValue = nonApplicable ? nonApplicable.nodeValue : false,
irrelevant = nonApplicableValue === true || computedValue == 0,
number = nonApplicableValue == false && computedValue != null,
pointedOut = pointedOutObjectives.find(objective => objective == variableName)
irrelevant = nonApplicableValue === true || formuleValue == 0,
number = nonApplicableValue == false && formuleValue != null,
pointedOut = pointedOutObjectives.find(objective => objective == dottedName)
;<li key={name} className={classNames({unsatisfied, irrelevant, number, pointedOut})}>
<Link to={"/regle/" + name} className="understand">
<Link to={"/regle/" + encodeRuleName(name)} className="understand">
<div className="rule-box">
<div className="rule-type">
{type}
</div>
<div className="rule-name">
{capitalize(name)}
{capitalise0(name)}
</div>
<p>
{conversationStarted && (
@ -55,7 +60,7 @@ export default class Results extends Component {
"Vous n'êtes pas concerné"
: unsatisfied ?
'En attente de vos réponses...'
: <span className="figure">{humanFigure(2)(computedValue) + '€'}</span>
: <span className="figure">{humanFigure(2)(formuleValue) + '€'}</span>
)}
</p>
</div>

View File

@ -1,178 +0,0 @@
import React, { Component } from 'react'
import ReactDOM from 'react-dom'
// import {findRuleByName} from '../engine/rules.js'
import './Rule.css'
import JSONTree from 'react-json-tree'
import R from 'ramda'
import PageTypeIcon from './PageTypeIcon'
import {connect} from 'react-redux'
import mockSituation from '../engine/mockSituation.yaml'
import {START_CONVERSATION} from '../actions'
import classNames from 'classnames'
import possiblesDestinataires from '../../règles/ressources/destinataires/destinataires.yaml'
import {capitalise0} from '../utils'
import knownMecanisms from '../engine/known-mecanisms.yaml'
import marked from '../engine/marked'
import References from './References'
import {AttachDictionary} from './AttachDictionary'
// situationGate function useful for testing :
let testingSituationGate = v => // eslint-disable-line no-unused-vars
R.path(v.split('.'))(mockSituation)
@connect(
state => ({
// situationGate: name => formValueSelector('conversation')(state, name),
analysedSituation: state.analysedSituation,
form: state.form
}),
dispatch => ({
startConversation: rootVariable => dispatch({type: START_CONVERSATION, rootVariable}),
})
)
export default class Rule extends Component {
componentDidMount() {
// C'est ici que la génération du formulaire, et donc la traversée des variables commence
this.props.startConversation('surcoût CDD')
}
render() {
let {
match: {params: {name}},
analysedSituation,
form
} = this.props,
objectives = R.path(['formule', 'explanation', 'explanation'])(analysedSituation)
if (!objectives) return null
let rule = objectives.find(R.pathEq(['explanation', 'name'], name)).explanation
if (!rule) {
this.props.router.push('/404')
return null
}
let
situationExists = !R.isEmpty(form)
let destinataire = R.path([rule.type, 'destinataire'])(rule),
destinataireData = possiblesDestinataires[destinataire]
return (
<div id="rule">
<PageTypeIcon type="comprendre"/>
<h1>
<span className="rule-type">{rule.type}</span>
<span className="rule-name">{capitalise0(name)}</span>
</h1>
<section id="rule-meta">
<div id="meta-paragraph">
<p>
{rule.description}
</p>
</div>
<div id="destinataire">
<h2>Destinataire</h2>
{!destinataireData ?
<p>Non renseigné</p>
: <div>
<a href={destinataireData.lien} target="_blank">
{destinataireData.image &&
<img src={require('../../règles/ressources/destinataires/' + destinataireData.image)} /> }
{!destinataireData.image &&
<div id="calligraphy">{destinataire}</div>
}
</a>
{destinataireData.nom && <div id="destinataireName">{destinataireData.nom}</div>}
</div>
}
</div>
<div>
<h2>Références</h2>
{this.renderReferences(rule)}
</div>
</section>
<Algorithm {...{rule, situationExists}}/>
</div>
)
}
renderReferences({'références': refs}) {
if (!refs) return <p>Cette règle manque de références.</p>
return <References refs={refs}/>
}
}
@AttachDictionary(knownMecanisms)
class Algorithm extends React.Component {
state = {
showValues: false
}
render(){
let {rule, situationExists, explain} = this.props,
showValues = situationExists && this.state.showValues
return (
<div id="algorithm">
<section id="rule-rules" className={classNames({showValues})}>
{ do {
// TODO ce let est incompréhensible !
let [,cond] =
R.toPairs(rule).find(([,v]) => v && v.rulePropType == 'cond') || []
cond != null &&
<section id="declenchement">
<h2>Conditions de déclenchement</h2>
{cond.jsx}
</section>
}}
<section id="formule">
<h2>Calcul</h2>
{rule['formule'].jsx}
</section>
</section>
{situationExists && <div>
<button id="showValues" onClick={() => this.setState({showValues: !this.state.showValues})}>
<i className="fa fa-rocket" aria-hidden="true"></i> &nbsp;{!showValues ? 'Injecter votre situation' : 'Cacher votre situation'}
</button>
</div>}
</div>
)
}
}
let JSONView = ({o, rootKey}) => (
<div className="json">
<JSONTree
getItemString={() => ''}
theme={theme}
hideRoot={true}
shouldExpandNode={() => true}
data={rootKey ? {[rootKey]: o} : o}
/>
</div>
)
var theme = {
scheme: 'atelier forest',
author: 'bram de haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/forest)',
base00: '#1b1918',
base01: '#2c2421',
base02: '#68615e',
base03: '#766e6b',
base04: '#9c9491',
base05: '#a8a19f',
base06: '#e6e2e0',
base07: '#f1efee',
base08: '#f22c40',
base09: '#df5320',
base0A: '#d5911a',
base0B: '#5ab738',
base0C: '#00ad9c',
base0D: '#407ee7',
base0E: '#6666ea',
base0F: '#c33ff3'
}

View File

@ -98,16 +98,20 @@
opacity: .95;
}
#reinitialise {
#foldedSteps .header {
margin-bottom: 1em;
text-align: center;
}
#reinitialise button {
#foldedSteps .header h3 {
display: inline;
}
#foldedSteps .header button {
font-size: 80%;
color: #4A89DC;
border: none;
}
#reinitialise button i {
#foldedSteps .header button i {
margin-right: .3em;
vertical-align: top
}

View File

@ -6,12 +6,14 @@ import './conversation/conversation.css'
import {START_CONVERSATION} from '../actions'
import Aide from './Aide'
import PageTypeIcon from './PageTypeIcon'
import simulateurs from '../../règles/simulateurs.yaml'
import R from 'ramda'
import {Redirect, Link, withRouter} from 'react-router-dom'
import {createMarkdownDiv} from '../engine/marked'
import './Simulateur.css'
import classNames from 'classnames'
import {findRuleByName, decodeRuleName} from '../engine/rules'
import {capitalise0} from '../utils'
let situationSelector = formValueSelector('conversation')
@withRouter
@ -34,56 +36,61 @@ export default class extends React.Component {
let {
match: {
params: {
simulateurId
name: encodedName
}
}
} = this.props
} = this.props,
name = decodeRuleName(encodedName)
this.simulateurId = simulateurId
this.simulateur = R.find(R.propEq('id', simulateurId))(simulateurs)
this.encodedName = encodedName
this.name = name
this.rule = findRuleByName(name)
// C'est ici que la génération du formulaire, et donc la traversée des variables commence
if (this.simulateur)
this.props.startConversation(this.simulateur.objectif)
if (this.rule.formule)
this.props.startConversation(name)
}
render(){
if (!this.simulateur) return <Redirect to="/404"/>
if (!this.rule.formule) return <Redirect to="/404"/>
let
started = !this.props.match.params.intro,
{foldedSteps, unfoldedSteps, situation} = this.props,
sim = path =>
R.path(R.unless(R.is(Array), R.of)(path))(this.simulateur),
objectif = this.simulateur.objectif,
R.path(R.unless(R.is(Array), R.of)(path))(this.rule.simulateur || {}),
reinitalise = () => {
this.props.resetForm(objectif);
this.props.startConversation(objectif);
this.props.resetForm(this.name);
this.props.startConversation(this.name);
}
return (
<div id="sim" className={classNames({started})}>
<PageTypeIcon type="simulation" />
<h1>{sim('titre')}</h1>
<div id="simSubtitle">{sim('sous-titre')}</div>
<div className="intro centered">
{sim('introduction').map( ({icône, texte, titre}) =>
<div key={titre}>
<i title={titre} className={"fa "+icône} aria-hidden="true"></i>
<span>
{texte}
</span>
</div>
)}
</div>
<h1>{sim('titre') || capitalise0(this.rule['titre'] || this.rule['nom'])}</h1>
{sim('sous-titre') &&
<div id="simSubtitle">{sim('sous-titre')}</div>
}
{sim(['introduction', 'notes']) &&
<div className="intro centered">
{sim(['introduction', 'notes']).map( ({icône, texte, titre}) =>
<div key={titre}>
<i title={titre} className={"fa "+icône} aria-hidden="true"></i>
<span>
{texte}
</span>
</div>
)}
</div>
}
{
// Tant que le bouton 'C'est parti' n'est pas cliqué, on affiche l'intro
!started ?
<div>
<div className="action centered">
<p>{sim(['action', 'texte'])}</p>
<button onClick={() => this.props.history.push(`/simu/${this.simulateurId}`) }>
{sim(['action', 'bouton'])}
<p>{sim(['introduction', 'motivation']) || 'Simulez cette règle en quelques clics'}</p>
<button onClick={() => this.props.history.push(`/simu/${this.encodedName}`) }>
C'est parti !
</button>
</div>
<div className="remarks centered">
@ -101,7 +108,8 @@ export default class extends React.Component {
<div id="questions-answers">
{ !R.isEmpty(foldedSteps) &&
<div id="foldedSteps">
<div id="reinitialise" >
<div className="header" >
<h3>Vos réponses</h3>
<button onClick={reinitalise}>
<i className="fa fa-trash" aria-hidden="true"></i>
Tout effacer
@ -130,10 +138,7 @@ export default class extends React.Component {
}}
</div>
{unfoldedSteps.length == 0 &&
<div id="fin">
<img src={require('../images/fin.png')} />
{createMarkdownDiv(sim('conclusion'))}
</div>}
<Conclusion />}
</div>
<Aide />
</div>
@ -144,3 +149,22 @@ export default class extends React.Component {
)
}
}
class Conclusion extends Component {
render() {
return (
<div id="fin">
<img src={require('../images/fin.png')} />
<p>
Nous n'avons plus de questions : votre simulation est terminée.
</p>
<p>
Cliquez sur les obligations en bas pour comprendre vos résultats.
</p>
<p>
Une remarque ? <Link to="/contact">Écrivez-nous !</Link>
</p>
</div>
)
}
}

View File

@ -0,0 +1,36 @@
import React from 'react'
import classNames from 'classnames'
import R from 'ramda'
import {AttachDictionary} from '../AttachDictionary'
import knownMecanisms from 'Engine/known-mecanisms.yaml'
import marked from 'Engine/marked'
@AttachDictionary(knownMecanisms)
export default class Algorithm extends React.Component {
state = {
showValues: false
}
render(){
let {traversedRule: rule, showValues} = this.props
return (
<div id="algorithm">
<section id="rule-rules" className={classNames({showValues})}>
{ do {
// TODO ce let est incompréhensible !
let [,cond] =
R.toPairs(rule).find(([,v]) => v && v.rulePropType == 'cond') || []
cond != null &&
<section id="declenchement">
<h2>Conditions de déclenchement</h2>
{cond.jsx}
</section>
}}
<section id="formule">
<h2>Calcul</h2>
{rule['formule'].jsx}
</section>
</section>
</div>
)
}
}

View File

@ -0,0 +1,67 @@
#examples {
min-width: 30%;
}
#examples h2.subtitled {
margin-bottom: .1em;
}
#examples .subtitle {
font-size: 85%;
font-style: italic;
margin-top: 0.3em;
margin-bottom: 1.6em
}
#injectSituation {
background: rgb(74, 137, 220);
color: white;
border: none;
font-size: 100%;
padding: .3em .6em;
margin-bottom: .6em;
}
#injectSituation.selected {
font-weight: bold;
}
#examples ul {
padding-left: 0;
list-style-type: none;
}
#examples .example {
font-weight: 500;
margin: .6em;
cursor: pointer;
}
#examples .example.selected .name {
font-weight: bold;
}
#examples i {
margin-right: .6em;
}
#examples .example i {
font-size: 200%;
vertical-align: sub;
}
#examples .example:not(.ok) {
color: #e74c3c;
}
#examples .example .ko {
font-size: 80%;
color: #666;
}
#examples .expected {
font-weight: 700;
color: #031bd6;
}
#examples .example.ok i {
color: #2ecc71
}

View File

@ -0,0 +1,89 @@
import React, { Component } from "react"
import R from "ramda"
import classNames from "classnames"
import {
decodeRuleName,
findRuleByName,
disambiguateRuleReference
} from "Engine/rules.js"
import { analyseSituation } from "Engine/traverse"
import "./Examples.css"
export default class Examples extends Component {
runExamples() {
let { rule } = this.props,
exemples = rule.exemples || []
return exemples.map(ex => {
// les variables dans les tests peuvent être exprimées relativement à l'espace de nom de la règle,
// comme dans sa formule
let exempleSituation = R.pipe(
R.toPairs,
R.map(([k, v]) => [disambiguateRuleReference(rule, k), v]),
R.fromPairs
)(ex.situation)
let runExemple = analyseSituation(rule.name)(v => exempleSituation[v]),
exempleCalculatedValue = runExemple["non applicable si"] &&
runExemple["non applicable si"].nodeValue
? null
: runExemple.formule.nodeValue
return {
...ex,
ok: Math.abs( ex['valeur attendue'] - exempleCalculatedValue ) < .1, //TODO on peut sûrement faire mieux...
rule: runExemple
}
})
}
render() {
let examples = this.runExamples(),
focusedExample = R.path(['focusedExample', 'nom'])(this.props),
{clearInjection, inject, situationExists , showValues} = this.props
return (
<div id="examples">
<h2 className="subtitled">Examples de calcul</h2>
<p className="subtitle">Cliquez sur un exemple pour le visualiser</p>
{situationExists && <div>
<button
className={classNames({selected: !focusedExample && showValues})}
id="injectSituation"
onClick={() => showValues ? clearInjection(): inject()}>
<i className="fa fa-rocket" aria-hidden="true"></i> &nbsp;{focusedExample || !showValues ? 'Injecter votre situation' : 'Cacher votre situation'}
</button>
</div>
}
{R.isEmpty(examples) ?
<p><i className="fa fa-exclamation-triangle" aria-hidden="true"></i><em>Cette règle manque d'exemples...</em></p>
: <ul>{
examples.map(({nom, ok, rule, 'valeur attendue': expected}) =>
<li key={nom} className={classNames("example", {ok, selected: focusedExample == nom})}
onClick={() => focusedExample == nom ? clearInjection() : inject({nom, ok, rule})}
>
<span> {
ok ?
<i className="fa fa-check-circle" aria-hidden="true"></i>
: <i className="fa fa-times" aria-hidden="true"></i>
}</span>
<span className="name">{nom}</span>
{!ok &&
<div className="ko">
Ce test ne passe pas
{showValues && <span>
: la valeur attendue était {' '}
<span className="expected">{expected}</span>
</span>}
</div>
}
</li>
)
}
</ul>
}
</div>
)
}
}

View File

@ -1,7 +1,7 @@
import React from 'react'
import references from '../../règles/ressources/références/références.yaml'
import './References.css'
import R from 'ramda'
import references from 'Règles/ressources/références/références.yaml'
import './References.css'
export default ({refs}) => (
<ul className="references">
@ -18,7 +18,7 @@ export default ({refs}) => (
<span className="url">
{domain}
{refData.image &&
<img src={require('../../règles/ressources/références/' + refData.image)}/> }
<img src={require('Règles/ressources/références/' + refData.image)}/> }
</span>
</span>
<a href={link} target="_blank">

View File

@ -30,7 +30,7 @@
}
#rule h2 {
font-size: 100%;
font-size: 140%;
font-weight: 400;
border-bottom: 1px solid #4B4B66;
display: inline-block;
@ -105,27 +105,17 @@
#rule-rules .situationValue {
display: none;
padding-left: 1em;
font-weight: 600;
color: #4A89DC;
font-weight: 700;
color: #031bd6;
}
#rule-rules.showValues .situationValue {
display: inline;
}
#showValues {
background: rgb(74, 137, 220);
color: white;
border: none;
font-size: 100%;
padding: .4em;
margin: auto;
margin-left: 3em;
}
#algorithm {
margin-top: 3em;
width: 100%;
margin-right: 10em;
}
.dictionaryPanelWrapper {
position: fixed;
@ -170,11 +160,10 @@
#rule-rules section {
margin: 1em 0 1em 3em;
font-weight: 500;
font-size: 90%;
color: #444
}
#rule-rules section h2 {
font-size: 130%;
#rule-rules section > div {
font-size: 90%;
}
.node {
padding-left: 1em;
@ -350,3 +339,12 @@
font-size: 160%;
color: #3498db;
}
#rule-calc {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: baseline;
}

View File

@ -0,0 +1,120 @@
import React, { Component } from 'react'
import ReactDOM from 'react-dom'
import {connect} from 'react-redux'
import {formValueSelector} from 'redux-form'
import R from 'ramda'
import './Rule.css'
import PageTypeIcon from '../PageTypeIcon'
import {decodeRuleName, findRuleByName, disambiguateRuleReference} from 'Engine/rules.js'
import mockSituation from 'Engine/mockSituation.yaml'
import {analyseSituation} from 'Engine/traverse'
import {START_CONVERSATION} from '../../actions'
import possiblesDestinataires from 'Règles/ressources/destinataires/destinataires.yaml'
import {capitalise0} from '../../utils'
import References from './References'
import Algorithm from './Algorithm'
import Examples from './Examples'
// situationGate function useful for testing :
let testingSituationGate = v => // eslint-disable-line no-unused-vars
R.path(v.split('.'))(mockSituation)
@connect(
state => ({
situationGate: name => formValueSelector('conversation')(state, name),
form: state.form
}),
dispatch => ({
startConversation: rootVariable => dispatch({type: START_CONVERSATION, rootVariable}),
})
)
export default class Rule extends Component {
state = {
example: null, showValues: false
}
componentWillReceiveProps(nextProps){
let get = R.path(['match', 'params', 'name'])
if (get(nextProps) !== get(this.props))
this.setRule(get(nextProps))
}
setRule(name){
this.rule = analyseSituation(decodeRuleName(name))(this.props.situationGate)
}
componentWillMount(){
let {
match: {params: {name}},
situationGate
} = this.props
this.setRule(name)
}
render() {
// if (!rule) {
// this.props.router.push('/404')
// return null
// }
let
situationExists = !R.isEmpty(this.props.form)
let
{type, name, description} = this.rule,
destinataire = R.path([type, 'destinataire'])(this.rule),
destinataireData = possiblesDestinataires[destinataire]
return (
<div id="rule">
<PageTypeIcon type="comprendre"/>
<h1>
<span className="rule-type">{type}</span>
<span className="rule-name">{capitalise0(name)}</span>
</h1>
<section id="rule-meta">
<div id="meta-paragraph">
<p>
{description}
</p>
</div>
<div id="destinataire">
<h2>Destinataire</h2>
{!destinataireData ?
<p>Non renseigné</p>
: <div>
<a href={destinataireData.lien} target="_blank">
{destinataireData.image &&
<img src={require('Règles/ressources/destinataires/' + destinataireData.image)} /> }
{!destinataireData.image &&
<div id="calligraphy">{destinataire}</div>
}
</a>
{destinataireData.nom && <div id="destinataireName">{destinataireData.nom}</div>}
</div>
}
</div>
<div>
<h2>Références</h2>
{this.renderReferences(this.rule)}
</div>
</section>
<section id="rule-calc">
<Algorithm {...{traversedRule: R.path(['example', 'rule'])(this.state) || this.rule, showValues: this.state.showValues}}/>
<Examples
situationExists={situationExists}
rule={this.rule}
focusedExample={this.state.example}
showValues={this.state.showValues}
clearInjection={() => this.setState({example: null, showValues: false})}
inject={example => this.setState({example, showValues: true})}/>
</section>
</div>
)
}
renderReferences({'références': refs}) {
if (!refs) return <p>Cette règle manque de références.</p>
return <References refs={refs}/>
}
}

View File

@ -4,7 +4,7 @@ import './reset.css'
import {Link, Route, BrowserRouter as Router, Switch} from 'react-router-dom'
import HomeEmbauche from '../components/HomeEmbauche'
import HomeSyso from '../components/HomeSyso'
import Rule from '../components/Rule'
import Rule from '../components/rule/Rule'
import Route404 from '../components/Route404'
import Contact from '../components/Contact'
import Simulateur from '../components/Simulateur'
@ -38,7 +38,7 @@ export default class Layout extends Component {
<Route exact path="/syso" component={HomeSyso}/>
<Route path="/contact" component={Contact} />
<Route path="/regle/:name" component={Rule} />
<Route path="/simu/:simulateurId/:intro?" component={Simulateur} />
<Route path="/simu/:name/:intro?" component={Simulateur} />
<Route component={Route404} />
</Switch>
</div>

View File

@ -38,10 +38,10 @@ export let reduceSteps = (state, action) => {
}
if (action.type == STEP_ACTION && action.name == 'unfold') {
let stepFinder = R.propEq('name', action.step),
foldedSteps = R.pipe(
R.splitWhen(stepFinder),
R.head
)(state.foldedSteps)
foldedSteps = R.reject(stepFinder)(state.foldedSteps)
if (foldedSteps.length != state.foldedSteps.length - 1)
throw 'Problème lors du dépliement d\'une réponse'
return {
...returnObject,
foldedSteps,
@ -79,9 +79,10 @@ let analyse = rootVariable => R.pipe(
*/
let buildNextSteps = analysedSituation => {
let missingVariables = collectMissingVariables('groupByMissingVariable')(
R.path(['formule', 'explanation', 'explanation'])(analysedSituation)
analysedSituation
)
/*
Parmi les variables manquantes, certaines sont citées dans une règle de type 'une possibilité'.
**On appelle ça des groupes de type 'variante'.**
@ -181,14 +182,6 @@ export let generateGridQuestions = missingVariables => R.pipe(
)(relevantVariants)
})
)
//TODO reintroduce objectives
// {
// objectives: R.pipe(
// R.chain(v => missingVariables[variant.dottedName + ' . ' + v]),
// R.uniq()
// )(variant['une possibilité'])
// }
)
export let generateSimpleQuestions = missingVariables => R.pipe(

View File

@ -1,8 +1,12 @@
# Pour éditer ou comprendre ce fichier, utilisez l'éditeur web Nearley : https://omrelli.ug/nearley-playground/
main ->
CalcExpression {% id %}
| Variable {% id %}
| ModifiedVariable {% id %}
| Comparison {% id %}
CalcExpression {% id %}
| Variable {% id %}
| NegatedVariable {% id %}
| ModifiedVariable {% id %}
| Comparison {% id %}
Comparison -> Comparable _ ComparisonOperator _ Comparable {% d => ({
category: 'comparison',
@ -15,11 +19,16 @@ Comparable -> (int | CalcExpression | Variable) {% d => d[0][0] %}
ComparisonOperator -> ">" | "<" | ">=" | "<=" | "="
NegatedVariable -> "≠" _ Variable {% d => ({category: 'negatedVariable', variable: d[2] }) %}
# Modificateurs temporels pas utilisés aujourd'hui
ModifiedVariable -> Variable _ Modifier {% d => ({category: 'modifiedVariable', modifier: d[2], variable: d[0] }) %}
Modifier -> "[" TemporalModifier "]" {% d =>d[1][0] %}
TemporalModifier -> "annuel" | "mensuel" | "jour ouvré" {% id %}
#-----
CalcExpression -> Term _ ArithmeticOperator _ Term {% d => ({
category: 'calcExpression',
@ -29,7 +38,7 @@ CalcExpression -> Term _ ArithmeticOperator _ Term {% d => ({
}) %}
Term -> Variable {% id %}
| int {% id %}
| int {% id %}
ArithmeticOperator -> "+" {% id %}
| "-" {% id %}

View File

@ -2,6 +2,7 @@
# La description peut être rédigée en markdown :-)
une de ces conditions:
type: boolean
description: |
C'est un `ou` logique.
@ -9,6 +10,7 @@ une de ces conditions:
Renvoie vrai si l'une des conditions est vraie.
toutes ces conditions:
type: boolean
description: |
C'est un `et` logique.
@ -17,6 +19,7 @@ toutes ces conditions:
Renvoie vrai si toutes les conditions vraies.
logique numérique:
type: numeric
description: |
Contient une liste de couples condition-conséquence.
@ -27,10 +30,12 @@ logique numérique:
Si aucune condition n'est vraie, alors ce mécanisme renvoie implicitement `non applicable` (ce qui peut se traduire par la valeur `0` si nous sommes dans un contexte numérique).
taux:
type: numeric
description: |
C'est tout simplement une valeur numérique exprimée en pourcentage.
multiplication:
type: numeric
description: |
C'est une multiplication un peu améliorée, très utile pour exprimer les cotisations.
@ -39,6 +44,7 @@ multiplication:
La multiplication peut être plafonnée : ce plafond sépare l'assiette en deux, et la partie au-dessus du plafond est tout simplement ignorée. Dans ce cas, elle se comporte comme une barème en taux marginaux à deux tranches, la deuxième au taux nul et allant de `plafond` à l'infini.
le maximum de:
type: numeric
description: |
Renvoie l'élément de la liste de propositions fournie qui la la plus grande valeur.
@ -47,6 +53,7 @@ le maximum de:
Il est conseillé de renseigner une description de chaque proposition par exemple quand elles représentent des méthodes de calcul alternatives parmi lesquelles il faut en choisir une.
somme:
type: numeric
description: |
C'est tout simplement la somme de chaque terme de la liste.
@ -71,12 +78,14 @@ non applicable si:
La formule de calcul peut donc être ignorée, quel que soit son montant.
barème:
type: numeric
description: |
C'est un barème en taux marginaux, mécanisme de calcul connu son utilisation dans le calcul de l'impôt sur le revenu.
L'assiette est décomposée en plusieurs tranches, qui sont multipliées par un taux spécifique.
Les tranches sont très souvent exprimées sous forme de facteurs (par exemple [1, 2, 4]) d'une variable que l'on appelle multiplicateur, par exemple le plafond de la sécurité sociale.
composantes:
type: numeric
description: |
Beaucoup de cotisations sont composées de deux parties qui partage la méthode de calcul mais diffèrent par des paramètres différents.

View File

@ -4,26 +4,27 @@ import R from 'ramda'
TODO sélection temporaire de dossier, tant que toute la base de règles n'est pas vérifiée
*/
let objectivesContext = require.context(
'../../règles/rémunération-travail/cdd', true,
// /([a-zA-Z]|-|_)+.yaml$/)
/(CIF|indemnité_fin_contrat|indemnité_compensatrice_congés_payés|majoration-chomage).yaml/)
let requireAll = requireContext =>
requireContext.keys().map(requireContext)
let entityContext = require.context(
'../../règles/rémunération-travail/entités/ok', true)
let objectives = R.pipe(
R.map(objectivesContext),
let rules= R.pipe(
R.chain(
requireAll,
),
R.unnest,
)(objectivesContext.keys())
R.reject(R.isNil)
)( // This array can't be generated, as the arguments to require.context can't be literals :-|
[
require.context(
'../../règles/rémunération-travail/cdd',
true, /([A-Za-z\u00C0-\u017F]|\.|-|_)+.yaml$/),
require.context(
'../../règles/rémunération-travail/entités/ok',
true, /([A-Za-z\u00C0-\u017F]|\.|-|_)+.yaml$/),
require.context(
'../../règles/rémunération-travail/cotisations/ok',
true, /([A-Za-z\u00C0-\u017F]|\.|-|_)+.yaml$/),
])
let entities = R.pipe(
R.map(entityContext),
R.unnest,
)(entityContext.keys())
export default [...objectives, ...entities].filter(r => r != null)
export default rules

View File

@ -1,3 +1,4 @@
// Séparation artificielle, temporaire, entre ces deux types de règles
import rawRules from './load-rules'
import R from 'ramda'
@ -40,6 +41,9 @@ export let nameLeaf = R.pipe(
R.last
)
export let encodeRuleName = name => name.replace(/\s/g, '-')
export let decodeRuleName = name => name.replace(/\-/g, ' ')
/* Les variables peuvent être exprimées dans la formule d'une règle relativement à son propre espace de nom, pour une plus grande lisibilité. Cette fonction résoud cette ambiguité.
*/
export let disambiguateRuleReference = ({ns, name}, partialName) => {
@ -120,15 +124,30 @@ let collectNodeMissingVariables = (root, source=root, results=[]) => {
return results
}
// On peut travailler sur une somme, les objectifs sont alors les variables de cette somme.
// Ou sur une variable unique ayant une formule, elle est elle-même le seul objectif
export let getObjectives = analysedSituation => {
let formuleType = R.path(["formule", "explanation", "name"])(
analysedSituation
)
return formuleType == "somme"
? R.pluck(
"explanation",
R.path(["formule", "explanation", "explanation"])(analysedSituation)
)
: formuleType ? [analysedSituation] : null
}
export let collectMissingVariables = (groupMethod='groupByMissingVariable') => analysedSituation =>
R.pipe(
R.unless(R.is(Array), R.of),
getObjectives,
R.chain( v =>
R.pipe(
collectNodeMissingVariables,
R.flatten,
R.map(mv => [v.variableName, mv])
R.map(mv => [v.dottedName, mv])
)(v)
),
//groupBy missing variable but remove mv from value, it's now in the key

View File

@ -58,27 +58,31 @@ let fillVariableNode = (rule, situationGate) => (parseResult) => {
let
{fragments} = parseResult,
variablePartialName = fragments.join(' . '),
variableName = disambiguateRuleReference(rule, variablePartialName),
// y = console.log('variableName', variableName),
variable = findRuleByDottedName(variableName),
variableIsRule = variable.formule != null,
dottedName = disambiguateRuleReference(rule, variablePartialName),
variable = findRuleByDottedName(dottedName),
variableIsCalculable = variable.formule != null,
//TODO perf : mettre un cache sur les variables !
// On le fait pas pour l'instant car ça peut compliquer les fonctionnalités futures
// et qu'il n'y a aucun problème de perf aujourd'hui
parsedRule = variableIsRule && treatRuleRoot(
parsedRule = variableIsCalculable && treatRuleRoot(
situationGate,
variable
),
nodeValue = variableIsRule ? parsedRule.nodeValue : evaluateVariable(situationGate, variableName, variable.format),
missingVariables = variableIsRule ? [] : (nodeValue == null ? [variableName] : [])
situationValue = evaluateVariable(situationGate, dottedName, variable),
nodeValue = situationValue
!= null ? situationValue
: !variableIsCalculable
? null
: parsedRule.nodeValue,
explanation = parsedRule,
missingVariables = variableIsCalculable ? [] : (nodeValue == null ? [dottedName] : [])
return {
nodeValue,
category: 'variable',
fragments: fragments,
variableName,
name: variableName,
dottedName,
type: 'boolean | numeric',
explanation: parsedRule,
missingVariables,
@ -90,6 +94,28 @@ let fillVariableNode = (rule, situationGate) => (parseResult) => {
}
}
let buildNegatedVariable = variable => {
let nodeValue = variable.nodeValue == null ? null : !variable.nodeValue
return {
nodeValue,
category: 'mecanism',
name: 'négation',
type: 'boolean',
explanation: variable,
jsx: <Node
classes="inlineExpression negation"
value={nodeValue}
child={
<span className="nodeContent">
<span className="operator">¬</span>
{variable.jsx}
</span>
}
/>
}
}
let treat = (situationGate, rule) => rawNode => {
let reTreat = treat(situationGate, rule)
@ -104,11 +130,15 @@ let treat = (situationGate, rule) => rawNode => {
if (additionnalResults && additionnalResults.length > 0) throw "Attention ! L'expression <" + rawNode + '> ne peut être traitée de façon univoque'
if (!R.contains(parseResult.category)(['variable', 'calcExpression', 'modifiedVariable', 'comparison']))
if (!R.contains(parseResult.category)(['variable', 'calcExpression', 'modifiedVariable', 'comparison', 'negatedVariable']))
throw "Attention ! Erreur de traitement de l'expression : " + rawNode
if (parseResult.category == 'variable')
return fillVariableNode(rule, situationGate)(parseResult, rawNode)
return fillVariableNode(rule, situationGate)(parseResult)
if (parseResult.category == 'negatedVariable')
return buildNegatedVariable(
fillVariableNode(rule, situationGate)(parseResult.variable)
)
if (parseResult.category == 'calcExpression') {
let
@ -370,13 +400,13 @@ let treat = (situationGate, rule) => rawNode => {
}
if (k === 'taux') {
//TODO gérer les taux historisés
if (R.is(String)(v))
let reg = /^(\d+(\.\d+)?)\%$/
if (R.test(reg)(v))
return {
category: 'percentage',
type: 'numeric',
percentage: v,
nodeValue: transformPercentage(v),
nodeValue: R.match(reg)(v)[1]/100,
explanation: null,
jsx:
<span className="percentage" >
@ -385,7 +415,7 @@ let treat = (situationGate, rule) => rawNode => {
}
// Si c'est une liste historisée de pourcentages
// TODO revoir le test avant le bug de l'an 2100
else if ( R.all(R.test(/(19|20)\d\d(-\d\d)?(-\d\d)?/))(R.keys(v)) ) {
else if ( R.is(Array)(v) && R.all(R.test(/(19|20)\d\d(-\d\d)?(-\d\d)?/))(R.keys(v)) ) {
//TODO sélectionner la date de la simulation en cours
let lazySelection = R.first(R.values(v))
return {
@ -473,7 +503,7 @@ let treat = (situationGate, rule) => rawNode => {
name="multiplication"
value={nodeValue}
child={
<ul>
<ul className="properties">
<li key="assiette">
<span className="key">assiette: </span>
<span className="value">{assiette.jsx}</span>
@ -518,7 +548,6 @@ let treat = (situationGate, rule) => rawNode => {
composante: c.nom ? {nom: c.nom} : c.attributs
})
),
l = console.log('composantes', composantes.map(val)),
nodeValue = anyNull(composantes) ? null
: R.reduce(R.add, 0, composantes.map(val))
@ -684,6 +713,18 @@ let treat = (situationGate, rule) => rawNode => {
}
//TODO c'est moche :
export let computeRuleValue = (formuleValue, condValue) =>
condValue === undefined
? formuleValue
: formuleValue === 0
? 0
: condValue === null
? null
: condValue === true
? 0
: formuleValue
let treatRuleRoot = (situationGate, rule) => R.pipe(
R.evolve({ // -> Voilà les attributs que peut comporter, pour l'instant, une Variable.
@ -759,17 +800,7 @@ let treatRuleRoot = (situationGate, rule) => R.pipe(
let
formuleValue = r.formule.nodeValue,
condValue = R.path(['non applicable si', 'nodeValue'])(r),
nodeValue =
condValue === undefined
? formuleValue
: formuleValue === 0
? 0
: condValue === null
? null
: condValue === true
? 0
: formuleValue
nodeValue = computeRuleValue(formuleValue, condValue)
return {...r, nodeValue}
}

View File

@ -20,10 +20,18 @@ let evaluateBottomUp = situationGate => startingFragments => {
}
export let evaluateVariable = (situationGate, variableName, format) => {
/* Evalue la valeur d'une variable
en utilisant la fonction situationGate qui donne accès à la situation courante*/
export let evaluateVariable = (situationGate, variableName, rule) => {
// test rec
let value = situationGate(variableName)
return format != null ?
(value == undefined ? null : value)
: evaluateBottomUp(situationGate)(splitName(variableName))
return rule.format != null ?
value
: !rule.formule ?
// c'est une variante, eg. motifs . classique . accroissement d'activité
evaluateBottomUp(situationGate)(splitName(variableName))
: rule.formule['une possibilité'] ?
evaluateBottomUp(situationGate)(splitName(variableName))
: value
}

View File

@ -7,7 +7,7 @@ import { euro, months } from './components/conversation/formValueTypes.js'
import { EXPLAIN_VARIABLE, POINT_OUT_OBJECTIVES} from './actions'
import R from 'ramda'
import {findGroup, findRuleByDottedName, parentName, collectMissingVariables, findVariantsAndRecords} from './engine/rules'
import {findGroup, findRuleByDottedName, parentName, findVariantsAndRecords} from './engine/rules'
import {reduceSteps, generateGridQuestions, generateSimpleQuestions} from './engine/generateQuestions'

View File

@ -1,4 +1,4 @@
import { takeEvery} from 'redux-saga'
import { takeEvery} from 'redux-saga/effects'
// import { call, put} from 'redux-saga/effects'
// import Promise from 'core-js/fn/promise'

View File

@ -1,5 +1,6 @@
var webpack = require('webpack'),
autoprefixer = require('autoprefixer'),
path = require('path'),
prodEnv = process.env.NODE_ENV == 'production' // eslint-disable-line no-undef
module.exports = {
@ -15,10 +16,16 @@ module.exports = {
'./source/entry.js'
],
output: {
path: require('path').resolve('./dist/'),
path: path.resolve('./dist/'),
filename: 'bundle.js',
publicPath: '/dist/'
},
resolve: {
alias: {
Engine: path.resolve('source/engine/'),
Règles: path.resolve('règles/')
}
},
module: {
loaders: [ {
test: /\.css$/,
@ -62,7 +69,7 @@ module.exports = {
}]
},
plugins: [
new webpack.EnvironmentPlugin(['NODE_ENV']),
new webpack.EnvironmentPlugin({ NODE_ENV: 'development' }),
new webpack.NoEmitOnErrorsPlugin(),
new webpack.ProvidePlugin({
'fetch': 'imports?this=>global!exports?global.fetch!whatwg-fetch',