diff --git a/package.json b/package.json index 30988b416..f293fc631 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/règles/rémunération-travail/entités/ok/CDD.yaml b/règles/rémunération-travail/entités/ok/CDD.yaml index e315eec4a..e9e51d14d 100644 --- a/règles/rémunération-travail/entités/ok/CDD.yaml +++ b/règles/rémunération-travail/entités/ok/CDD.yaml @@ -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 diff --git a/source/actions.js b/source/actions.js index d55637aaa..5160d932a 100644 --- a/source/actions.js +++ b/source/actions.js @@ -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' diff --git a/source/components/Results.css b/source/components/Results.css index 7c296dd3e..80ba36482 100644 --- a/source/components/Results.css +++ b/source/components/Results.css @@ -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; diff --git a/source/components/Results.js b/source/components/Results.js index bfcfecf84..2e8a85234 100644 --- a/source/components/Results.js +++ b/source/components/Results.js @@ -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 - ;
  • + ;
  • diff --git a/source/components/Simulateur.js b/source/components/Simulateur.js index 5d432475e..fc86a7a0c 100644 --- a/source/components/Simulateur.js +++ b/source/components/Simulateur.js @@ -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 (
    @@ -108,69 +108,9 @@ export default class extends React.Component {

    - : ( -
    -
    -
    - { !R.isEmpty(foldedSteps) && -
    -
    -

    Vos réponses

    - -
    - {foldedSteps - .map(step => ( - - ))} -
    - } -
    - { !R.isEmpty(unfoldedSteps) && do { - let step = R.head(unfoldedSteps) - ; - }} -
    - {unfoldedSteps.length == 0 && - } -
    - -
    -
    - )} + : } ) } } - -class Conclusion extends Component { - render() { - return ( -
    - -
    -

    - Votre simulation est terminée ! -

    -

    - N'hésitez pas à modifier vos réponses, ou cliquez sur vos résultats pour comprendre le calcul. -

    - -
    -
    - ) - } -} diff --git a/source/components/conversation/Conversation.js b/source/components/conversation/Conversation.js new file mode 100644 index 000000000..0b2bffa76 --- /dev/null +++ b/source/components/conversation/Conversation.js @@ -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 ( +
    +
    + { !R.isEmpty(foldedSteps) && +
    +
    +

    Vos réponses

    + +
    + {foldedSteps + .map(step => ( + + ))} +
    + } + { !R.isEmpty(extraSteps) && +
    +
    +

    Affiner votre situation

    +
    + {extraSteps + .map(step => ( + + ))} +
    + } +
    + { !R.isEmpty(unfoldedSteps) && do { + let step = R.head(unfoldedSteps) + ; + }} +
    + {unfoldedSteps.length == 0 && + } +
    + +
    + ) + } +} + + +class Conclusion extends Component { + render() { + return ( +
    + +
    +

    + Votre simulation est terminée ! +

    +

    + N'hésitez pas à modifier vos réponses, ou cliquez sur vos résultats pour comprendre le calcul. +

    + +
    +
    + ) + } +} diff --git a/source/components/conversation/FormDecorator.js b/source/components/conversation/FormDecorator.js index 8184028ef..e7521642b 100644 --- a/source/components/conversation/FormDecorator.js +++ b/source/components/conversation/FormDecorator.js @@ -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 (
    null} //pointOutObjectives(objectives)} - onMouseLeave={() => pointOutObjectives([])}> + > {this.state.helpVisible && this.renderHelpBox(helpText)}
    {this.renderHeader(unfolded, valueType, human, helpText, wideQuestion, subquestion)} @@ -176,8 +173,5 @@ export var FormDecorator = formType => RenderField => {helpComponent}
    } - componentWillUnmount(){ - this.props.pointOutObjectives([]) - } } diff --git a/source/components/rule/Rule.js b/source/components/rule/Rule.js index 10c2ca34e..c29d821aa 100644 --- a/source/components/rule/Rule.js +++ b/source/components/rule/Rule.js @@ -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 => ({ diff --git a/source/reducers.js b/source/reducers.js index 4189518ba..dc2683187 100644 --- a/source/reducers.js +++ b/source/reducers.js @@ -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