mon-entreprise/source/reducers.js

219 lines
5.7 KiB
JavaScript

import {
head,
isEmpty,
pathOr,
reject,
contains,
without,
concat,
length
} from 'ramda'
import { combineReducers } from 'redux'
import reduceReducers from 'reduce-reducers'
import { reducer as formReducer, formValueSelector } from 'redux-form'
import {
rules,
findRuleByName,
collectDefaults,
formatInputs
} from 'Engine/rules'
import { getNextSteps } from 'Engine/generateQuestions'
import computeThemeColours from 'Components/themeColours'
import {
STEP_ACTION,
START_CONVERSATION,
EXPLAIN_VARIABLE,
CHANGE_THEME_COLOUR
} from './actions'
import { analyseMany, parseAll } from 'Engine/traverse'
import ReactPiwik from 'Components/Tracker'
// assume "wraps" a given situation function with one that overrides its values with
// the given assumptions
export let assume = (evaluator, assumptions) => state => name => {
let userInput = evaluator(state)(name)
return userInput != null ? userInput : assumptions[name]
}
let nextWithoutDefaults = (
state,
analysis,
targetNames,
intermediateSituation
) => {
let reanalysis = analyseMany(state.parsedRules, targetNames)(
intermediateSituation(state)
),
nextSteps = getNextSteps(intermediateSituation(state), reanalysis)
return { currentQuestion: head(nextSteps), nextSteps }
}
export let reduceSteps = (tracker, flatRules, answerSource) => (
state,
action
) => {
// Optimization - don't parse on each analysis
if (!state.parsedRules) state.parsedRules = parseAll(flatRules)
if (
![START_CONVERSATION, STEP_ACTION, 'USER_INPUT_UPDATE'].includes(
action.type
)
)
return state
let targetNames =
action.type == START_CONVERSATION
? action.targetNames
: state.targetNames || []
let sim =
targetNames.length === 1 ? findRuleByName(flatRules, targetNames[0]) : {},
// Hard assumptions cannot be changed, they are used to specialise a simulator
// before the user sees the first question
hardAssumptions = pathOr({}, ['simulateur', 'hypothèses'], sim),
intermediateSituation = assume(answerSource, hardAssumptions),
// Most rules have default values
rulesDefaults = collectDefaults(flatRules),
situationWithDefaults = assume(intermediateSituation, rulesDefaults)
let analysis = analyseMany(state.parsedRules, targetNames)(
situationWithDefaults(state)
)
if (action.type === 'USER_INPUT_UPDATE') {
return { ...state, analysis, situationGate: situationWithDefaults(state) }
}
let nextWithDefaults = getNextSteps(situationWithDefaults(state), analysis),
assumptionsMade = !isEmpty(rulesDefaults),
done = nextWithDefaults.length == 0
let newState = {
...state,
targetNames,
analysis,
situationGate: situationWithDefaults(state),
explainedVariable: null,
done,
...(done && assumptionsMade
? // The simulation is "over" - except we can now fill in extra questions
// where the answers were previously given default reasonable assumptions
nextWithoutDefaults(state, analysis, targetNames, intermediateSituation)
: {
currentQuestion: head(nextWithDefaults),
nextSteps: nextWithDefaults
})
}
if (action.type == START_CONVERSATION) {
return {
...newState,
/* when objectives change, reject them from answered questions
Hack : 'salaire de base' is the only inversable variable, so the only
one that could be the next target AND already in the answered steps */
foldedSteps: action.fromScratch
? []
: reject(contains('salaire de base'))(state.foldedSteps)
}
}
if (action.type == STEP_ACTION && action.name == 'fold') {
tracker.push([
'trackEvent',
'answer:' + action.source,
action.step + ': ' + situationWithDefaults(state)(action.step)
])
if (!newState.currentQuestion) {
tracker.push([
'trackEvent',
'done',
'after' + length(newState.foldedSteps) + 'questions'
])
}
return {
...newState,
foldedSteps: [...state.foldedSteps, state.currentQuestion]
}
}
if (action.type == STEP_ACTION && action.name == 'unfold') {
tracker.push(['trackEvent', 'unfold', action.step])
// We are possibly "refolding" a previously open question
let previous = state.currentQuestion,
// we fold it back into foldedSteps if it had been answered
answered = previous && answerSource(state)(previous) != undefined,
foldedSteps = answered
? concat(state.foldedSteps, [previous])
: state.foldedSteps
return {
...newState,
foldedSteps: without([action.step], foldedSteps),
currentQuestion: action.step
}
}
}
function themeColours(state = computeThemeColours(), { type, colour }) {
if (type == CHANGE_THEME_COLOUR) return computeThemeColours(colour)
else return state
}
function explainedVariable(state = null, { type, variableName = null }) {
switch (type) {
case EXPLAIN_VARIABLE:
return variableName
default:
return state
}
}
function currentExample(state = null, { type, situation, name }) {
switch (type) {
case 'SET_EXAMPLE':
return name != null ? { name, situation } : null
default:
return state
}
}
export default reduceReducers(
combineReducers({
sessionId: (id = Math.floor(Math.random() * 1000000000000) + '') => id,
// this is handled by redux-form, pas touche !
form: formReducer,
/* Have forms been filled or ignored ?
false means the user is reconsidering its previous input */
foldedSteps: (steps = []) => steps,
currentQuestion: (state = null) => state,
nextSteps: (state = []) => state,
parsedRules: (state = null) => state,
analysis: (state = null) => state,
targetNames: (state = null) => state,
situationGate: (state = name => null) => state,
done: (state = null) => state,
iframe: (state = false) => state,
themeColours,
explainedVariable,
currentExample
}),
// cross-cutting concerns because here `state` is the whole state tree
reduceSteps(ReactPiwik, rules, formatInputs(rules, formValueSelector))
)