Merge pull request #2 from sgmap/toute-regle-se-simule
Toute règle se simule; exemples sur /règle; résolutions de bugspull/6/head
commit
5d7654fca8
|
@ -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",
|
||||
|
|
|
@ -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.
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -10,7 +10,6 @@
|
|||
|
||||
- Dans les faits, les CDD Senior perçoivent une indemnité d’un montant équivalent à l’indemnité 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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 l’entreprise jusqu’à 20 %. De 20 % à 20,30 %, la répartition est la suivante : 66,67 % à la charge du salarié et 33,33 % pour l’employeur.
|
||||
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 l’entreprise jusqu’à 20 %. De 20 % à 20,30 %, la répartition est la suivante : 66,67 % à la charge du salarié et 33,33 % pour l’employeur.
|
||||
|
||||
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
|
|
@ -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:
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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(
|
||||
|
|
|
@ -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})}>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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> {!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'
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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> {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>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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">
|
|
@ -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;
|
||||
}
|
|
@ -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}/>
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Reference in New Issue