[moteur] Premier POC simulateur CDD
L'UI du simulateur de coût d'embauche, conversationnelle, y a été greffée. Le graphe des dépendance des variable est calaculé pour afficher les bonnes questions, et le résultat. Seule une variable est prise en compte, le CIF CDD :-Dpull/6/head
parent
79dde7e555
commit
45fcb5f118
|
@ -24,7 +24,9 @@
|
|||
"react-json-tree": "^0.10.0",
|
||||
"react-redux": "^4.4.5",
|
||||
"react-router": "^2.6.1",
|
||||
"reduce-reducers": "^0.1.2",
|
||||
"redux": "^3.5.2",
|
||||
"redux-form": "^6.4.3",
|
||||
"redux-saga": "^0.10.5",
|
||||
"reselect": "^2.5.2",
|
||||
"whatwg-fetch": "^1.0.0"
|
||||
|
|
|
@ -10,9 +10,10 @@
|
|||
- CDD poursuivi en CDI
|
||||
# Types de CDD
|
||||
- CDD type saisonnier
|
||||
- contrat jeune vacances
|
||||
- contrat aidé # voir la définition précise dans indemnité de fin de contrat
|
||||
- apprentissage
|
||||
# TODO Commentés pour le développement de la démo CDD seulement
|
||||
# - contrat jeune vacances
|
||||
# - contrat aidé # voir la définition précise dans indemnité de fin de contrat
|
||||
# - apprentissage
|
||||
|
||||
formule:
|
||||
linéaire:
|
||||
|
|
|
@ -10,6 +10,6 @@
|
|||
- Variable: assiette cotisations sociales
|
||||
description: L'assiette de la plupart des cotisations sociales pour le calcul des cotisations sociales sur le travail salarié.
|
||||
formule:
|
||||
somme: ? # donc type numérique
|
||||
somme: ? # juste pour le type numérique
|
||||
# - salaire brut
|
||||
# - primes etc.
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
// The input "conversation" is composed of "steps"
|
||||
// The state keeps track of which of them have been submitted
|
||||
// The user can also come back to one of his answers and edit it
|
||||
export const STEP_ACTION = 'STEP_ACTION'
|
||||
export function stepAction(name, newState) {
|
||||
return {type: STEP_ACTION, name, newState}
|
||||
}
|
||||
|
||||
export const START_CONVERSATION = 'START_CONVERSATION'
|
||||
|
||||
// Reset the form
|
||||
export const UNSUBMIT_ALL = 'UNSUBMIT_ALL'
|
||||
|
||||
|
||||
// Collect the input information from the forms, send them to the simulation engine API
|
||||
// then update the results in the UI
|
||||
export const SIMULATION_UPDATE_REQUEST = 'SIMULATION_UPDATE_REQUEST'
|
||||
export const SIMULATION_UPDATE_SUCCESS = 'SIMULATION_UPDATE_SUCCESS'
|
||||
export const SIMULATION_UPDATE_FAILURE = 'SIMULATION_UPDATE_FAIL'
|
||||
|
||||
// Modify the UI parts displayed to the user
|
||||
export const TOGGLE_TOP_SECTION = 'TOGGLE_TOP_SECTION'
|
||||
export const TOGGLE_ADVANCED_SECTION = 'TOGGLE_ADVANCED_SECTION'
|
||||
|
||||
// The initial request triggers the display of results based on default input information (not filled by the user)
|
||||
export const INITIAL_REQUEST = 'INITIAL_REQUEST'
|
||||
|
||||
export const CHANGE_THEME_COLOUR = 'CHANGE_THEME_COLOUR'
|
||||
export function changeThemeColour(colour) {return {type: CHANGE_THEME_COLOUR, colour}}
|
|
@ -2,20 +2,22 @@
|
|||
padding: 2em;
|
||||
}
|
||||
|
||||
|
||||
#sim section {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
#conversation {
|
||||
margin: 3em auto;
|
||||
font-size: 120%;
|
||||
line-height: normal;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
min-height: 10em;
|
||||
margin: 3em 0;
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
#questions-answers {
|
||||
background: blue;
|
||||
min-width: 50%;
|
||||
}
|
||||
|
||||
|
@ -25,6 +27,16 @@
|
|||
}
|
||||
|
||||
#results {
|
||||
width: 100%;
|
||||
width: 90%;
|
||||
background: purple;
|
||||
}
|
||||
|
||||
#results ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
#results li {
|
||||
display: inline-block;
|
||||
border: 1px solid;
|
||||
padding: .6em 2em;
|
||||
}
|
||||
|
|
|
@ -1,47 +1,55 @@
|
|||
import React, { Component } from 'react'
|
||||
import {analyseSituation, variableType} from '../traverse'
|
||||
import './CDD.css'
|
||||
import IntroCDD from './IntroCDD'
|
||||
import Results from './Results'
|
||||
import {reduxForm, formValueSelector} from 'redux-form'
|
||||
import {connect} from 'react-redux'
|
||||
import './conversation/conversation.css'
|
||||
import {START_CONVERSATION} from '../actions'
|
||||
|
||||
@connect(({form: {conversation}}) => ({conversationState: conversation && conversation.values}))
|
||||
class Aide extends Component {
|
||||
render() {
|
||||
return <section id="help">
|
||||
{JSON.stringify(this.props.conversationState)}
|
||||
</section>
|
||||
}
|
||||
}
|
||||
let situationSelector = formValueSelector('conversation')
|
||||
|
||||
@reduxForm(
|
||||
{form: 'conversation'}
|
||||
)
|
||||
@connect(state => ({
|
||||
situation: variableName => situationSelector(state, variableName),
|
||||
steps: state.steps,
|
||||
themeColours: state.themeColours,
|
||||
analysedSituation: state.analysedSituation
|
||||
}), dispatch => ({
|
||||
startConversation: () => dispatch({type: START_CONVERSATION})
|
||||
}))
|
||||
export default class CDD extends Component {
|
||||
state = {
|
||||
situation: {}
|
||||
componentDidMount() {
|
||||
this.props.startConversation()
|
||||
}
|
||||
render() {
|
||||
|
||||
let [missingVariable] = analyseSituation(this.state.situation)
|
||||
let type = variableType(missingVariable)
|
||||
let {steps} = this.props
|
||||
|
||||
let conversation = steps.map(step =>
|
||||
<step.component key={step.name} {...step}/>
|
||||
)
|
||||
|
||||
return (
|
||||
<div id="sim">
|
||||
<section id="introduction">
|
||||
<p>
|
||||
Le CDD en France est un contrat d'exception au CDI. On y a donc recours sous certaines conditions seulement. Cet outil vous aidera à respecter ces conditions et à calculer le prix mensuel de l'embauche, qui en dépend, en vous proposant une suite de questions.
|
||||
|
||||
Ici, vous avez le droit de ne pas savoir : certaines questions sont complexes, elles seront toujours accompagnées d'une aide contextuelle. Si ce n'est pas le cas, engueulez-nous* !
|
||||
</p>
|
||||
<p>
|
||||
*: écrivez à contact@contact.contact (on fera mieux après). La loi française est complexe, souvent à raison. Nous ne la changerons pas, mais pouvons la rendre plus transparente.
|
||||
</p>
|
||||
</section>
|
||||
<IntroCDD />
|
||||
<div id="conversation">
|
||||
<section id="questions-answers">
|
||||
<form onSubmit={e => e.preventDefault()}>
|
||||
<label>
|
||||
{missingVariable}
|
||||
<input type="text"
|
||||
value={this.state.value}
|
||||
onChange={e => this.setState({situation: {[missingVariable]: true}}) } />
|
||||
</label>
|
||||
<input type="submit" value="Submit" />
|
||||
</form>
|
||||
</section>
|
||||
<section id="help">
|
||||
Aide
|
||||
{conversation}
|
||||
</section>
|
||||
<Aide />
|
||||
</div>
|
||||
<section id="results">
|
||||
Résultats
|
||||
</section>
|
||||
<Results {...this.props}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
import React, {Component} from 'react'
|
||||
|
||||
export default DecoratedComponent =>
|
||||
class extends Component {
|
||||
state = {
|
||||
hover: false,
|
||||
}
|
||||
toggleHover = () =>
|
||||
this.setState({hover: !this.state.hover})
|
||||
render() {
|
||||
return <span onMouseEnter={this.toggleHover} onMouseLeave={this.toggleHover} >
|
||||
<DecoratedComponent {...this.props} hover={this.state.hover}/>
|
||||
</span>
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
import React from 'react'
|
||||
|
||||
export default () =>
|
||||
<section id="introduction">
|
||||
<p>
|
||||
Le CDD en France est un contrat d'exception au CDI. On y a donc recours sous certaines conditions seulement. Cet outil vous aidera à respecter ces conditions et à calculer le prix mensuel de l'embauche, qui en dépend, en vous proposant une suite de questions.
|
||||
|
||||
Ici, vous avez le droit de ne pas savoir : certaines questions sont complexes, elles seront toujours accompagnées d'une aide contextuelle. Si ce n'est pas le cas, engueulez-nous* !
|
||||
</p>
|
||||
<p>
|
||||
*: écrivez à contact@contact.contact (on fera mieux après). La loi française est complexe, souvent à raison. Nous ne la changerons pas, mais pouvons la rendre plus transparente.
|
||||
</p>
|
||||
</section>
|
|
@ -0,0 +1,27 @@
|
|||
import React, { Component } from 'react'
|
||||
|
||||
export default class Results extends Component {
|
||||
render() {
|
||||
let {analysedSituation} = this.props
|
||||
return (
|
||||
<section id="results">
|
||||
<h2>Cotisations</h2>
|
||||
<ul>
|
||||
{analysedSituation.map(({name, type, derived: [dependencies, value]}) =>
|
||||
<li key={name}>
|
||||
<h3>{type} {name}</h3>
|
||||
<p>
|
||||
{dependencies && dependencies.length ?
|
||||
'Répondez aux questions !'
|
||||
: value != null ?
|
||||
value + '€'
|
||||
: 'Non applicable'
|
||||
}
|
||||
</p>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
#calculable-items>li {
|
||||
margin-top: 4em;
|
||||
padding-bottom: 2em;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.item .left {
|
||||
display: inline-block;
|
||||
width: 30%;
|
||||
}
|
||||
.item .right {
|
||||
display: inline-block;
|
||||
width: 40%;
|
||||
}
|
||||
/* Display formulas */
|
||||
|
||||
.calc {
|
||||
text-align: center;
|
||||
}
|
||||
.calc h3 {
|
||||
font-weight: 300;
|
||||
font-size: 100%;
|
||||
color: #666;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.linear {
|
||||
font-size: 130%;
|
||||
}
|
||||
.linear .label {
|
||||
font-size: 70%;
|
||||
color: #333;
|
||||
color: #2980b9;
|
||||
}
|
||||
|
||||
.linear .operator {
|
||||
margin: 0 1em;
|
||||
color: #2980b9;
|
||||
font-size: 150%;
|
||||
}
|
||||
|
||||
.linear .limit {
|
||||
margin-top: 1em;
|
||||
}
|
||||
.linear .limit .label {
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
.linear .base, .calc .rate {
|
||||
display: inline-block;
|
||||
}
|
|
@ -1,106 +0,0 @@
|
|||
import React, { Component } from 'react'
|
||||
import './SelectedVariable.css'
|
||||
import TagMap from './TagMap'
|
||||
import R from 'ramda'
|
||||
|
||||
export default class SelectedVariable extends Component {
|
||||
render() {
|
||||
let {
|
||||
variable: {
|
||||
name,
|
||||
first: {
|
||||
description
|
||||
},
|
||||
tags,
|
||||
calculable
|
||||
},
|
||||
selectedTags
|
||||
} = this.props,
|
||||
|
||||
tagsList = R.pluck('tags', calculable),
|
||||
commonTags = R.tail(tagsList).reduce(
|
||||
(result, next) => R.intersection(result, R.toPairs(next)),
|
||||
R.toPairs(R.head(tagsList))
|
||||
),
|
||||
itemsWithUniqueTags = R.map(item => [item, R.fromPairs(R.difference(R.toPairs(item.tags), commonTags))], calculable)
|
||||
|
||||
|
||||
return (
|
||||
<section id="selected-variable">
|
||||
<h1>{name}</h1>
|
||||
<p>{description}</p>
|
||||
<TagMap data={commonTags} />
|
||||
{/*
|
||||
<ul>
|
||||
{Object.keys(tags)
|
||||
.filter(name => !selectedTags.find(([n]) => name == n))
|
||||
.map(name =>
|
||||
<li key={name}>
|
||||
{name + ': ' + tags[name]}
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
*/}
|
||||
<Items itemsWithUniqueTags={itemsWithUniqueTags}/>
|
||||
</section>)
|
||||
}
|
||||
}
|
||||
|
||||
class Items extends Component {
|
||||
render() {
|
||||
let {itemsWithUniqueTags} = this.props
|
||||
|
||||
return (
|
||||
<ul id="calculable-items">
|
||||
{itemsWithUniqueTags.map(([item, tags], i) =>
|
||||
<Item key={i} item={item} tags={tags}/>
|
||||
)}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let Item = ({item: {linear, marginalRateTaxScale}, tags}) =>
|
||||
<li className="item">
|
||||
<div className="left">
|
||||
<TagMap data={tags} />
|
||||
</div>
|
||||
<div className="right">
|
||||
{ linear && <Linear data={linear}/>
|
||||
|| marginalRateTaxScale && <TaxScale data={marginalRateTaxScale}/>
|
||||
}
|
||||
</div>
|
||||
</li>
|
||||
|
||||
|
||||
let Linear = ({data: {
|
||||
base,
|
||||
limit,
|
||||
historique
|
||||
}}) => <div className="calc">
|
||||
<h3>Calcul linéaire</h3>
|
||||
<div className="linear">
|
||||
<div className="base">
|
||||
<div className="label">base</div>
|
||||
<div className="value">{base}</div>
|
||||
</div>
|
||||
<span className="operator">
|
||||
✕
|
||||
</span>
|
||||
<div className="rate">
|
||||
<div className="label">Taux</div>
|
||||
<div className="value">{
|
||||
historique[(Object.keys(historique).sort()[0])]
|
||||
}</div>
|
||||
</div>
|
||||
{ limit != null && <div className="limit">
|
||||
<span className="label">dans la limite de : </span>
|
||||
<span className="value"> {limit}</span>
|
||||
</div> }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
let TaxScale = ({data}) => <div className="calc tax-scale">
|
||||
<h3>Règle de calcul: barème</h3>
|
||||
{JSON.stringify(data)}
|
||||
</div>
|
|
@ -1,13 +0,0 @@
|
|||
import React from 'react'
|
||||
import R from 'ramda'
|
||||
|
||||
let TagMap = ({data}) =>
|
||||
<ul className="tag-map">
|
||||
{R.unless(R.isArrayLike, R.toPairs)(data).map(([name, value]) => <li key={name}>
|
||||
<span>{name}</span> :
|
||||
<span className="tag-value">{value}</span>
|
||||
</li>)
|
||||
}
|
||||
</ul>
|
||||
|
||||
export default TagMap
|
|
@ -1,48 +0,0 @@
|
|||
import React from 'react'
|
||||
import TagMap from './TagMap'
|
||||
|
||||
export default class TagNavigation extends React.Component {
|
||||
render(){
|
||||
let {tagsToSelect, selectedTags, selectTag, resetTags} = this.props
|
||||
return (
|
||||
<section id="tag-navigation">
|
||||
<h2>☛ Explorez par catégorie</h2>
|
||||
<div className="content">
|
||||
{selectedTags.length > 0 &&
|
||||
<div id="selected">
|
||||
<TagMap data={selectedTags} />
|
||||
<button onClick={resetTags}>Effacer ma sélection ✕</button>
|
||||
</div>
|
||||
}
|
||||
<ul id="to-select">
|
||||
{tagsToSelect.map(tag =>
|
||||
<Tag selectTag={selectTag} key={tag.name} tag={tag} />
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class Tag extends React.Component {
|
||||
render(){
|
||||
let {tag: {name, choices, number}, selectTag} = this.props
|
||||
return (<li>
|
||||
<span className="name">
|
||||
{name}
|
||||
<span className="nb">
|
||||
({number} variable{number > 1 ? 's' : ''})
|
||||
</span>
|
||||
</span>
|
||||
<ul className="choices">
|
||||
{[...choices].map(c =>
|
||||
<li className="tag-value" key={c} onClick={() => selectTag(name, c)}>
|
||||
{c}
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</li>)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
import React from 'react'
|
||||
|
||||
import SelectedVariable from './SelectedVariable'
|
||||
import colors from './variable-colors.yaml'
|
||||
console.log(colors.length);
|
||||
import R from 'ramda'
|
||||
|
||||
function convertHex(hex,opacity){
|
||||
let r = parseInt(hex.substring(0,2), 16),
|
||||
g = parseInt(hex.substring(2,4), 16),
|
||||
b = parseInt(hex.substring(4,6), 16),
|
||||
result =`rgba(${r},${g},${b},${opacity})`
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
const Variable = ({color, name, selectVariable}) =>
|
||||
<li
|
||||
className="variable" style={{background: convertHex(color, .2)}}
|
||||
onClick={() => selectVariable(name)} >
|
||||
<h3>{name}</h3>
|
||||
</li>
|
||||
|
||||
export default class Variables extends React.Component {
|
||||
render(){
|
||||
let {variables, selectedTags, selectedVariable, selectVariable} = this.props
|
||||
console.log('variables prop in <Variables', variables)
|
||||
// let
|
||||
// variableSet =
|
||||
// variables.reduce((set, {variable}) => set.add(variable), new Set()), // get unique variable names
|
||||
// variableColors = [...variableSet].reduce((correspondance, v, i) => Object.assign(correspondance, {[v]: colors[i]}), {})
|
||||
|
||||
console.log('selectedVariable',selectedVariable)
|
||||
if (selectedVariable != null)
|
||||
return <SelectedVariable
|
||||
variable={R.find(R.propEq('name', selectedVariable))(variables)}
|
||||
selectedTags={selectedTags}
|
||||
/>
|
||||
|
||||
return <ul id="variables">
|
||||
{variables.map((v, i) =>
|
||||
<Variable key={i}
|
||||
color={colors[i]} name={v.name}
|
||||
selectVariable={selectVariable}
|
||||
/>
|
||||
)}
|
||||
</ul>
|
||||
}
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
import React, {Component} from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import Question from '../components/Forms/Question'
|
||||
import Input from '../components/Forms/Input'
|
||||
import SelectCommune from '../components/Forms/SelectCommune'
|
||||
import SelectTauxRisque from '../components/Forms/SelectTauxRisque'
|
||||
import RhetoricalQuestion from '../components/Forms/RhetoricalQuestion'
|
||||
import TextArea from '../components/Forms/TextArea'
|
||||
import Group from '../components/Group'
|
||||
import ResultATMP from '../components/ResultATMP'
|
||||
import {reduxForm, formValueSelector} from 'redux-form'
|
||||
import { percentage } from '../formValueTypes.js'
|
||||
import validate from '../conversation-validate'
|
||||
|
||||
let advancedInputSelector = formValueSelector('advancedQuestions'),
|
||||
basicInputSelector = formValueSelector('basicInput')
|
||||
|
||||
@reduxForm({
|
||||
form: 'advancedQuestions',
|
||||
validate,
|
||||
})
|
||||
@connect(state => ({
|
||||
formValue: (field, simple) => simple ? basicInputSelector(state, field): advancedInputSelector(state, field),
|
||||
steps: state.steps,
|
||||
themeColours: state.themeColours
|
||||
}))
|
||||
class Conversation extends Component {
|
||||
render() {
|
||||
let { formValue, steps, themeColours: {colour, textColour}} = this.props
|
||||
let effectifEntreprise = formValue('effectifEntreprise', 'basicInput')
|
||||
|
||||
/* C'est ici qu'est définie la suite de questions à poser. */
|
||||
return (
|
||||
<div id="conversation">
|
||||
<SelectCommune
|
||||
visible={effectifEntreprise >= 10}
|
||||
title="Commune"
|
||||
question="Quelle est la commune de l'embauche ?"
|
||||
name="codeINSEE" />
|
||||
<Input
|
||||
title="Complémentaire santé"
|
||||
question="Quel est le montant total par salarié de la complémentaire santé obligatoire de l'entreprise ?"
|
||||
visible={effectifEntreprise < 10 || steps.get('codeINSEE')}
|
||||
name="mutuelle" />
|
||||
<Group
|
||||
text="Risques professionnels"
|
||||
visible={steps.get('mutuelle')}
|
||||
foldTrigger="tauxRisque"
|
||||
valueType={percentage}
|
||||
>
|
||||
<Question
|
||||
visible={true}
|
||||
title="Taux de risque connu"
|
||||
question="Connaissez-vous votre taux de risque AT/MP ?"
|
||||
name="tauxRisqueConnu" />
|
||||
<Input
|
||||
title="Taux de risque"
|
||||
question="Entrez votre taux de risque"
|
||||
visible={formValue('tauxRisqueConnu') == 'Oui'}
|
||||
name="tauxRisque" />
|
||||
<Group name="tauxInconnu" visible={formValue('tauxRisqueConnu')== 'Non'}>
|
||||
<SelectTauxRisque
|
||||
visible={true}
|
||||
title="Code de risque sélectionné"
|
||||
question="Quelle est la catégorie de risque de votre entreprise ?"
|
||||
name="selectTauxRisque" />
|
||||
<ResultATMP
|
||||
name="resultATMP"
|
||||
selectedTauxRisque={formValue('selectTauxRisque')}
|
||||
formValue={formValue}
|
||||
{...{steps}}
|
||||
effectif={formValue('effectifEntreprise', 'basicInput')} />
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
<Input
|
||||
title="Pourcentage d'alternants"
|
||||
question="Quel est le pourcentage d'alternants dans votre entreprise ?"
|
||||
visible={effectifEntreprise >= 249 && steps.get('tauxRisque')}
|
||||
name="pourcentage_alternants" />
|
||||
|
||||
<Question
|
||||
visible={
|
||||
(effectifEntreprise < 249 && steps.get('tauxRisque'))
|
||||
|| steps.get('pourcentage_alternants')}
|
||||
title="Régime Alsace-Moselle"
|
||||
question="Le salarié est-il affilié au régime d'Alsace-Moselle ?"
|
||||
name="alsaceMoselle" />
|
||||
|
||||
<Question
|
||||
title="Pénibilité du travail"
|
||||
question="Le salarié est-il exposé à des facteurs de pénibilité au-delà des seuils d'exposition ?"
|
||||
visible={steps.get('alsaceMoselle')}
|
||||
name="penibilite" />
|
||||
|
||||
<Question
|
||||
title="Exonération Jeune Entreprise Innovante"
|
||||
question="Profitez-vous du statut Jeune Entreprise Innovante pour cette embauche ?"
|
||||
visible={steps.get('penibilite')}
|
||||
name="jei" />
|
||||
|
||||
<Question
|
||||
title="Votre avis"
|
||||
question="Votre estimation est terminée. En êtes-vous satisfait ?"
|
||||
visible={steps.get('jei')}
|
||||
name="serviceUtile" />
|
||||
<RhetoricalQuestion
|
||||
visible={formValue('serviceUtile') === ':-)'}
|
||||
name="partage"
|
||||
question={<span>
|
||||
Merci. N'hésitez pas à partager le simulateur !
|
||||
</span>
|
||||
} />
|
||||
<TextArea
|
||||
visible={formValue('serviceUtile') === ':-|'}
|
||||
name="remarque"
|
||||
title="Votre remarque"
|
||||
question={'Que pouvons-nous faire pour l\'améliorer ?'}
|
||||
/>
|
||||
</div>)
|
||||
}
|
||||
}
|
||||
|
||||
export default Conversation
|
|
@ -0,0 +1,172 @@
|
|||
import React, { Component } from 'react'
|
||||
import classNames from 'classnames'
|
||||
import { connect } from 'react-redux'
|
||||
import {Field, change} from 'redux-form'
|
||||
import {stepAction} from '../../actions'
|
||||
import IgnoreStepButton from './IgnoreStepButton'
|
||||
import StepAnswer from './StepAnswer'
|
||||
|
||||
/*
|
||||
This higher order component wraps "Form" components (e.g. Question.js), that represent user inputs,
|
||||
with a header, click actions and more goodies.
|
||||
|
||||
Read https://medium.com/@dan_abramov/mixins-are-dead-long-live-higher-order-components-94a0d2f9e750
|
||||
to understand those precious higher order components.
|
||||
*/
|
||||
|
||||
export var FormDecorator = formType => RenderField =>
|
||||
@connect( //... this helper directly to the redux state to avoid passing more props
|
||||
state => ({
|
||||
steps: state.steps,
|
||||
answers: state.form.conversation && state.form.conversation.values,
|
||||
themeColours: state.themeColours
|
||||
}),
|
||||
dispatch => ({
|
||||
stepAction: (name, newState) => dispatch(stepAction(name, newState)),
|
||||
setFormValue: (field, value) => dispatch(change('conversation', field, value))
|
||||
})
|
||||
)
|
||||
class extends Component {
|
||||
state = {
|
||||
helpVisible: false
|
||||
}
|
||||
render() {
|
||||
let {
|
||||
name,
|
||||
visible,
|
||||
steps,
|
||||
stepAction,
|
||||
possibleChoice, // should be found in the question set theoritically, but it is used for a single choice question -> the question itself is dynamic and cannot be input as code,
|
||||
themeColours,
|
||||
// formerly in conversation-steps
|
||||
valueType,
|
||||
defaultValue,
|
||||
attributes,
|
||||
choices,
|
||||
optionsURL,
|
||||
human,
|
||||
helpText
|
||||
} = this.props
|
||||
|
||||
this.step = steps.find(s => s.name == name)
|
||||
|
||||
let ignoreStep = () => {
|
||||
// Renseigne automatiquement la valeur de la saisie (en se plongeant dans les entrailles de redux-form)
|
||||
this.props.setFormValue(name, defaultValue)
|
||||
stepAction(name, 'ignored')
|
||||
}
|
||||
|
||||
|
||||
/* La saisie peut être cachée car ce n'est pas encore son tour,
|
||||
ou parce qu'elle a déjà été remplie. Dans ce dernier cas, un résumé
|
||||
de la réponse est affiché */
|
||||
let stepState = this.step.state,
|
||||
completed = stepState && stepState != 'editing',
|
||||
unfolded = !completed
|
||||
|
||||
if (!visible) return null
|
||||
|
||||
/* Nos propriétés personnalisées à envoyer au RenderField.
|
||||
Elles sont regroupées dans un objet précis pour pouvoir être enlevées des
|
||||
props passées à ce dernier, car React 15.2 n'aime pas les attributes inconnus
|
||||
des balises html, <input> dans notre cas.
|
||||
*/
|
||||
let stepProps = {
|
||||
attributes, /* Input component's html attributes */
|
||||
choices, /* Question component's radio choices */
|
||||
optionsURL, /* Select component's data source */
|
||||
possibleChoice, /* RhetoricalQuestion component's only choice :'-( */
|
||||
//TODO hack, enables redux-form/CHANGE to update the form state before the traverse functions are run
|
||||
submit: () => setTimeout(() => stepAction(name, 'filled'), 1),
|
||||
valueType
|
||||
}
|
||||
|
||||
/* There won't be any answer zone here, widen the question zone */
|
||||
let wideQuestion = formType == 'rhetorical-question' && !possibleChoice
|
||||
|
||||
return (
|
||||
<div className={classNames('step', {unfolded}, formType)} >
|
||||
{this.state.helpVisible && this.renderHelpBox(helpText)}
|
||||
<div style={{visibility: this.state.helpVisible ? 'hidden' : 'visible'}}>
|
||||
{this.renderHeader(unfolded, valueType, human, helpText, wideQuestion)}
|
||||
{unfolded &&
|
||||
<fieldset>
|
||||
{ defaultValue &&
|
||||
<IgnoreStepButton name={name} action={ignoreStep}/>
|
||||
}
|
||||
<Field
|
||||
component={RenderField}
|
||||
name={name}
|
||||
stepProps={stepProps}
|
||||
themeColours={themeColours}
|
||||
/>
|
||||
</fieldset>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
< Le titre de ma question > ----------- < (? bulle d'aide) OU résultat >
|
||||
*/
|
||||
renderHeader(unfolded, valueType, human, helpText, wideQuestion) {
|
||||
return (
|
||||
<span className="form-header" >
|
||||
{ unfolded ? this.renderQuestion(unfolded, helpText, wideQuestion) : this.renderTitleAndAnswer(valueType, human)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
renderQuestion = (unfolded, helpText, wideQuestion) =>
|
||||
<span>
|
||||
<h1
|
||||
style={{
|
||||
border: '2px solid ' + this.props.themeColours.colour, // higher border width and colour to emphasize focus
|
||||
background: 'none',
|
||||
color: this.props.themeColours.textColourOnWhite,
|
||||
maxWidth: wideQuestion ? '95%' : ''
|
||||
}}
|
||||
>{this.props.question}</h1>
|
||||
{helpText &&
|
||||
<span
|
||||
className="help-button"
|
||||
onClick={() => this.setState({helpVisible: true})}>
|
||||
aide
|
||||
</span>
|
||||
}
|
||||
</span>
|
||||
|
||||
renderTitleAndAnswer(valueType, human) {
|
||||
let {
|
||||
name,
|
||||
stepAction,
|
||||
answers,
|
||||
themeColours
|
||||
} = this.props,
|
||||
value = answers[name],
|
||||
ignored = this.step.state === 'ignored'
|
||||
|
||||
return (
|
||||
<span onClick={() => stepAction(name, 'editing')}>
|
||||
<h1>{this.props.title}</h1>
|
||||
<StepAnswer {...{value, human, valueType, ignored, themeColours}} />
|
||||
</span>)
|
||||
}
|
||||
|
||||
renderHelpBox(helpText) {
|
||||
let helpComponent =
|
||||
typeof helpText === 'string' ?
|
||||
(<p>{helpText}</p>) :
|
||||
helpText
|
||||
|
||||
return <div className="help-box">
|
||||
<a
|
||||
className="close-help"
|
||||
onClick={() => this.setState({helpVisible: false})}>
|
||||
<span className="close-text">revenir <span className="icon">✕</span></span>
|
||||
</a>
|
||||
{helpComponent}
|
||||
</div>
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
import React, {Component} from 'react'
|
||||
import GroupTitle from './GroupTitle'
|
||||
import classnames from 'classnames'
|
||||
import { connect } from 'react-redux'
|
||||
import { change} from 'redux-form'
|
||||
import {submitStep, editStep} from '../actions'
|
||||
import ReactCSSTransitionGroup from 'react-addons-css-transition-group'
|
||||
import IgnoreStepButton from './Forms/IgnoreStepButton'
|
||||
import conversationData from '../conversation-steps'
|
||||
import {formValueSelector} from 'redux-form'
|
||||
import StepAnswer from './Forms/StepAnswer'
|
||||
|
||||
|
||||
/* Groups can be used only to avoid repeating conditions for all its children,
|
||||
or to gather a set of questions that will be eventually collapsed to a final @value,
|
||||
marked with the 'explicit' class */
|
||||
@connect(state => ({
|
||||
steps: state.steps,
|
||||
formValue: field => formValueSelector('advancedQuestions')(state, field),
|
||||
themeColours: state.themeColours
|
||||
}), dispatch => ({
|
||||
editStep: name => dispatch(editStep(name)),
|
||||
submitStep: (name, ignored) => dispatch(submitStep(name, ignored)),
|
||||
setFormValue: (field, value) => dispatch(change('advancedQuestions', field, value)),
|
||||
}))
|
||||
export default class Group extends Component {
|
||||
|
||||
render() {
|
||||
let {visible, steps, foldTrigger, children, text, themeColours: {colour}} = this.props,
|
||||
folded = foldTrigger ? steps.get(foldTrigger) && steps.get(foldTrigger) != 'editing' : false
|
||||
|
||||
if (!visible) return null
|
||||
|
||||
return (
|
||||
<div className={classnames('form-group', {folded, unfolded: !folded, explicit: text})}>
|
||||
{this.renderHeader(folded)}
|
||||
<div className="group-content" style={!folded && text ? {borderLeft: '1px dashed' + colour} : {}}>
|
||||
<ReactCSSTransitionGroup
|
||||
transitionName="group-animated"
|
||||
transitionEnterTimeout={300}
|
||||
transitionLeaveTimeout={200} >
|
||||
{!folded && <ul className="group-items">
|
||||
{children.map(child =>
|
||||
<li key={child.props.name}>
|
||||
{child}
|
||||
</li>)}
|
||||
</ul>}
|
||||
</ReactCSSTransitionGroup>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderHeader(folded) {
|
||||
let {
|
||||
steps, foldTrigger, editStep, setFormValue, submitStep,
|
||||
text, valueType, formValue, themeColours
|
||||
} = this.props
|
||||
|
||||
if (!text) return null
|
||||
|
||||
let
|
||||
headerClick = () => editStep(foldTrigger),
|
||||
{defaultValue, human} = conversationData[foldTrigger],
|
||||
ignoreGroup = () => {
|
||||
setFormValue(foldTrigger, defaultValue)
|
||||
submitStep(foldTrigger, true)
|
||||
},
|
||||
value = formValue(foldTrigger),
|
||||
ignored = steps.get(name) === 'ignored'
|
||||
|
||||
return (
|
||||
<div className="header">
|
||||
<GroupTitle themeColours={themeColours} {...{text, folded}} onClick={headerClick} />
|
||||
{ !folded && <IgnoreStepButton action={ignoreGroup} /> }
|
||||
{ folded &&
|
||||
<StepAnswer {...{value, human, valueType, ignored, themeColours}} />
|
||||
}
|
||||
</div>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
import React from 'react'
|
||||
|
||||
/* Simple way for a visual stack : using two h1,
|
||||
hinting at the fact that it is a group result */
|
||||
export default ({text, onClick, folded, themeColours: {colour, textColourOnWhite}}) =>
|
||||
<div className="group-title" onClick={onClick}>
|
||||
{folded &&
|
||||
<h1 style={{
|
||||
color: 'transparent',
|
||||
position: 'absolute',
|
||||
left: '.15em',
|
||||
top: '.20em',
|
||||
border: '1px solid #aaa',
|
||||
borderTop: 'none',
|
||||
borderLeft: 'none',
|
||||
}}>
|
||||
{text}
|
||||
</h1>
|
||||
}
|
||||
<h1 style={folded ? {
|
||||
cursor: 'pointer',
|
||||
border: '1px solid #aaa',
|
||||
} : {
|
||||
border: '1px solid ' + colour,
|
||||
color: textColourOnWhite,
|
||||
}}>
|
||||
{text}
|
||||
</h1>
|
||||
</div>
|
|
@ -0,0 +1,22 @@
|
|||
import React, { Component } from 'react'
|
||||
|
||||
export default class IgnoreStepButton extends Component {
|
||||
componentDidMount() {
|
||||
// removeEventListener will need the exact same function instance
|
||||
this.boundHandleKeyDown = this.handleKeyDown.bind(this)
|
||||
|
||||
window.addEventListener('keydown', this.boundHandleKeyDown)
|
||||
}
|
||||
handleKeyDown({key}) {
|
||||
if (key !== 'Escape') return
|
||||
this.props.action()
|
||||
}
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('keydown', this.boundHandleKeyDown)
|
||||
}
|
||||
render() {
|
||||
return <a className="ignore" onClick={this.props.action}>
|
||||
passer
|
||||
</a>
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
import React, {Component} from 'react'
|
||||
import {FormDecorator} from './FormDecorator'
|
||||
import classnames from 'classnames'
|
||||
|
||||
@FormDecorator('input')
|
||||
export default class Input extends Component {
|
||||
render() {
|
||||
let {
|
||||
name,
|
||||
input,
|
||||
stepProps: {attributes, submit, valueType},
|
||||
meta: {
|
||||
touched, error, active,
|
||||
},
|
||||
themeColours
|
||||
} = this.props,
|
||||
answerSuffix = valueType.suffix,
|
||||
suffixed = answerSuffix != null,
|
||||
inputError = touched && error,
|
||||
sendButtonDisabled = !input.value || inputError
|
||||
|
||||
return (
|
||||
<span>
|
||||
<span className="answer">
|
||||
<input
|
||||
type="text" {...input}
|
||||
className={classnames({suffixed})}
|
||||
id={'step-' + name}
|
||||
{...attributes}
|
||||
style={{borderColor: themeColours.colour}}
|
||||
onKeyDown={({key}) =>
|
||||
key == 'Enter' && input.value && (
|
||||
!error ?
|
||||
submit() :
|
||||
input.onBlur() // blur will trigger the error
|
||||
)}
|
||||
/>
|
||||
{ suffixed &&
|
||||
<label className="suffix" htmlFor={'step-' + name} style={!active ? {color: '#aaa'} : {}}>
|
||||
{answerSuffix}
|
||||
</label>
|
||||
}
|
||||
<button className="send" style={{visibility: sendButtonDisabled ? 'hidden' : 'visible', color: themeColours.textColour, background: themeColours.colour}}
|
||||
onClick={() => !error ? submit() : null} >
|
||||
<span className="text">valider</span>
|
||||
<span className="icon">✓</span>
|
||||
</button>
|
||||
</span>
|
||||
{inputError && <span className="step-input-error">{error}</span>}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
import React, {Component} from 'react'
|
||||
import {FormDecorator} from './FormDecorator'
|
||||
import {answer, answered} from './userAnswerButtonStyle'
|
||||
import HoverDecorator from '../HoverDecorator'
|
||||
|
||||
@HoverDecorator
|
||||
class RadioLabel extends Component {
|
||||
|
||||
render() {
|
||||
let {choice, input, submit, hover, themeColours} = this.props,
|
||||
labelStyle =
|
||||
Object.assign(
|
||||
(choice === input.value || hover) ? answered(themeColours) : answer(themeColours),
|
||||
)
|
||||
|
||||
return (
|
||||
<label
|
||||
style={labelStyle}
|
||||
className="radio" >
|
||||
<input
|
||||
type="radio" {...input} onClick={submit}
|
||||
value={choice} checked={choice === input.value ? 'checked' : ''} />
|
||||
{choice}
|
||||
</label>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/* Ceci est une saisie de type "radio" : l'utilisateur choisit une réponse dans une liste.
|
||||
FormDecorator permet de factoriser du code partagé par les différents types de saisie,
|
||||
dont Question est un example */
|
||||
@FormDecorator('question')
|
||||
export default class Question extends Component {
|
||||
render() {
|
||||
let {
|
||||
input,
|
||||
stepProps: {submit, choices},
|
||||
themeColours
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
<span>
|
||||
{ choices.map((choice) =>
|
||||
<RadioLabel key={choice} {...{choice, input, submit, themeColours}}/>
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
import React, {Component} from 'react'
|
||||
import {FormDecorator} from './FormDecorator'
|
||||
import {answer} from './userAnswerButtonStyle'
|
||||
|
||||
@FormDecorator('rhetorical-question')
|
||||
export default class RhetoricalQuestion extends Component {
|
||||
render() {
|
||||
let {
|
||||
input,
|
||||
stepProps: {submit, possibleChoice},
|
||||
themeColours
|
||||
} = this.props
|
||||
|
||||
if (!possibleChoice) return null // No action possible, don't render an answer
|
||||
|
||||
let {text, value} = possibleChoice
|
||||
|
||||
return (
|
||||
<span className="answer">
|
||||
<label key={value} className="radio" style={answer(themeColours)}>
|
||||
<input
|
||||
type="radio" {...input} onClick={submit}
|
||||
value={value} />
|
||||
{text}
|
||||
</label>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
import React, { Component } from 'react'
|
||||
import {answered} from './userAnswerButtonStyle'
|
||||
|
||||
|
||||
export default class StepAnswer extends Component {
|
||||
render() {
|
||||
let {
|
||||
value, human, valueType, ignored, themeColours
|
||||
} = this.props,
|
||||
// Show a beautiful answer to the user, rather than the technical form value
|
||||
humanFunc = human || valueType && valueType.human || (v => v)
|
||||
|
||||
return (
|
||||
<span key="1" className="resume" style={answered(themeColours)} >
|
||||
{humanFunc(value)}
|
||||
{ignored && <span className="answer-ignored">(défaut)</span>}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
import React, {Component} from 'react'
|
||||
import {FormDecorator} from './FormDecorator'
|
||||
|
||||
@FormDecorator('text-area')
|
||||
export default class Input extends Component {
|
||||
render() {
|
||||
let {
|
||||
name,
|
||||
input,
|
||||
stepProps: {submit, attributes},
|
||||
meta: {
|
||||
touched, error,
|
||||
},
|
||||
themeColours
|
||||
} = this.props,
|
||||
inputError = touched && error,
|
||||
sendButtonDisabled = !input.value || inputError
|
||||
|
||||
return (
|
||||
<span>
|
||||
<span className="answer">
|
||||
<textarea
|
||||
{...attributes}
|
||||
{...input}
|
||||
id={'step-' + name}
|
||||
onKeyDown={({key, ctrlKey}) =>
|
||||
key == 'Enter' && ctrlKey && input.value && (
|
||||
!error ?
|
||||
submit() :
|
||||
input.onBlur() // blur will trigger the error
|
||||
)}
|
||||
/>
|
||||
<button className="send"
|
||||
style={{visibility: sendButtonDisabled ? 'hidden' : 'visible', color: themeColours.textColour, background: themeColours.colour}}
|
||||
onClick={() => !error ? submit() : null} >
|
||||
<span className="text">valider</span>
|
||||
<span className="icon">✓</span>
|
||||
</button>
|
||||
</span>
|
||||
{inputError && <span className="step-input-error">{error}</span>}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,208 @@
|
|||
import React from 'react'
|
||||
import { percentage, euro } from './formValueTypes.js'
|
||||
import {simulationDate} from './openfisca.js'
|
||||
|
||||
export default {
|
||||
|
||||
// DEFAULTS : These inputs do not exist, but the API needs them
|
||||
'defaults': {
|
||||
adapt: () => ({
|
||||
allegement_fillon_mode_recouvrement: 'anticipe_regularisation_fin_de_periode',
|
||||
allegement_cotisation_allocations_familiales_mode_recouvrement: 'anticipe_regularisation_fin_de_periode',
|
||||
contrat_de_travail_debut: simulationDate(),
|
||||
}),
|
||||
},
|
||||
|
||||
/*****************************
|
||||
BASIC INPUT FORM FIELDS */
|
||||
|
||||
/* Le type d'entreprise association 190X n'est pas défini comme une catégorie dans OpenFisca,
|
||||
mais comme un booléen */
|
||||
'typeEntreprise': {
|
||||
initial: 'entreprise',
|
||||
adapt: raw => raw === 'entreprise_est_association_non_lucrative' && {
|
||||
'entreprise_est_association_non_lucrative': true,
|
||||
},
|
||||
},
|
||||
|
||||
/* Nous simulons une embauche, donc nous incrémentons l'effectif */
|
||||
'effectifEntreprise': {
|
||||
initial: 0,
|
||||
adapt: raw => ({'effectif_entreprise': +raw + 1}),
|
||||
},
|
||||
|
||||
/* Nous voulons un ratio : on multiplie donc le nombre d'heures par semaine capté par
|
||||
(la durée légale mensuelle divisée par la durée légale hebdomadaire) */
|
||||
'heuresParSemaine': {
|
||||
initial: 30,
|
||||
adapt: raw => ({ 'heures_remunerees_volume': raw * (151.66 / 35)}),
|
||||
},
|
||||
|
||||
'typeSalaireEntré': {
|
||||
initial: 'brut',
|
||||
adapt: () => ({}),
|
||||
},
|
||||
|
||||
'salaire': {
|
||||
initial: 2300,
|
||||
adapt: (raw, value, values) => ({
|
||||
// Use other values to determine the name of this key
|
||||
[values['typeSalaireEntré'] == 'brut' ?
|
||||
'salaire_de_base' :
|
||||
'salaire_net_a_payer'
|
||||
]: value }),
|
||||
},
|
||||
|
||||
'tempsDeTravail': {
|
||||
initial: 'temps_plein',
|
||||
adapt: raw => ({'contrat_de_travail': raw}),
|
||||
},
|
||||
|
||||
'categorieSalarié': {
|
||||
initial: 'prive_non_cadre',
|
||||
adapt: raw => ({'categorie_salarie': raw}),
|
||||
},
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/****************************
|
||||
ADVANCED VIEW STEPS
|
||||
|
||||
One step is a Question, and an Answer field for the user.
|
||||
The steps, called in Conversation.js, use these data.
|
||||
The value stored in the state is the raw user input.
|
||||
The value is validated (with a "pre" normalisation step) to be able to submit the step.
|
||||
It is then 'adapted' into an object fragment. Object fragments are merged to be sent to the API.
|
||||
*/
|
||||
|
||||
'mutuelle': { // the name of the form field. Data is stored in state.form.advancedQuestions.mutuelle
|
||||
// The attributes of the HTML form field
|
||||
attributes: {
|
||||
/* We use 'text' inputs : browser behaviour with input=number
|
||||
doesn't quite work with our "update simulation on input change"... */
|
||||
inputMode: 'numeric',
|
||||
placeholder: 'votre réponse', // help for the first input
|
||||
},
|
||||
valueType: euro, /* Will give the input a suffix (€), a human representation
|
||||
that will be used in the form resume, and a validation function */
|
||||
defaultValue: '40', // The user can pass steps in the advanced view, this value is set
|
||||
helpText: // What will be displayed in the help box
|
||||
<p>
|
||||
L'employeur a l'obligation en 2016 de proposer et financer à 50% une offre
|
||||
de complémentaire santé. Son montant est libre, tant qu'elle couvre un panier légal de soins.
|
||||
<br/>
|
||||
<a href="https://www.service-public.fr/professionnels-entreprises/vosdroits/F33754" target="_blank">
|
||||
Voir les détails (service-public.fr)
|
||||
</a>
|
||||
</p>,
|
||||
adapt: (raw, validated) => ({'complementaire_sante_montant': validated}),
|
||||
},
|
||||
|
||||
'alsaceMoselle': {
|
||||
choices: [ 'Oui', 'Non' ],
|
||||
defaultValue: 'Non',
|
||||
helpText:
|
||||
<p>
|
||||
Cette affiliation est obligatoire si l'activité est exercée dans les départements du Bas-Rhin, du Haut-Rhin et de la Moselle. Elle l'est aussi dans certains autres cas, expliqués sur <a href="http://regime-local.fr/salaries/" target="_blank">cette page.</a>
|
||||
<br/>
|
||||
</p>,
|
||||
adapt: raw => ({salarie_regime_alsace_moselle: raw === 'Oui' ? 1 : 0}),
|
||||
},
|
||||
|
||||
'codeINSEE': {
|
||||
defaultValue: {codeInsee: '29019', nomCommune: 'Ville de 100 000 habitants'},
|
||||
human: v => v.nomCommune,
|
||||
helpText: <p>Quelle est la commune du lieu de travail effectif du salarié ?</p>,
|
||||
adapt: (selectObject) => ({'depcom_entreprise': selectObject && selectObject.codeInsee || ''}),
|
||||
},
|
||||
|
||||
'pourcentage_alternants': {
|
||||
attributes: {
|
||||
inputMode: 'numeric',
|
||||
},
|
||||
valueType: percentage,
|
||||
defaultValue: '0',
|
||||
helpText: <p>Ce pourcentage de l'ensemble de vos salariés nous permet de calculer le montant de la Contribution Supplémentaire à l'Apprentissage, destinée à encourager cette forme d'emploi.</p>,
|
||||
adapt: (raw, validated) => ({'ratio_alternants': validated / 100}),
|
||||
},
|
||||
|
||||
'tauxRisqueConnu': {
|
||||
choices: [ 'Oui', 'Non' ],
|
||||
helpText:
|
||||
<p>
|
||||
C'est le taux de la cotisation accidents du travail (AT) et maladies professionnelles (MP). Il est accessible sur <a href="http://www.net-entreprises.fr/html/compte-accident-travail.htm" target="_blank">net-entreprises.fr</a> ou reçu par courrier.
|
||||
</p>,
|
||||
},
|
||||
|
||||
'tauxRisque': {
|
||||
attributes: {
|
||||
inputMode: 'numeric',
|
||||
placeholder: 'Par ex. 1,1',
|
||||
},
|
||||
valueType: percentage,
|
||||
defaultValue: '1',
|
||||
adapt: validated => ({taux_accident_travail: validated / 100}),
|
||||
},
|
||||
|
||||
'selectTauxRisque': {
|
||||
fields: [ 'resume' ],
|
||||
human: v => v.text,
|
||||
optionsURL: 'https://cdn.rawgit.com/sgmap/taux-collectifs-cotisation-atmp/master/taux-2016.json',
|
||||
},
|
||||
|
||||
'penibilite': {
|
||||
choices: [ 'Plusieurs facteurs', 'Un facteur', 'Non'],
|
||||
defaultValue: 'Non',
|
||||
helpText: <p>
|
||||
Les employeurs qui exposent un salarié à un facteur de pénibilité au-delà des seuils prévus est redevable d'une cotisation de pénibilité additionnelle. Elle est doublée si les facteurs sont multiples.
|
||||
<br/>
|
||||
<a href="https://www.service-public.fr/professionnels-entreprises/vosdroits/F33777" target="_blank">
|
||||
Comprendre la cotisation pénibilité (service-public.fr)
|
||||
</a>
|
||||
</p>,
|
||||
adapt: raw => ({
|
||||
exposition_penibilite: {
|
||||
'Non': 0, 'Un facteur': 1, 'Plusieurs facteurs': 2
|
||||
}[raw]
|
||||
}),
|
||||
},
|
||||
|
||||
'jei': {
|
||||
choices: [ 'Oui', 'Non' ],
|
||||
defaultValue: 'Non',
|
||||
helpText: <p>
|
||||
Votre entreprise doit être éligible à ce statut, et votre employé doit notamment être fortement impliqué dans le projet de R&D.
|
||||
<br/>
|
||||
<a href="https://www.service-public.fr/professionnels-entreprises/vosdroits/F31188" target="_blank">
|
||||
Voir toutes les conditions (service-public.fr)
|
||||
</a>
|
||||
</p>,
|
||||
adapt: raw => ({jeune_entreprise_innovante: raw === 'Oui' ? 1 : 0}),
|
||||
},
|
||||
|
||||
'serviceUtile': {
|
||||
choices: [ ':-|', ':-)' ],
|
||||
defaultValue: null,
|
||||
helpText: <p>
|
||||
Dites-nous si ce simulateur vous a été utile
|
||||
</p>
|
||||
},
|
||||
|
||||
'partage': {},
|
||||
|
||||
'remarque': {
|
||||
attributes: {
|
||||
cols: 30,
|
||||
rows: 6,
|
||||
placeholder: 'Votre remarque, accompagnée de votre email si vous voulez un retour.',
|
||||
},
|
||||
validator: {
|
||||
test: v => v !== '',
|
||||
error: 'Entrez votre remarque',
|
||||
},
|
||||
human: v => v.substring(0,20) + '...',
|
||||
},
|
||||
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
import steps from './conversation-steps'
|
||||
|
||||
export default values =>
|
||||
values == null ? {} :
|
||||
Object.keys(values).reduce((final, next) => {
|
||||
let value = values[next],
|
||||
{valueType = {}, validator} = steps[next],
|
||||
{pre = (v => v), test, error} = Object.assign({}, validator, valueType.validator)
|
||||
if (!test) return final
|
||||
|
||||
let valid = test(pre(value))
|
||||
|
||||
return Object.assign(final, valid ? null : {[next]: error})
|
||||
}, {})
|
|
@ -0,0 +1,358 @@
|
|||
.step {
|
||||
position: relative;
|
||||
margin-bottom: 4.5em;
|
||||
}
|
||||
|
||||
|
||||
.form-group {
|
||||
position: relative;
|
||||
margin-bottom: 4.5em;
|
||||
}
|
||||
|
||||
.form-group.explicit > .group-content {
|
||||
margin-left: 1em;
|
||||
padding: .8em 0 0 1.3em;
|
||||
}
|
||||
|
||||
.form-group.folded .group-content {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.form-group .header {
|
||||
margin: 4em 0 .6em 0;
|
||||
}
|
||||
|
||||
.group-items {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Group items animation */
|
||||
|
||||
.group-animated-enter {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.group-animated-enter.group-animated-enter-active {
|
||||
opacity: 1;
|
||||
transition: opacity 300ms ease-in;
|
||||
}
|
||||
|
||||
.group-animated-leave {
|
||||
opacity: 1;
|
||||
transform: scaleY(1);
|
||||
}
|
||||
|
||||
.group-animated-leave.group-animated-leave-active {
|
||||
opacity: 0;
|
||||
transform: scaleY(0);
|
||||
transform-origin: top;
|
||||
transition: opacity 200ms ease, transform 200ms ease-out;
|
||||
}
|
||||
|
||||
/* END Group items animation */
|
||||
|
||||
/* Group ignore buttons */
|
||||
.form-group .header .ignore {
|
||||
margin-left: 3em;
|
||||
}
|
||||
|
||||
.group-title {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
.group-title h1 {
|
||||
font-size: 100%;
|
||||
font-weight: 400;
|
||||
padding: .5em .8em;
|
||||
margin: 0;
|
||||
border-radius: 1em;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
border: 1px solid rgba(0,0,0,.3);
|
||||
}
|
||||
|
||||
.step h1 {
|
||||
display: inline-block;
|
||||
max-width: 55%;
|
||||
font-size: 100%;
|
||||
font-weight: 400;
|
||||
padding: .5em 1em;
|
||||
margin: 0;
|
||||
border-radius: 1.4em;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
border: 1px solid #aaa;
|
||||
}
|
||||
/* The unfolded step is the currently focused
|
||||
question : make it visible with colors */
|
||||
.step.unfolded h1 {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* Our little help icon */
|
||||
.help-button {
|
||||
display: inline-block;
|
||||
float: right;
|
||||
margin-top: .5em;
|
||||
line-height: 1.1em;
|
||||
border-radius: 1em;
|
||||
font-size: 90%;
|
||||
color: #777;
|
||||
border: 1px solid;
|
||||
background: none;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
color: #aaa;
|
||||
text-transform: uppercase;
|
||||
font-size: 60%;
|
||||
padding: .25em .6em;
|
||||
}
|
||||
.help-button:hover {
|
||||
color: #333;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
.step fieldset {
|
||||
margin: .8em 0;
|
||||
}
|
||||
|
||||
.step label.radio,
|
||||
/* A resume of what's been answered */
|
||||
.resume {
|
||||
text-align: center;
|
||||
margin-left: 1em;
|
||||
margin-top: 2em;
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
padding: 0 .8em;
|
||||
line-height: 1.8em;
|
||||
border-radius: 1em;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.resume {
|
||||
transition: 1s display;
|
||||
}
|
||||
|
||||
.answer-ignored {
|
||||
font-size: 80%;
|
||||
opacity: .8;
|
||||
margin-left: .4em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.ignore, .ignore:visited {
|
||||
cursor: pointer;
|
||||
color: #888;
|
||||
font-weight: 200;
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid #ccc;
|
||||
font-size: 80%;
|
||||
line-height: 1.2em;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* step ignore buttons */
|
||||
fieldset > .ignore {
|
||||
position: absolute;
|
||||
bottom: 0em;
|
||||
}
|
||||
|
||||
.step input[type=radio] {
|
||||
display : none;
|
||||
}
|
||||
|
||||
.step input[type=text] {
|
||||
border-bottom: 1px solid;
|
||||
display: inline-block;
|
||||
line-height: 1.6em;
|
||||
height: 1.6em; /* IE 11 needs this */
|
||||
font-size: 100%;
|
||||
padding: 0;
|
||||
padding-right: .4em;
|
||||
width: 8em;
|
||||
text-align: right;
|
||||
padding-left: .2em;
|
||||
}
|
||||
|
||||
/* Remove IE's clear button the appears before our suffix */
|
||||
.step input::-ms-clear {
|
||||
width : 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.step input::placeholder {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.step input.suffixed {
|
||||
padding-right: 1.5em;
|
||||
}
|
||||
|
||||
.step label.suffix {
|
||||
position: relative;
|
||||
left: -1.5em;
|
||||
font-size: 85%;
|
||||
vertical-align: middle;
|
||||
transition: color .1s;
|
||||
}
|
||||
|
||||
.step input[type=text]:focus,
|
||||
.step input[type=number]:focus {
|
||||
border-color: #D0D4D8;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.help-box {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.help-box p {
|
||||
padding: 1em;
|
||||
font-size: 90%;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.close-help {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
font-size: 105%;
|
||||
}
|
||||
.close-text {
|
||||
text-transform: uppercase;
|
||||
font-size: 60%;
|
||||
}
|
||||
|
||||
.close-text .icon {
|
||||
font-size: 150%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
|
||||
.step .send {
|
||||
padding: 0 .1em 0 .5em;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
border: 1px solid;
|
||||
border-radius: .2em;
|
||||
line-height: 0em;
|
||||
}
|
||||
|
||||
.step .send .icon {
|
||||
margin-left: .3em;
|
||||
font-size: 135%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.step .send .text {
|
||||
text-transform: uppercase;
|
||||
font-size: 90%;
|
||||
line-height: 2em;
|
||||
}
|
||||
|
||||
.answer {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.step-input-error {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: -1.5em;
|
||||
font-size: .8em;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.step textarea {
|
||||
vertical-align: middle;
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
#share-link {
|
||||
color: white;
|
||||
padding: .3em .3em;
|
||||
display: inline-block;
|
||||
margin-top: .3em;
|
||||
border-radius: .25em;
|
||||
}
|
||||
#share-icon {
|
||||
font-size: 200%;
|
||||
vertical-align: middle;
|
||||
line-height: 0em;
|
||||
margin-left: .3em;
|
||||
}
|
||||
|
||||
.select-answer.commune .Select {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
|
||||
.info-zone {
|
||||
font-size: 65%;
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
color: #666;
|
||||
line-height: 1.6em;
|
||||
}
|
||||
|
||||
.input-tip {
|
||||
height: 2em;
|
||||
}
|
||||
|
||||
.input-tip p {
|
||||
margin: .1em;
|
||||
}
|
||||
|
||||
#show-advanced {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Positioning the animated elements absolutely + transition-delay will make it possible
|
||||
for the appearing element to appear without stacking up below the first one */
|
||||
#user-next-action {
|
||||
position: relative;
|
||||
height: 2em;
|
||||
margin-bottom: 3em;
|
||||
}
|
||||
|
||||
.input-tip {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
#reinit {
|
||||
position: static;
|
||||
display: block;
|
||||
text-align: center;
|
||||
font-size: 65%;
|
||||
}
|
||||
|
||||
.user-next-action-animation-enter {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.user-next-action-animation-enter.user-next-action-animation-enter-active {
|
||||
opacity: 1;
|
||||
transition: opacity 700ms;
|
||||
transition-delay: 700ms;
|
||||
}
|
||||
|
||||
.user-next-action-animation-leave {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.user-next-action-animation-leave.user-next-action-animation-leave-active {
|
||||
opacity: 0;
|
||||
transition: opacity 700ms ease;
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import { number } from './validators'
|
||||
|
||||
/*
|
||||
Here are common formats that can be attached to Form components
|
||||
*/
|
||||
|
||||
export let percentage = {
|
||||
suffix: '%',
|
||||
human: value => value + ' ' + '%',
|
||||
validator: number
|
||||
}
|
||||
|
||||
export let euro = {
|
||||
suffix: '€',
|
||||
human: value => value + ' ' + '€',
|
||||
validator: number
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
.step .select-answer {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.select-answer .Select {
|
||||
width: 90%;
|
||||
display: inline-block;
|
||||
float: right;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* TODO : should probably use tables here to center
|
||||
spans vertically */
|
||||
|
||||
.select-option {
|
||||
}
|
||||
|
||||
.select-option span {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.option-title-container {
|
||||
width: 65%;
|
||||
font-size: 85%;
|
||||
}
|
||||
|
||||
.option-secondary-container {
|
||||
width: 10%;
|
||||
min-width: 3em;
|
||||
}
|
||||
|
||||
.option-secondary-container span {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.option-label-container {
|
||||
width: 20%;
|
||||
font-size: 85%;
|
||||
background-color: #ddd;
|
||||
color: #333;
|
||||
border-radius: .25em;
|
||||
padding: .5em;
|
||||
text-align: center;
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
import React, { Component } from 'react'
|
||||
import {FormDecorator} from './FormDecorator'
|
||||
import ReactSelect from 'react-select'
|
||||
import 'react-select/dist/react-select.css'
|
||||
import './Select.css'
|
||||
|
||||
let getOptions = input =>
|
||||
input.length !== 5 ?
|
||||
Promise.resolve({}) :
|
||||
fetch(`https://apicarto.sgmap.fr/codes-postaux/communes/${input}`)
|
||||
.then(response => {
|
||||
if (!response.ok)
|
||||
return [ {nomCommune: 'Aucune commune trouvée', disabled: true} ]
|
||||
return response.json()
|
||||
})
|
||||
.then(json => ({options: json}))
|
||||
.catch(function(error) {
|
||||
console.log('Erreur dans la recherche de communes à partir du code postal', error) // eslint-disable-line no-console
|
||||
return {options: []}
|
||||
})
|
||||
|
||||
@FormDecorator('select')
|
||||
export default class SelectCommune extends Component {
|
||||
render() {
|
||||
let {
|
||||
input: {
|
||||
onChange,
|
||||
},
|
||||
stepProps: {submit}
|
||||
} = this.props,
|
||||
submitOnChange =
|
||||
option => {
|
||||
onChange(option)
|
||||
submit()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="select-answer commune">
|
||||
<ReactSelect.Async
|
||||
onChange={submitOnChange}
|
||||
labelKey="codePostal"
|
||||
optionRenderer={({nomCommune}) => nomCommune}
|
||||
valueKey="codeInsee"
|
||||
placeholder="Entrez votre code postal"
|
||||
noResultsText="Nous n'avons trouvé aucune commune"
|
||||
searchPromptText={null}
|
||||
loadingPlaceholder="Recherche en cours..."
|
||||
loadOptions={getOptions}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import React from 'react'
|
||||
|
||||
export default (option) => (
|
||||
<div className="select-option">
|
||||
<span className="option-title-container">
|
||||
{option['Nature du risque']}
|
||||
</span>
|
||||
<span className="option-secondary-container">
|
||||
<span>{option['Taux net'] + ' %'}</span>
|
||||
</span>
|
||||
<span className="option-label-container">
|
||||
{option['Catégorie']}
|
||||
</span>
|
||||
</div>
|
||||
)
|
|
@ -0,0 +1,79 @@
|
|||
import React, { Component } from 'react'
|
||||
import {FormDecorator} from './FormDecorator'
|
||||
import ReactSelect from 'react-select'
|
||||
import SelectOption from './SelectOption.js'
|
||||
import 'react-select/dist/react-select.css'
|
||||
import './Select.css'
|
||||
|
||||
|
||||
class ReactSelectWrapper extends Component {
|
||||
render() {
|
||||
let {
|
||||
value, onBlur, onChange, submit,
|
||||
options,
|
||||
submitOnChange =
|
||||
option => {
|
||||
option.text = option['Taux net'] + ' %'
|
||||
onChange(option)
|
||||
submit()
|
||||
},
|
||||
selectValue = value && value['Code risque'],
|
||||
// but ReactSelect obviously needs a unique identifier
|
||||
} = this.props
|
||||
|
||||
if (!options) return null
|
||||
|
||||
return (
|
||||
// For redux-form integration, checkout https://github.com/erikras/redux-form/issues/82#issuecomment-143164199
|
||||
<ReactSelect
|
||||
options={options}
|
||||
onChange={submitOnChange}
|
||||
labelKey="Nature du risque"
|
||||
valueKey="Code risque"
|
||||
placeholder="Tapez des mots ou déroulez la liste complète"
|
||||
optionRenderer={SelectOption}
|
||||
valueRenderer={(value) => value['Nature du risque'].substring(0, 50) + '...'}
|
||||
clearable={false}
|
||||
value={selectValue}
|
||||
onBlur={() => onBlur(value)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@FormDecorator('select')
|
||||
export default class Select extends Component {
|
||||
state = {
|
||||
options: null,
|
||||
}
|
||||
|
||||
render() {
|
||||
let {
|
||||
input,
|
||||
stepProps: {submit},
|
||||
} = this.props
|
||||
return (
|
||||
<div className="select-answer">
|
||||
<ReactSelectWrapper {...input} options={this.state.options} submit={submit} />
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
fetch(this.props.stepProps.optionsURL)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
let error = new Error(response.statusText)
|
||||
error.response = response
|
||||
throw error
|
||||
}
|
||||
return response.json()
|
||||
})
|
||||
.then(json => this.setState({options: json}))
|
||||
.catch(error =>
|
||||
console.log('Erreur dans la récupération des codes risques', error) // eslint-disable-line no-console
|
||||
)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
|
||||
export let answered = ({colour, textColour}) => ({
|
||||
background: colour,
|
||||
border: '1px solid ' + colour,
|
||||
color: textColour,
|
||||
})
|
||||
|
||||
export let answer = ({textColourOnWhite}) => ({
|
||||
color: textColourOnWhite,
|
||||
border: '1px solid ' + textColourOnWhite,
|
||||
})
|
|
@ -0,0 +1,9 @@
|
|||
// Regexps used to validate processed user inputs
|
||||
|
||||
export let number = {
|
||||
pre: v =>
|
||||
v.replace(/,/g, '.') // commas -> dots
|
||||
.replace(/\s/g, ''), // remove spaces
|
||||
test: v => /^[0-9]+(\.[0-9]+)?$/.test(v),
|
||||
error: 'Vous devez entrer un nombre',
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
/* Given a backgorund color, should you write on it in black or white ?
|
||||
Taken from http://stackoverflow.com/questions/3942878/how-to-decide-font-color-in-white-or-black-depending-on-background-color#comment61936401_3943023
|
||||
*/
|
||||
|
||||
export default function(color, simple) {
|
||||
let r = hexToR(color),
|
||||
g = hexToG(color),
|
||||
b = hexToB(color)
|
||||
|
||||
if (simple) { // The YIQ formula
|
||||
return ((r * 0.299 + g * 0.587 + b * 0.114) > 128) ?
|
||||
'#000000' : '#ffffff'
|
||||
} // else complex formula
|
||||
let
|
||||
uicolors = [ r / 255, g / 255, b / 255 ],
|
||||
c = uicolors.map(c =>
|
||||
c <= 0.03928 ?
|
||||
c / 12.92 :
|
||||
Math.pow((c + 0.055) / 1.055, 2.4)
|
||||
),
|
||||
L = 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2]
|
||||
|
||||
return (L > 0.179) ? '#000000' : '#ffffff'
|
||||
}
|
||||
|
||||
/* Hex to RGB conversion:
|
||||
* http://www.javascripter.net/faq/hextorgb.htm
|
||||
*/
|
||||
let cutHex = h => (h.charAt(0) == '#') ? h.substring(1, 7) : h,
|
||||
hexToR = h => parseInt((cutHex(h)).substring(0, 2),16),
|
||||
hexToG = h => parseInt((cutHex(h)).substring(2, 4),16),
|
||||
hexToB = h => parseInt((cutHex(h)).substring(4, 6),16)
|
|
@ -0,0 +1,27 @@
|
|||
import findContrastedTextColour from './findContrastedTextColour'
|
||||
|
||||
export default forcedThemeColour => {
|
||||
let
|
||||
scriptColour = () => {
|
||||
let script = document.currentScript || [ ...document.getElementsByTagName('script') ].pop()
|
||||
return script && script.getAttribute('couleur')
|
||||
},
|
||||
// Use the default theme colour if the host page hasn't made a choice
|
||||
defaultColour = '#4A89DC',
|
||||
colour = forcedThemeColour || scriptColour() || defaultColour,
|
||||
textColour = findContrastedTextColour(colour, true), // the 'simple' version feels better...
|
||||
inverseTextColour = textColour === '#ffffff' ? '#000' : '#fff',
|
||||
lightenTextColour = textColour => textColour === '#ffffff' ? 'rgba(255, 255, 255, .85)' : '#333',
|
||||
lighterTextColour = lightenTextColour(textColour),
|
||||
lighterInverseTextColour = lightenTextColour(inverseTextColour),
|
||||
textColourOnWhite = textColour === '#ffffff' ? colour : '#333'
|
||||
|
||||
return {
|
||||
colour,
|
||||
textColour,
|
||||
inverseTextColour,
|
||||
lighterTextColour,
|
||||
lighterInverseTextColour,
|
||||
textColourOnWhite
|
||||
}
|
||||
}
|
|
@ -1,464 +0,0 @@
|
|||
- 4C2F27
|
||||
- 0048BA
|
||||
- B0BF1A
|
||||
- 7CB9E8
|
||||
- B284BE
|
||||
- 5D8AA8
|
||||
- 00308F
|
||||
- 72A0C1
|
||||
- AF002A
|
||||
- 84DE02
|
||||
- E32636
|
||||
- C46210
|
||||
- E52B50
|
||||
- 9F2B68
|
||||
- F19CBB
|
||||
- AB274F
|
||||
- D3212D
|
||||
- 3B7A57
|
||||
- 00C4B0
|
||||
- FFBF00
|
||||
- FF7E00
|
||||
- FF033E
|
||||
- 9966CC
|
||||
- A4C639
|
||||
- F2F3F4
|
||||
- CD9575
|
||||
- 665D1E
|
||||
- 915C83
|
||||
- 841B2D
|
||||
- FAEBD7
|
||||
- 008000
|
||||
- 8DB600
|
||||
- FBCEB1
|
||||
- 00FFFF
|
||||
- 7FFFD4
|
||||
- D0FF14
|
||||
- 4B5320
|
||||
- 3B444B
|
||||
- 8F9779
|
||||
- E9D66B
|
||||
- B2BEB5
|
||||
- 87A96B
|
||||
- FF9966
|
||||
- A52A2A
|
||||
- FDEE00
|
||||
- 6E7F80
|
||||
- 568203
|
||||
- FF2052
|
||||
- C39953
|
||||
- 007FFF
|
||||
- F0FFFF
|
||||
- F0FFFF
|
||||
- DBE9F4
|
||||
- 2E5894
|
||||
- 89CFF0
|
||||
- A1CAF1
|
||||
- F4C2C2
|
||||
- FEFEFA
|
||||
- FF91AF
|
||||
- 21ABCD
|
||||
- FAE7B5
|
||||
- FFE135
|
||||
- 006A4E
|
||||
- E0218A
|
||||
- 7C0A02
|
||||
- 1DACD6
|
||||
- 848482
|
||||
- 98777B
|
||||
- BCD4E6
|
||||
- 9F8170
|
||||
- FA6E79
|
||||
- F5F5DC
|
||||
- 9C2542
|
||||
- E88E5A
|
||||
- FFE4C4
|
||||
- 3D2B1F
|
||||
- 967117
|
||||
- CAE00D
|
||||
- BFFF00
|
||||
- FE6F5E
|
||||
- BF4F51
|
||||
- 000000
|
||||
- 3D0C02
|
||||
- 54626F
|
||||
- 253529
|
||||
- 3B3C36
|
||||
- BFAFB2
|
||||
- FFEBCD
|
||||
- A57164
|
||||
- 318CE7
|
||||
- ACE5EE
|
||||
- FAF0BE
|
||||
- 0000FF
|
||||
- 1F75FE
|
||||
- 0093AF
|
||||
- 0087BD
|
||||
- 0018A8
|
||||
- 333399
|
||||
- 0247FE
|
||||
- A2A2D0
|
||||
- 00B9FB
|
||||
- 5DADEC
|
||||
- ACE5EE
|
||||
- 126180
|
||||
- 5072A7
|
||||
- 6699CC
|
||||
- 0D98BA
|
||||
- 553592
|
||||
- 8A2BE2
|
||||
- 4F86F7
|
||||
- 1C1CF0
|
||||
- DE5D83
|
||||
- 79443B
|
||||
- 0095B6
|
||||
- E3DAC9
|
||||
- DDE26A
|
||||
- CC0000
|
||||
- 006A4E
|
||||
- 873260
|
||||
- 0070FF
|
||||
- B5A642
|
||||
- CB4154
|
||||
- 1DACD6
|
||||
- 66FF00
|
||||
- BF94E4
|
||||
- D891EF
|
||||
- C32148
|
||||
- 1974D2
|
||||
- FF007F
|
||||
- 08E8DE
|
||||
- D19FE8
|
||||
- FFAA1D
|
||||
- 3399FF
|
||||
- F4BBFF
|
||||
- FF55A3
|
||||
- FB607F
|
||||
- 004225
|
||||
- CD7F32
|
||||
- 737000
|
||||
- 964B00
|
||||
- A52A2A
|
||||
- AF6E4D
|
||||
- cc9966
|
||||
- 6B4423
|
||||
- 1B4D3E
|
||||
- FFC1CC
|
||||
- E7FEFF
|
||||
- 7BB661
|
||||
- F0DC82
|
||||
- 480607
|
||||
- 800020
|
||||
- DEB887
|
||||
- A17A74
|
||||
- CC5500
|
||||
- E97451
|
||||
- 8A3324
|
||||
- BD33A4
|
||||
- 702963
|
||||
- 536872
|
||||
- 5F9EA0
|
||||
- 91A3B0
|
||||
- 006B3C
|
||||
- ED872D
|
||||
- E30022
|
||||
- FFF600
|
||||
- A67B5B
|
||||
- 4B3621
|
||||
- 1E4D2B
|
||||
- A3C1AD
|
||||
- C19A6B
|
||||
- EFBBCC
|
||||
- 78866B
|
||||
- FFFF99
|
||||
- FFEF00
|
||||
- FF0800
|
||||
- E4717A
|
||||
- 00BFFF
|
||||
- 592720
|
||||
- C41E3A
|
||||
- 00CC99
|
||||
- 960018
|
||||
- D70040
|
||||
- EB4C42
|
||||
- FF0038
|
||||
- FFA6C9
|
||||
- B31B1B
|
||||
- 56A0D3
|
||||
- ED9121
|
||||
- 00563F
|
||||
- 062A78
|
||||
- 703642
|
||||
- C95A49
|
||||
- 92A1CF
|
||||
- ACE1AF
|
||||
- 007BA7
|
||||
- 2F847C
|
||||
- B2FFFF
|
||||
- 4997D0
|
||||
- DE3163
|
||||
- EC3B83
|
||||
- 007BA7
|
||||
- 2A52BE
|
||||
- 6D9BC3
|
||||
- 007AA5
|
||||
- E03C31
|
||||
- A0785A
|
||||
- F7E7CE
|
||||
- F1DDCF
|
||||
- 36454F
|
||||
- 232B2B
|
||||
- E68FAC
|
||||
- DFFF00
|
||||
- 7FFF00
|
||||
- DE3163
|
||||
- FFB7C5
|
||||
- 954535
|
||||
- DE6FA1
|
||||
- A8516E
|
||||
- AA381E
|
||||
- 856088
|
||||
- 4AFF00
|
||||
- 7B3F00
|
||||
- D2691E
|
||||
- FFA700
|
||||
- 98817B
|
||||
- E34234
|
||||
- CD607E
|
||||
- D2691E
|
||||
- E4D00A
|
||||
- 9FA91F
|
||||
- 7F1734
|
||||
- FBCCE7
|
||||
- 0047AB
|
||||
- D2691E
|
||||
- 965A3E
|
||||
- 6F4E37
|
||||
- C4D8E2
|
||||
- F88379
|
||||
- 002E63
|
||||
- 8C92AC
|
||||
- B87333
|
||||
- DA8A67
|
||||
- AD6F69
|
||||
- CB6D51
|
||||
- 996666
|
||||
- FF3800
|
||||
- FF7F50
|
||||
- F88379
|
||||
- FF4040
|
||||
- FD7C6E
|
||||
- 893F45
|
||||
- FBEC5D
|
||||
- B31B1B
|
||||
- 6495ED
|
||||
- FFF8DC
|
||||
- 2E2D88
|
||||
- FFF8E7
|
||||
- FFBCD9
|
||||
- 81613C
|
||||
- FFFDD0
|
||||
- DC143C
|
||||
- BE0032
|
||||
- 990000
|
||||
- F5F5F5
|
||||
- 00FFFF
|
||||
- 00B7EB
|
||||
- 4E82B4
|
||||
- 28589C
|
||||
- 188BC2
|
||||
- 4682BF
|
||||
- 58427C
|
||||
- FFD300
|
||||
- F56FA1
|
||||
- FFFF31
|
||||
- F0E130
|
||||
- 00008B
|
||||
- 666699
|
||||
- 654321
|
||||
- 88654E
|
||||
- 5D3954
|
||||
- A40000
|
||||
- 08457E
|
||||
- 986960
|
||||
- CD5B45
|
||||
- 008B8B
|
||||
- 536878
|
||||
- B8860B
|
||||
- A9A9A9
|
||||
- 013220
|
||||
- 006400
|
||||
- 1F262A
|
||||
- 00416A
|
||||
- 00147E
|
||||
- 1A2421
|
||||
- BDB76B
|
||||
- 483C32
|
||||
- 734F96
|
||||
- 534B4F
|
||||
- 543D37
|
||||
- 8B008B
|
||||
- A9A9A9
|
||||
- 003366
|
||||
- 4A5D23
|
||||
- 556B2F
|
||||
- FF8C00
|
||||
- 9932CC
|
||||
- 779ECB
|
||||
- 03C03C
|
||||
- 966FD6
|
||||
- C23B22
|
||||
- E75480
|
||||
- 003399
|
||||
- 4F3A3C
|
||||
- 301934
|
||||
- 872657
|
||||
- 8B0000
|
||||
- E9967A
|
||||
- 560319
|
||||
- 8FBC8F
|
||||
- 3C1414
|
||||
- 8CBED6
|
||||
- 483D8B
|
||||
- 2F4F4F
|
||||
- 177245
|
||||
- 918151
|
||||
- FFA812
|
||||
- 483C32
|
||||
- CC4E5C
|
||||
- 00CED1
|
||||
- D1BEA8
|
||||
- 9400D3
|
||||
- 9B870C
|
||||
- 00703C
|
||||
- 555555
|
||||
- D70A53
|
||||
- 40826D
|
||||
- A9203E
|
||||
- EF3038
|
||||
- E9692C
|
||||
- DA3287
|
||||
- FAD6A5
|
||||
- B94E48
|
||||
- 704241
|
||||
- C154C1
|
||||
- 056608
|
||||
- 0E7C61
|
||||
- 004B49
|
||||
- 333366
|
||||
- F5C71A
|
||||
- 9955BB
|
||||
- CC00CC
|
||||
- 820000
|
||||
- D473D4
|
||||
- 355E3B
|
||||
- FFCBA4
|
||||
- FF1493
|
||||
- A95C68
|
||||
- 850101
|
||||
- 843F5B
|
||||
- FF9933
|
||||
- 00BFFF
|
||||
- 4A646C
|
||||
- 556B2F
|
||||
- 7E5E60
|
||||
- 66424D
|
||||
- 330066
|
||||
- BA8759
|
||||
- 1560BD
|
||||
- 2243B6
|
||||
- 669999
|
||||
- C19A6B
|
||||
- EDC9AF
|
||||
- EA3C53
|
||||
- B9F2FF
|
||||
- 696969
|
||||
- C53151
|
||||
- 9B7653
|
||||
- 1E90FF
|
||||
- D71868
|
||||
- 85BB65
|
||||
- 828E84
|
||||
- 664C28
|
||||
- 967117
|
||||
- 00009C
|
||||
- E5CCC9
|
||||
- EFDFBB
|
||||
- E1A95F
|
||||
- 555D50
|
||||
- C2B280
|
||||
- 1B1B1B
|
||||
- 614051
|
||||
- F0EAD6
|
||||
- 1034A6
|
||||
- 7DF9FF
|
||||
- FF003F
|
||||
- 00FFFF
|
||||
- 00FF00
|
||||
- 6F00FF
|
||||
- F4BBFF
|
||||
- CCFF00
|
||||
- BF00FF
|
||||
- 3F00FF
|
||||
- 8F00FF
|
||||
- FFFF33
|
||||
- 50C878
|
||||
- 6C3082
|
||||
- 1B4D3E
|
||||
- B48395
|
||||
- AB4B52
|
||||
- CC474B
|
||||
- 563C5C
|
||||
- 96C8A2
|
||||
- 44D7A8
|
||||
- C19A6B
|
||||
- 801818
|
||||
- B53389
|
||||
- DE5285
|
||||
- F400A1
|
||||
- E5AA70
|
||||
- 4D5D53
|
||||
- FDD5B1
|
||||
- 4F7942
|
||||
- FF2800
|
||||
- 6C541E
|
||||
- FF5470
|
||||
- CE2029
|
||||
- B22222
|
||||
- E25822
|
||||
- FC8EAC
|
||||
- 6B4423
|
||||
- F7E98E
|
||||
- EEDC82
|
||||
- A2006D
|
||||
- FFFAF0
|
||||
- FFBF00
|
||||
- FF1493
|
||||
- CCFF00
|
||||
- FF004F
|
||||
- 014421
|
||||
- 228B22
|
||||
- A67B5B
|
||||
- 856D4D
|
||||
- 0072BB
|
||||
- FD3F92
|
||||
- 86608E
|
||||
- 9EFD38
|
||||
- D473D4
|
||||
- FD6C9E
|
||||
- 811453
|
||||
- 4E1609
|
||||
- C72C48
|
||||
- F64A8A
|
||||
- 77B5FE
|
||||
- 8806CE
|
||||
- AC1E44
|
||||
- A6E7FF
|
||||
- E936A7
|
||||
- FF00FF
|
||||
- C154C1
|
||||
- FF77FF
|
||||
- CC397B
|
||||
- C74375
|
||||
- E48400
|
||||
- CC6666
|
|
@ -5,6 +5,7 @@ import DevTools from '../DevTools'
|
|||
import routes from '../routes'
|
||||
import {Router, browserHistory} from 'react-router'
|
||||
|
||||
|
||||
export default class App extends Component {
|
||||
render() {
|
||||
const { store } = this.props
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React, { Component } from 'react'
|
||||
import './Layout.css'
|
||||
import './reset.css'
|
||||
import {Link} from 'react-router'
|
||||
|
||||
export default class Layout extends Component {
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
Unify browser styles and reset opinionated defaults;
|
||||
Reset style decisions that would break (rather than just customize)
|
||||
the widget's form layout.
|
||||
*/
|
||||
|
||||
[hidden],
|
||||
.js-only {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Reset fieldset style */
|
||||
fieldset {
|
||||
border: 0;
|
||||
padding: 0;
|
||||
padding-top: .01em;
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Remove spinner controls from Firefox */
|
||||
input[type="number"] {
|
||||
appearance: textfield;
|
||||
}
|
||||
|
||||
select {
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
input {
|
||||
line-height: normal;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 100%;
|
||||
font-weight: normal;
|
||||
}
|
|
@ -2,11 +2,11 @@ import rules from './load-rules'
|
|||
import possibleVariableTypes from './possibleVariableTypes.yaml'
|
||||
|
||||
|
||||
export let findRuleByName = name =>
|
||||
export let findRuleByName = search =>
|
||||
rules
|
||||
.map(extractRuleTypeAndName)
|
||||
.find( ([, n]) =>
|
||||
n === name
|
||||
.find( ({name}) =>
|
||||
name === search
|
||||
)
|
||||
|
||||
export let searchRules = searchInput =>
|
||||
|
@ -19,7 +19,7 @@ export let searchRules = searchInput =>
|
|||
|
||||
export let extractRuleTypeAndName = rule => {
|
||||
let type = possibleVariableTypes.find(t => rule[t])
|
||||
return [type, rule[type], rule]
|
||||
return {type, name: rule[type], rule}
|
||||
}
|
||||
|
||||
export let hasKnownRuleType = rule => rule && extractRuleTypeAndName(rule)[0]
|
||||
export let hasKnownRuleType = rule => rule && extractRuleTypeAndName(rule).type
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
import { combineReducers } from 'redux'
|
||||
import {reducer as formReducer} from 'redux-form'
|
||||
|
||||
import { SUBMIT_STEP, EDIT_STEP, UNSUBMIT_ALL} from './actions'
|
||||
import {
|
||||
SIMULATION_UPDATE_REQUEST, SIMULATION_UPDATE_SUCCESS,
|
||||
TOGGLE_TOP_SECTION, TOGGLE_ADVANCED_SECTION,
|
||||
} from './actions'
|
||||
import computeThemeColours from './themeColours'
|
||||
import {change} from 'redux-form'
|
||||
|
||||
function steps(state = new Map(), {type, name, ignored}) {
|
||||
switch (type) {
|
||||
case SUBMIT_STEP:
|
||||
return new Map([ ...state ]).set(
|
||||
name,
|
||||
ignored ? 'ignored' : 'filled'
|
||||
)
|
||||
case EDIT_STEP:
|
||||
return new Map([ ...state ]).set(
|
||||
name,
|
||||
'editing'
|
||||
)
|
||||
case UNSUBMIT_ALL:
|
||||
return new Map()
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
function pending(state = false, action) {
|
||||
switch (action.type) {
|
||||
case SIMULATION_UPDATE_REQUEST:
|
||||
return true
|
||||
case SIMULATION_UPDATE_SUCCESS:
|
||||
return false
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
function results(state = {}, {type, results}) {
|
||||
switch (type) {
|
||||
case SIMULATION_UPDATE_SUCCESS:
|
||||
return results.values
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
function activeSections(state = {top: 'input', advanced: false}, {type}) {
|
||||
switch (type) {
|
||||
// What is the active top section, input or details ?
|
||||
case TOGGLE_TOP_SECTION:
|
||||
return Object.assign({}, state, {top: state.top === 'input' ? 'details' : 'input' })
|
||||
// Is the advanced input active ?
|
||||
case TOGGLE_ADVANCED_SECTION:
|
||||
return Object.assign({}, state, {advanced: !state.advanced})
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
function inputChanged(state = false, {type}) {
|
||||
switch(type) {
|
||||
case change().type:
|
||||
return true
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
function themeColours(state = computeThemeColours(), {type, colour}) {
|
||||
if (type == 'CHANGE_THEME_COLOUR')
|
||||
return computeThemeColours(colour)
|
||||
else return state
|
||||
}
|
||||
|
||||
export default combineReducers({
|
||||
// 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 */
|
||||
steps,
|
||||
|
||||
// Is an (advanced simulation) request pending ?
|
||||
pending,
|
||||
results,
|
||||
|
||||
activeSections,
|
||||
|
||||
// Has the user edited one form field ?
|
||||
inputChanged,
|
||||
|
||||
themeColours,
|
||||
})
|
|
@ -1,9 +1,110 @@
|
|||
|
||||
import React from 'react'
|
||||
import { combineReducers } from 'redux'
|
||||
import { } from './actions'
|
||||
import reduceReducers from 'reduce-reducers'
|
||||
import {reducer as formReducer, formValueSelector} from 'redux-form'
|
||||
import {analyseSituation, variableType} from './traverse'
|
||||
import { euro } from './components/conversation/formValueTypes.js'
|
||||
|
||||
import Question from './components/conversation/Question'
|
||||
import Input from './components/conversation/Input'
|
||||
import RhetoricalQuestion from './components/conversation/RhetoricalQuestion'
|
||||
|
||||
import { STEP_ACTION, UNSUBMIT_ALL, START_CONVERSATION} from './actions'
|
||||
import R from 'ramda'
|
||||
|
||||
import computeThemeColours from './components/themeColours'
|
||||
|
||||
function steps(steps = [], {type}) {
|
||||
switch (type) {
|
||||
case UNSUBMIT_ALL:
|
||||
return []
|
||||
default:
|
||||
return steps
|
||||
}
|
||||
}
|
||||
|
||||
function themeColours(state = computeThemeColours(), {type, colour}) {
|
||||
if (type == 'CHANGE_THEME_COLOUR')
|
||||
return computeThemeColours(colour)
|
||||
else return state
|
||||
}
|
||||
|
||||
export default reduceReducers(
|
||||
combineReducers({
|
||||
// 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 */
|
||||
steps,
|
||||
|
||||
analysedSituation: (state = []) => state,
|
||||
|
||||
themeColours
|
||||
}),
|
||||
// cross-cutting concerns because here `state` is the whole state tree
|
||||
(state, action) => {
|
||||
if (action.type == STEP_ACTION || action.type == START_CONVERSATION) {
|
||||
let {newState, name} = action
|
||||
|
||||
// une étape vient d'être validée : on va changer son état
|
||||
let newSteps = R.pipe(
|
||||
R.map(step => step.name == name ? {...step, state: newState} : step),
|
||||
R.reject(R.whereEq({theEnd: true}))
|
||||
)(state.steps)
|
||||
|
||||
// on calcule la prochaine étape, à ajouter sur la pile
|
||||
let analysedSituation = analyseSituation(name => formValueSelector('conversation')(state, name)),
|
||||
[firstMissingVariable] = R.pipe(
|
||||
R.map(({derived: [missingVariables]}) => missingVariables),
|
||||
R.flatten
|
||||
)(analysedSituation),
|
||||
|
||||
type = variableType(firstMissingVariable),
|
||||
|
||||
stepData = Object.assign({
|
||||
name: firstMissingVariable,
|
||||
state: null,
|
||||
dependecyOfVariable: null, //TODO
|
||||
title: firstMissingVariable,
|
||||
question: firstMissingVariable,
|
||||
visible: true,
|
||||
helpText: <p>
|
||||
The impossible is possible, at Zombo.com
|
||||
<br/>
|
||||
<a href="https://www.service-public.fr/professionnels-entreprises/vosdroits/F33777" target="_blank">
|
||||
Comprendre comment tout ce bordel se ficelle
|
||||
</a>
|
||||
</p>
|
||||
}, type == 'boolean' ? {
|
||||
component: Question,
|
||||
choices: ['non', 'oui'],
|
||||
defaultValue: 'Non'
|
||||
}: type == 'numeric' ? {
|
||||
component: Input,
|
||||
defaultValue: 0,
|
||||
valueType: euro,
|
||||
attributes: {
|
||||
/* We use 'text' inputs : browser behaviour with input=number
|
||||
doesn't quite work with our "update simulation on input change"... */
|
||||
inputMode: 'numeric',
|
||||
placeholder: 'votre réponse'
|
||||
}
|
||||
} : firstMissingVariable == undefined ? {
|
||||
theEnd: true,
|
||||
component: RhetoricalQuestion,
|
||||
question: <span>
|
||||
{'Merci. N\'hésitez pas à partager le simulateur !'}
|
||||
</span>,
|
||||
helpText: null
|
||||
}: {})
|
||||
|
||||
|
||||
return {...state, steps: [...newSteps, stepData], analysedSituation}
|
||||
// ... do stuff
|
||||
} else {
|
||||
return state
|
||||
}
|
||||
|
||||
export default combineReducers({
|
||||
yo: (state = null) => state
|
||||
})
|
||||
}
|
||||
)
|
||||
|
|
|
@ -5,7 +5,6 @@ import Promise from 'core-js/fn/promise'
|
|||
// Nothing happening here !
|
||||
|
||||
function* handleSubmitStep() {
|
||||
console.log('salut')
|
||||
}
|
||||
|
||||
function* watchSteps() {
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
import { createSelector } from 'reselect'
|
||||
|
||||
import React from 'react'
|
|
@ -1,7 +1,7 @@
|
|||
import removeDiacritics from './utils/remove-diacritics'
|
||||
import R from 'ramda'
|
||||
import rules from './load-rules'
|
||||
import initialSituation from './initialSituation'
|
||||
// import initialSituation from './initialSituation'
|
||||
import {findRuleByName, extractRuleTypeAndName} from './model'
|
||||
|
||||
/*
|
||||
|
@ -53,126 +53,147 @@ let recognizeExpression = rawValue => {
|
|||
match = expressionTests['negatedVariable'](value)
|
||||
if (match) {
|
||||
let [, variableName] = match
|
||||
return [variableName, `!${variableName}`]
|
||||
// return [variableName, `!${variableName}`]
|
||||
return [variableName, situation => situation(variableName) == 'non']
|
||||
|
||||
}
|
||||
|
||||
match = expressionTests['variableComparedToNumber'](value)
|
||||
if (match) {
|
||||
let [, variableName, symbol, number] = match
|
||||
return [variableName, `situation[${variableName}] ${symbol} ${number}`]
|
||||
return [variableName, situation => eval(`situation("${variableName}") ${symbol} ${number}`)] //eslint-disable-line no-unused-vars
|
||||
}
|
||||
|
||||
match = expressionTests['numberComparedToVariable'](value)
|
||||
if (match) {
|
||||
let [, number, symbol, variableName] = match
|
||||
return [variableName, `${number} ${symbol} situation[${variableName}]`]
|
||||
return [variableName, situation => eval(`${number} ${symbol} situation("${variableName}")`)] //eslint-disable-line no-unused-vars
|
||||
}
|
||||
|
||||
match = expressionTests['variableEqualsNumber'](value)
|
||||
if (match) {
|
||||
let [, variableName, number] = match
|
||||
return [variableName, `situation[${variableName}] == ${number}`]
|
||||
return [variableName, situation => situation(variableName) == number]
|
||||
}
|
||||
|
||||
|
||||
match = expressionTests['variable'](value)
|
||||
if (match) {
|
||||
let [variableName] = match
|
||||
return [variableName, `situation["${variableName}"]`]
|
||||
return [variableName, situation => situation(variableName) == 'oui']
|
||||
}
|
||||
}
|
||||
|
||||
let knownVariable = (situation, variableName) => (typeof situation(variableName) !== 'undefined')
|
||||
|
||||
let deriveRule = situation => R.pipe(
|
||||
R.toPairs,
|
||||
// Reduce to [variables needed to compute that variable, computed variable value]
|
||||
R.reduce(([variableNames, result], [key, value]) => {
|
||||
if (key === 'concerne'){
|
||||
let [variableName, evaluation] = recognizeExpression(value)
|
||||
// Si cette variable a été renseignée
|
||||
if (knownVariable(situation, variableName)){
|
||||
// Si l'expression n'est pas vraie...
|
||||
if (!evaluation(situation)){
|
||||
// On court-circuite toute la variable, et on n'a besoin d'aucune information !
|
||||
return R.reduced([[]])
|
||||
} else {
|
||||
// Sinon, on continue
|
||||
return [variableNames]
|
||||
}
|
||||
// sinon on demande la valeur de cette variable
|
||||
} else return [[...variableNames, variableName]]
|
||||
}
|
||||
|
||||
if (key === 'non applicable si') {
|
||||
let conditions = value['l\'une de ces conditions']
|
||||
let [subVariableNames, reduced] = R.reduce(([variableNames], expression) => {
|
||||
let [variableName, evaluation] = recognizeExpression(expression)
|
||||
|
||||
if (knownVariable(situation, variableName)){
|
||||
if (evaluation(situation)) {
|
||||
return R.reduced([[], true])
|
||||
} else {
|
||||
return [variableNames]
|
||||
}
|
||||
}
|
||||
return [[...variableNames, variableName]]
|
||||
}, [[], null])(conditions)
|
||||
if (reduced) return R.reduced([[]])
|
||||
else return [variableNames.concat(subVariableNames)]
|
||||
}
|
||||
|
||||
if (key === 'formule'){
|
||||
if (value['linéaire']){
|
||||
let {assiette, taux} = value['linéaire']
|
||||
|
||||
// A propos de l'assiette
|
||||
let assietteVariableName = removeDiacritics(assiette),
|
||||
assietteValue = situation(assietteVariableName),
|
||||
unknownAssiette = assietteValue == undefined
|
||||
|
||||
if (unknownAssiette){
|
||||
return [[...variableNames, assietteVariableName]]
|
||||
} else {
|
||||
if (variableNames.length > 0) {
|
||||
return [variableNames]
|
||||
}
|
||||
}
|
||||
|
||||
// Arrivés là, cette formule devrait être calculable !
|
||||
|
||||
// A propos du taux
|
||||
if (typeof taux !== 'string' && typeof taux !== 'number'){
|
||||
throw 'Oups, pas de taux compliqués s\'il-vous-plaît'
|
||||
}
|
||||
let tauxValue = taux.indexOf('%') > -1 ?
|
||||
+taux.replace('%', '')/100 :
|
||||
+taux
|
||||
|
||||
return R.reduced([null, assietteValue * tauxValue])
|
||||
}
|
||||
}
|
||||
|
||||
return [variableNames]
|
||||
}, [[], null])
|
||||
)
|
||||
|
||||
|
||||
|
||||
let analyseVariable = situation =>
|
||||
R.pipe(
|
||||
R.toPairs,
|
||||
R.reduce(([variableNames, result], [key, value]) => {
|
||||
if (key === 'concerne'){
|
||||
let [variableName, evaluation] = recognizeExpression(value)
|
||||
if (typeof situation[variableName] !== 'undefined'){
|
||||
if (!eval(evaluation)){
|
||||
return R.reduced([[]])
|
||||
} else {
|
||||
return [variableNames, null]
|
||||
}
|
||||
}
|
||||
return [[...variableNames, variableName]]
|
||||
}
|
||||
|
||||
if (key === 'non applicable si') {
|
||||
let conditions = value['l\'une de ces conditions']
|
||||
let subVariableNames = R.reduce((variableNames, expression) => {
|
||||
let [variableName, evaluation] = recognizeExpression(expression)
|
||||
// console.log('evaluation', variableName, evaluation)
|
||||
if (typeof situation[variableName] !== 'undefined'){
|
||||
if (eval(evaluation)){
|
||||
return R.reduced([])
|
||||
} else {
|
||||
return variableNames
|
||||
}
|
||||
}
|
||||
return [...variableNames, variableName]
|
||||
}, [])(conditions)
|
||||
console.log('subVariableNames', subVariableNames)
|
||||
return [variableNames.concat(subVariableNames)]
|
||||
}
|
||||
|
||||
if (key === 'formule'){
|
||||
if (value['linéaire']){
|
||||
let {assiette, taux} = value['linéaire']
|
||||
|
||||
// A propos de l'assiette
|
||||
let assietteVariableName = removeDiacritics(assiette),
|
||||
assietteValue = situation[assietteVariableName],
|
||||
unknownAssiette = typeof assietteValue !== 'number'
|
||||
|
||||
if (unknownAssiette){
|
||||
return [[...variableNames, assietteVariableName]]
|
||||
} else {
|
||||
if (variableNames.length > 0) {
|
||||
return [variableNames]
|
||||
}
|
||||
}
|
||||
|
||||
// Arrivés là, cette formule devrait être calculable !
|
||||
|
||||
// A propos du taux
|
||||
if (typeof taux !== 'string' && typeof taux !== 'number'){
|
||||
throw 'Oups, pas de taux compliqués s\'il-vous-plaît'
|
||||
}
|
||||
let tauxValue = taux.indexOf('%') > -1 ?
|
||||
+taux.replace('%', '')/100 :
|
||||
+taux
|
||||
|
||||
return R.reduced([null, assietteValue * tauxValue])
|
||||
}
|
||||
}
|
||||
|
||||
return [variableNames]
|
||||
}, [[], null])
|
||||
extractRuleTypeAndName, // -> {type, name, rule}
|
||||
data => R.assoc(
|
||||
'derived',
|
||||
deriveRule(situation)(data.rule)
|
||||
)(data)
|
||||
)
|
||||
|
||||
let selectedRules = rules.filter(r => extractRuleTypeAndName(r)[1] == 'CIF CDD')
|
||||
// L'objectif de la simulation : quelles règles voulons nous calculer ?
|
||||
let selectedRules = rules.filter(r => extractRuleTypeAndName(r).name == 'CIF CDD')
|
||||
|
||||
export let analyseSituation = (situation = initialSituation) =>
|
||||
export let analyseSituation = situation =>
|
||||
R.pipe(
|
||||
R.map(analyseVariable(situation)),
|
||||
R.flatten()
|
||||
R.map(analyseVariable(situation))
|
||||
)(selectedRules)
|
||||
|
||||
console.log(rules)
|
||||
|
||||
|
||||
export let variableType = name => {
|
||||
let rule = findRuleByName(name)
|
||||
console.log('Getting variable type for ', name)
|
||||
if (name == null) return null
|
||||
|
||||
let found = findRuleByName(name)
|
||||
|
||||
// tellement peu de variables pour l'instant
|
||||
// que c'est très simpliste
|
||||
if (!rule) return 'boolean'
|
||||
if (rule.formule['somme']) return 'numeric'
|
||||
if (!found) return 'boolean'
|
||||
let {rule, type} = found
|
||||
if (typeof rule.formule['somme'] !== 'undefined') return 'numeric'
|
||||
}
|
||||
|
||||
// console.log('RES', JSON.stringify(res))
|
||||
|
||||
|
||||
|
||||
// let types = {
|
||||
|
|
Loading…
Reference in New Issue