commit
2c9062855c
|
@ -27,6 +27,7 @@
|
|||
"react-redux": "^5.0.5",
|
||||
"react-router": "^4.1.1",
|
||||
"react-router-dom": "^4.1.1",
|
||||
"react-scroll": "^1.5.4",
|
||||
"reduce-reducers": "^0.1.2",
|
||||
"redux": "^3.6.0",
|
||||
"redux-form": "6.8.0",
|
||||
|
@ -54,8 +55,8 @@
|
|||
"chokidar": "^1.7.0",
|
||||
"core-js": "^2.4.1",
|
||||
"css-loader": "^0.28.1",
|
||||
"eslint": "^4.4.1",
|
||||
"daggy": "^1.1.0",
|
||||
"eslint": "^4.4.1",
|
||||
"eslint-plugin-react": "^7.0.1",
|
||||
"express": "^4.15.3",
|
||||
"fantasy-combinators": "0.0.1",
|
||||
|
|
|
@ -78,3 +78,9 @@
|
|||
titre: Votre obligation
|
||||
motivation: Découvrez en quelques clics le montant des 4 obligations du CDD
|
||||
# CIF, majoration chômage, indemnité de fin de contrat, indemnité compensatrice des congés payés
|
||||
hypothèses:
|
||||
contrat salarié . type de contrat: CDD
|
||||
par défaut:
|
||||
contrat salarié . CDD . événement: non
|
||||
contrat salarié . CDD . congés non pris: 0
|
||||
contrat salarié . CDD . contrat jeune vacances: non
|
||||
|
|
|
@ -30,5 +30,3 @@ export function changeThemeColour(colour) {return {type: CHANGE_THEME_COLOUR, co
|
|||
|
||||
|
||||
export const EXPLAIN_VARIABLE = 'EXPLAIN_VARIABLE'
|
||||
|
||||
export const POINT_OUT_OBJECTIVES = 'POINT_OUT_OBJECTIVES'
|
||||
|
|
|
@ -135,23 +135,6 @@
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
#results li:not(.pointedOut):hover .rule-box {
|
||||
background: #ddd;
|
||||
}
|
||||
#results li.irrelevant .rule-box {
|
||||
background: rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
|
||||
#results li.pointedOut:not(.irrelevant) .rule-name {
|
||||
color: #4A89DC;
|
||||
}
|
||||
#results li.pointedOut .rule-type {
|
||||
color: #4A89DC;
|
||||
}
|
||||
|
||||
#results li.pointedOut .rule-box {
|
||||
border-bottom: .8em solid #4A89DC;
|
||||
}
|
||||
#results li.number p {
|
||||
color: #4A89DC;
|
||||
font-weight: bold;
|
||||
|
|
|
@ -18,18 +18,16 @@ let humanFigure = decimalDigits => value => fmt(value.toFixed(decimalDigits))
|
|||
@withRouter
|
||||
@connect(
|
||||
state => ({
|
||||
pointedOutObjectives: state.pointedOutObjectives,
|
||||
analysedSituation: state.analysedSituation,
|
||||
conversationStarted: !R.isEmpty(state.form),
|
||||
conversationFirstAnswer: R.path(['form', 'conversation', 'values'])(state),
|
||||
situationGate: (name => formValueSelector('conversation')(state, name))
|
||||
situationGate: state.situationGate
|
||||
})
|
||||
)
|
||||
export default class Results extends Component {
|
||||
render() {
|
||||
let {
|
||||
analysedSituation,
|
||||
pointedOutObjectives,
|
||||
conversationStarted,
|
||||
conversationFirstAnswer: showResults,
|
||||
situationGate,
|
||||
|
@ -70,12 +68,9 @@ export default class Results extends Component {
|
|||
unsatisfied = ruleValue == null,
|
||||
nonApplicableValue = nonApplicable ? nonApplicable.nodeValue : false,
|
||||
irrelevant = nonApplicableValue === true || formuleValue == 0,
|
||||
number = nonApplicableValue == false && formuleValue != null,
|
||||
pointedOut =
|
||||
pointedOutObjectives.find(objective => objective == dottedName)
|
||||
|| R.contains(encodeRuleName(name))(location.pathname)
|
||||
number = nonApplicableValue == false && formuleValue != null
|
||||
|
||||
;<li key={name} className={classNames({unsatisfied, irrelevant, number, pointedOut})}>
|
||||
;<li key={name} className={classNames({unsatisfied, irrelevant, number})}>
|
||||
|
||||
|
||||
<Link to={"/regle/" + encodeRuleName(name)} >
|
||||
|
|
|
@ -1,31 +1,32 @@
|
|||
import R from 'ramda'
|
||||
import React, {Component} from 'react'
|
||||
import Helmet from 'react-helmet'
|
||||
import {reduxForm, formValueSelector, reset} from 'redux-form'
|
||||
import {formValueSelector, reset} from 'redux-form'
|
||||
import {connect} from 'react-redux'
|
||||
import {Redirect, Link, withRouter} from 'react-router-dom'
|
||||
import classNames from 'classnames'
|
||||
|
||||
import {START_CONVERSATION} from '../actions'
|
||||
import Aide from './Aide'
|
||||
import {createMarkdownDiv} from 'Engine/marked'
|
||||
import {rules, findRuleByName, decodeRuleName} from 'Engine/rules'
|
||||
import './conversation/conversation.css'
|
||||
import './Simulateur.css'
|
||||
import {capitalise0} from '../utils'
|
||||
import Satisfaction from './Satisfaction'
|
||||
import Conversation from './conversation/Conversation'
|
||||
|
||||
|
||||
let situationSelector = formValueSelector('conversation')
|
||||
|
||||
@withRouter
|
||||
@reduxForm({form: 'conversation', destroyOnUnmount: false})
|
||||
@connect(
|
||||
state => ({
|
||||
situation: variableName => situationSelector(state, variableName),
|
||||
foldedSteps: state.foldedSteps,
|
||||
unfoldedSteps: state.unfoldedSteps,
|
||||
extraSteps: state.extraSteps,
|
||||
themeColours: state.themeColours,
|
||||
analysedSituation: state.analysedSituation,
|
||||
situationGate: state.situationGate,
|
||||
}),
|
||||
dispatch => ({
|
||||
startConversation: rootVariable => dispatch({type: START_CONVERSATION, rootVariable}),
|
||||
|
@ -57,7 +58,7 @@ export default class extends React.Component {
|
|||
|
||||
let
|
||||
started = !this.props.match.params.intro,
|
||||
{foldedSteps, unfoldedSteps, situation} = this.props,
|
||||
{foldedSteps, extraSteps, unfoldedSteps, situation, situationGate} = this.props,
|
||||
sim = path =>
|
||||
R.path(R.unless(R.is(Array), R.of)(path))(this.rule.simulateur || {}),
|
||||
reinitalise = () => {
|
||||
|
@ -66,7 +67,6 @@ export default class extends React.Component {
|
|||
},
|
||||
title = sim('titre') || capitalise0(this.rule['titre'] || this.rule['nom'])
|
||||
|
||||
|
||||
return (
|
||||
<div id="sim" className={classNames({started})}>
|
||||
<Helmet>
|
||||
|
@ -108,69 +108,9 @@ export default class extends React.Component {
|
|||
</p>
|
||||
</div>
|
||||
</div>
|
||||
: (
|
||||
<div>
|
||||
<div id="conversation">
|
||||
<div id="questions-answers">
|
||||
{ !R.isEmpty(foldedSteps) &&
|
||||
<div id="foldedSteps">
|
||||
<div className="header" >
|
||||
<h3>Vos réponses</h3>
|
||||
<button onClick={reinitalise}>
|
||||
<i className="fa fa-trash" aria-hidden="true"></i>
|
||||
Tout effacer
|
||||
</button>
|
||||
</div>
|
||||
{foldedSteps
|
||||
.map(step => (
|
||||
<step.component
|
||||
key={step.name}
|
||||
{...step}
|
||||
step={step}
|
||||
answer={situation(step.name)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
<div id="unfoldedSteps">
|
||||
{ !R.isEmpty(unfoldedSteps) && do {
|
||||
let step = R.head(unfoldedSteps)
|
||||
;<step.component
|
||||
key={step.name}
|
||||
step={R.dissoc('component', step)}
|
||||
unfolded={true}
|
||||
answer={situation(step.name)}
|
||||
/>
|
||||
}}
|
||||
</div>
|
||||
{unfoldedSteps.length == 0 &&
|
||||
<Conclusion simu={this.name}/>}
|
||||
</div>
|
||||
<Aide />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
: <Conversation initialValues={ R.pathOr({},['simulateur','par défaut'], sim) } {...{foldedSteps, unfoldedSteps, extraSteps, reinitalise, situation, situationGate}}/>}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class Conclusion extends Component {
|
||||
render() {
|
||||
return (
|
||||
<div id="fin">
|
||||
<img src={require('../images/fin.png')} />
|
||||
<div id="fin-text">
|
||||
<p>
|
||||
Votre simulation est terminée !
|
||||
</p>
|
||||
<p>
|
||||
N'hésitez pas à modifier vos réponses, ou cliquez sur vos résultats pour comprendre le calcul.
|
||||
</p>
|
||||
<Satisfaction simu={this.props.simu}/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
import React, { Component } from 'react'
|
||||
import R from 'ramda'
|
||||
import Aide from '../Aide'
|
||||
import Satisfaction from '../Satisfaction'
|
||||
import {reduxForm} from 'redux-form'
|
||||
import Scroll from 'react-scroll'
|
||||
|
||||
@reduxForm({
|
||||
form: "conversation",
|
||||
destroyOnUnmount: false
|
||||
})
|
||||
export default class Conversation extends Component {
|
||||
render() {
|
||||
let {foldedSteps, unfoldedSteps, extraSteps, reinitalise, situation, situationGate} = this.props
|
||||
|
||||
Scroll.animateScroll.scrollToBottom()
|
||||
return (
|
||||
<div id="conversation">
|
||||
<div id="questions-answers">
|
||||
{ !R.isEmpty(foldedSteps) &&
|
||||
<div id="foldedSteps">
|
||||
<div className="header" >
|
||||
<h3>Vos réponses</h3>
|
||||
<button onClick={reinitalise}>
|
||||
<i className="fa fa-trash" aria-hidden="true"></i>
|
||||
Tout effacer
|
||||
</button>
|
||||
</div>
|
||||
{foldedSteps
|
||||
.map(step => (
|
||||
<step.component
|
||||
key={step.name}
|
||||
{...step}
|
||||
step={step}
|
||||
answer={situation(step.name)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
{ !R.isEmpty(extraSteps) &&
|
||||
<div id="foldedSteps">
|
||||
<div className="header" >
|
||||
<h3>Affiner votre situation</h3>
|
||||
</div>
|
||||
{extraSteps
|
||||
.map(step => (
|
||||
<step.component
|
||||
key={step.name}
|
||||
{...step}
|
||||
step={step}
|
||||
answer={situationGate(step.name)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
<div id="unfoldedSteps">
|
||||
{ !R.isEmpty(unfoldedSteps) && do {
|
||||
let step = R.head(unfoldedSteps)
|
||||
;<step.component
|
||||
key={step.name}
|
||||
step={R.dissoc('component', step)}
|
||||
unfolded={true}
|
||||
answer={situation(step.name)}
|
||||
/>
|
||||
}}
|
||||
</div>
|
||||
{unfoldedSteps.length == 0 &&
|
||||
<Conclusion simu={this.name}/>}
|
||||
</div>
|
||||
<Aide />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class Conclusion extends Component {
|
||||
render() {
|
||||
return (
|
||||
<div id="fin">
|
||||
<img src={require('../../images/fin.png')} />
|
||||
<div id="fin-text">
|
||||
<p>
|
||||
Votre simulation est terminée !
|
||||
</p>
|
||||
<p>
|
||||
N'hésitez pas à modifier vos réponses, ou cliquez sur vos résultats pour comprendre le calcul.
|
||||
</p>
|
||||
<Satisfaction simu={this.props.simu}/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@ import React, { Component } from 'react'
|
|||
import classNames from 'classnames'
|
||||
import { connect } from 'react-redux'
|
||||
import {Field, change} from 'redux-form'
|
||||
import {stepAction, POINT_OUT_OBJECTIVES} from '../../actions'
|
||||
import {stepAction} from '../../actions'
|
||||
import StepAnswer from './StepAnswer'
|
||||
import {capitalise0} from '../../utils'
|
||||
|
||||
|
@ -21,8 +21,7 @@ export var FormDecorator = formType => RenderField =>
|
|||
}),
|
||||
dispatch => ({
|
||||
stepAction: (name, step) => dispatch(stepAction(name, step)),
|
||||
setFormValue: (field, value) => dispatch(change('conversation', field, value)),
|
||||
pointOutObjectives: objectives => dispatch({type: POINT_OUT_OBJECTIVES, objectives})
|
||||
setFormValue: (field, value) => dispatch(change('conversation', field, value))
|
||||
})
|
||||
)
|
||||
class extends Component {
|
||||
|
@ -34,7 +33,6 @@ export var FormDecorator = formType => RenderField =>
|
|||
stepAction,
|
||||
themeColours,
|
||||
setFormValue,
|
||||
pointOutObjectives,
|
||||
/* Une étape déjà répondue est marquée 'folded'. Dans ce dernier cas, un résumé
|
||||
de la réponse est affiché */
|
||||
unfolded
|
||||
|
@ -83,8 +81,7 @@ export var FormDecorator = formType => RenderField =>
|
|||
return (
|
||||
<div
|
||||
className={classNames({step: unfolded}, formType)}
|
||||
onMouseEnter={() => null} //pointOutObjectives(objectives)}
|
||||
onMouseLeave={() => pointOutObjectives([])}>
|
||||
>
|
||||
{this.state.helpVisible && this.renderHelpBox(helpText)}
|
||||
<div style={{visibility: this.state.helpVisible ? 'hidden' : 'visible'}}>
|
||||
{this.renderHeader(unfolded, valueType, human, helpText, wideQuestion, subquestion)}
|
||||
|
@ -176,8 +173,5 @@ export var FormDecorator = formType => RenderField =>
|
|||
{helpComponent}
|
||||
</div>
|
||||
}
|
||||
componentWillUnmount(){
|
||||
this.props.pointOutObjectives([])
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -15,14 +15,9 @@ import Algorithm from './Algorithm'
|
|||
import Examples from './Examples'
|
||||
import Helmet from 'react-helmet'
|
||||
|
||||
|
||||
// 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),
|
||||
situationGate: state.situationGate,
|
||||
form: state.form
|
||||
}),
|
||||
dispatch => ({
|
||||
|
|
|
@ -4,58 +4,93 @@ import { combineReducers } from 'redux'
|
|||
import reduceReducers from 'reduce-reducers'
|
||||
import {reducer as formReducer, formValueSelector} from 'redux-form'
|
||||
|
||||
import {rules} from 'Engine/rules'
|
||||
import {buildNextSteps, generateGridQuestions, generateSimpleQuestions} from 'Engine/generateQuestions'
|
||||
import {rules, findRuleByName } from 'Engine/rules'
|
||||
import {buildNextSteps} from 'Engine/generateQuestions'
|
||||
import computeThemeColours from 'Components/themeColours'
|
||||
import { STEP_ACTION, START_CONVERSATION, EXPLAIN_VARIABLE, POINT_OUT_OBJECTIVES, CHANGE_THEME_COLOUR} from './actions'
|
||||
import { STEP_ACTION, START_CONVERSATION, EXPLAIN_VARIABLE, CHANGE_THEME_COLOUR} from './actions'
|
||||
|
||||
import {analyseTopDown} from 'Engine/traverse'
|
||||
|
||||
let situationGate = state =>
|
||||
name => formValueSelector('conversation')(state, name)
|
||||
// Our situationGate retrieves data from the "conversation" form
|
||||
let fromConversation = state => name => formValueSelector('conversation')(state, name)
|
||||
|
||||
let analyse = rootVariable => R.pipe(
|
||||
situationGate,
|
||||
// une liste des objectifs de la simulation (des 'rules' aussi nommées 'variables')
|
||||
analyseTopDown(rules, rootVariable)
|
||||
)
|
||||
// assume "wraps" a given situation function with one that overrides its values with
|
||||
// the given assumptions
|
||||
let assume = (evaluator, assumptions) => state => name => {
|
||||
let userInput = evaluator(state)(name)
|
||||
return userInput != null ? userInput : assumptions[name]
|
||||
}
|
||||
|
||||
export let reduceSteps = (state, action) => {
|
||||
|
||||
let flatRules = rules
|
||||
|
||||
if (![START_CONVERSATION, STEP_ACTION].includes(action.type))
|
||||
return state
|
||||
|
||||
let rootVariable = action.type == START_CONVERSATION ? action.rootVariable : state.analysedSituation.root.name
|
||||
|
||||
let returnObject = {
|
||||
let sim = findRuleByName(flatRules, rootVariable),
|
||||
// Hard assumptions cannot be changed, they are used to specialise a simulator
|
||||
// before the user sees the first question
|
||||
hardAssumptions = R.pathOr({},['simulateur','hypothèses'],sim),
|
||||
// Soft assumptions are revealed after the simulation ends, and can be changed
|
||||
softAssumptions = R.pathOr({},['simulateur','par défaut'],sim),
|
||||
intermediateSituation = assume(fromConversation, hardAssumptions),
|
||||
completeSituation = assume(intermediateSituation,softAssumptions)
|
||||
|
||||
let situationGate = completeSituation(state),
|
||||
analysedSituation = analyseTopDown(flatRules,rootVariable)(situationGate)
|
||||
|
||||
let newState = {
|
||||
...state,
|
||||
analysedSituation: analyse(rootVariable)(state)
|
||||
analysedSituation,
|
||||
situationGate: situationGate,
|
||||
extraSteps: []
|
||||
}
|
||||
|
||||
if (action.type == START_CONVERSATION) {
|
||||
return {
|
||||
...returnObject,
|
||||
...newState,
|
||||
foldedSteps: [],
|
||||
unfoldedSteps: buildNextSteps(situationGate(state), rules, returnObject.analysedSituation)
|
||||
unfoldedSteps: buildNextSteps(situationGate, flatRules, newState.analysedSituation)
|
||||
}
|
||||
}
|
||||
if (action.type == STEP_ACTION && action.name == 'fold') {
|
||||
let foldedSteps = [...state.foldedSteps, R.head(state.unfoldedSteps)],
|
||||
unfoldedSteps = buildNextSteps(situationGate, flatRules, newState.analysedSituation)
|
||||
|
||||
// The simulation is "over" - except we can now fill in extra questions
|
||||
// where the answers were previously given default reasonable assumptions
|
||||
if (unfoldedSteps.length == 0 && !R.isEmpty(softAssumptions)) {
|
||||
let newSituation = intermediateSituation(state),
|
||||
reanalyse = analyseTopDown(flatRules,rootVariable)(newSituation),
|
||||
extraSteps = buildNextSteps(newSituation, flatRules, reanalyse)
|
||||
|
||||
return {
|
||||
...newState,
|
||||
foldedSteps,
|
||||
extraSteps,
|
||||
unfoldedSteps: []
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...returnObject,
|
||||
foldedSteps: [...state.foldedSteps, R.head(state.unfoldedSteps)],
|
||||
unfoldedSteps: buildNextSteps(situationGate(state), rules, returnObject.analysedSituation)
|
||||
...newState,
|
||||
foldedSteps,
|
||||
unfoldedSteps
|
||||
}
|
||||
}
|
||||
if (action.type == STEP_ACTION && action.name == 'unfold') {
|
||||
let stepFinder = R.propEq('name', action.step),
|
||||
foldedSteps = R.reject(stepFinder)(state.foldedSteps)
|
||||
if (foldedSteps.length != state.foldedSteps.length - 1)
|
||||
throw 'Problème lors du dépliement d\'une réponse'
|
||||
foldedSteps = R.reject(stepFinder)(state.foldedSteps),
|
||||
extraSteps = R.reject(stepFinder)(state.extraSteps)
|
||||
|
||||
return {
|
||||
...returnObject,
|
||||
...newState,
|
||||
foldedSteps,
|
||||
unfoldedSteps: [R.find(stepFinder)(state.foldedSteps)]
|
||||
extraSteps,
|
||||
unfoldedSteps: [R.find(stepFinder)(R.concat(state.foldedSteps,state.extraSteps))]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -75,14 +110,6 @@ function explainedVariable(state = null, {type, variableName=null}) {
|
|||
}
|
||||
}
|
||||
|
||||
function pointedOutObjectives(state=[], {type, objectives}) {
|
||||
switch (type) {
|
||||
case POINT_OUT_OBJECTIVES:
|
||||
return objectives
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
export default reduceReducers(
|
||||
combineReducers({
|
||||
|
@ -93,15 +120,18 @@ export default reduceReducers(
|
|||
/* Have forms been filled or ignored ?
|
||||
false means the user is reconsidering its previous input */
|
||||
foldedSteps: (steps = []) => steps,
|
||||
extraSteps: (steps = []) => steps,
|
||||
unfoldedSteps: (steps = []) => steps,
|
||||
|
||||
analysedSituation: (state = []) => state,
|
||||
|
||||
situationGate: (state = state => name => null) => state,
|
||||
refine: (state = false) => state,
|
||||
|
||||
themeColours,
|
||||
|
||||
explainedVariable,
|
||||
explainedVariable
|
||||
|
||||
pointedOutObjectives,
|
||||
}),
|
||||
// cross-cutting concerns because here `state` is the whole state tree
|
||||
reduceSteps
|
||||
|
|
Loading…
Reference in New Issue