Merge pull request #55 from sgmap/affiner-situation

Affiner votre situation
pull/53/head
Laurent Bossavit 2017-09-28 23:26:50 +02:00 committed by GitHub
commit 2c9062855c
10 changed files with 178 additions and 142 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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([])
}
}

View File

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

View File

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