mon-entreprise/source/components/TargetSelection.js

361 lines
8.6 KiB
JavaScript

import classNames from 'classnames'
import { T } from 'Components'
import InputSuggestions from 'Components/conversation/InputSuggestions'
import PercentageField from 'Components/PercentageField'
import PeriodSwitch from 'Components/PeriodSwitch'
import withColours from 'Components/utils/withColours'
import withLanguage from 'Components/utils/withLanguage'
import withSitePaths from 'Components/utils/withSitePaths'
import { encodeRuleName } from 'Engine/rules'
import { compose, isEmpty, isNil, propEq } from 'ramda'
import React, { Component, PureComponent } from 'react'
import emoji from 'react-easy-emoji'
import { withTranslation } from 'react-i18next'
import { connect } from 'react-redux'
import { Link } from 'react-router-dom'
import { change, Field, formValueSelector, reduxForm } from 'redux-form'
import {
analysisWithDefaultsSelector,
flatRulesSelector
} from 'Selectors/analyseSelectors'
import Animate from 'Ui/animate'
import AnimatedTargetValue from 'Ui/AnimatedTargetValue'
import CurrencyInput from './CurrencyInput/CurrencyInput'
import './TargetSelection.css'
import { serialiseUnit } from 'Engine/units'
export default compose(
withTranslation(),
withColours,
reduxForm({
form: 'conversation',
destroyOnUnmount: false
}),
connect(
state => ({
getTargetValue: dottedName =>
formValueSelector('conversation')(state, dottedName),
analysis: analysisWithDefaultsSelector(state),
flatRules: flatRulesSelector(state),
activeInput: state.activeTargetInput,
objectifs: state.simulation?.config.objectifs || [],
secondaryObjectives:
state.simulation?.config['objectifs secondaires'] || []
}),
dispatch => ({
setFormValue: (field, name) =>
dispatch(change('conversation', field, name)),
setActiveInput: name =>
dispatch({ type: 'SET_ACTIVE_TARGET_INPUT', name })
})
)
)(
class TargetSelection extends PureComponent {
state = {
initialRender: true
}
getTargets() {
let { secondaryObjectives, analysis } = this.props
if (!analysis) return []
return analysis.targets.filter(
t => !secondaryObjectives.includes(t.dottedName)
)
}
componentDidMount() {
const props = this.props
let targets = this.getTargets()
// Initialize defaultValue for target that can't be computed
targets
.filter(
target =>
(!target.formule || isEmpty(target.formule)) &&
(!isNil(target.defaultValue) ||
!isNil(target.explanation?.defaultValue)) &&
!props.getTargetValue(target.dottedName)
)
.forEach(target => {
props.setFormValue(
target.dottedName,
!isNil(target.defaultValue)
? target.defaultValue
: target.explanation?.defaultValue
)
})
if (this.state.initialRender) {
this.setState({ initialRender: false })
}
}
render() {
let {
colours,
activeInput,
setActiveInput,
setFormValue,
objectifs
} = this.props,
targets = this.getTargets()
return (
<div id="targetSelection">
{(typeof objectifs[0] === 'string' ? [{ objectifs }] : objectifs).map(
({ icône, objectifs: groupTargets, nom }, index) => (
<React.Fragment key={nom || '0'}>
<div style={{ display: 'flex', alignItems: 'end' }}>
<div style={{ flex: 1 }}>
{nom && (
<h2 style={{ marginBottom: 0 }}>
{emoji(icône)} <T>{nom}</T>
</h2>
)}
</div>
{index === 0 && <PeriodSwitch />}
</div>
<section
className="ui__ plain card"
style={{
marginTop: '.6em',
color: colours.textColour,
background: `linear-gradient(
60deg,
${colours.darkColour} 0%,
${colours.colour} 100%
)`
}}>
<Targets
{...{
activeInput,
setActiveInput,
setFormValue,
targets: targets.filter(({ dottedName }) =>
groupTargets.includes(dottedName)
),
initialRender: this.state.initialRender
}}
/>
</section>
</React.Fragment>
)
)}
</div>
)
}
}
)
let Targets = ({
activeInput,
setActiveInput,
setFormValue,
targets,
initialRender
}) => (
<div>
<ul className="targets">
{targets
.map(target => target.explanation || target)
.filter(target => {
return (
target.isApplicable !== false &&
(target.question || target.nodeValue)
)
})
.map(target => (
<Target
key={target.dottedName}
initialRender={initialRender}
{...{
target,
setFormValue,
activeInput,
setActiveInput,
targets
}}
/>
))}
</ul>
</div>
)
const Target = ({
target,
activeInput,
targets,
setActiveInput,
setFormValue,
initialRender
}) => {
const isSmallTarget =
!target.question || !target.formule || isEmpty(target.formule)
return (
<li
key={target.name}
className={isSmallTarget ? 'small-target' : undefined}>
<Animate.appear unless={initialRender}>
<div>
<div className="main">
<Header
{...{
target,
isActiveInput: activeInput === target.dottedName
}}
/>
{isSmallTarget && (
<span
style={{
flex: 1,
borderBottom: '1px dashed #ffffff91',
marginLeft: '1rem'
}}
/>
)}
<TargetInputOrValue
{...{
target,
targets,
activeInput,
setActiveInput,
setFormValue
}}
/>
</div>
{activeInput === target.dottedName && (
<Animate.fromTop>
<InputSuggestions
suggestions={target.suggestions}
onFirstClick={value => {
setFormValue(target.dottedName, '' + value)
}}
rulePeriod={target.période}
colouredBackground={true}
/>
</Animate.fromTop>
)}
</div>
</Animate.appear>
</li>
)
}
let Header = withSitePaths(({ target, sitePaths }) => {
const ruleLink =
sitePaths.documentation.index + '/' + encodeRuleName(target.dottedName)
return (
<span className="header">
<span className="texts">
<span className="optionTitle">
<Link to={ruleLink}>{target.title || target.name}</Link>
</span>
<p>{target.summary}</p>
</span>
</span>
)
})
let CurrencyField = withColours(props => {
return (
<CurrencyInput
style={{
color: props.colours.textColour,
borderColor: props.colours.textColour
}}
debounce={600}
className="targetInput"
value={props.input.value}
{...props.input}
{...props}
/>
)
})
let DebouncedPercentageField = props => (
<PercentageField debounce={600} {...props} />
)
let TargetInputOrValue = withLanguage(
({
target,
targets,
activeInput,
setActiveInput,
language,
firstStepCompleted
}) => {
let inputIsActive = activeInput === target.dottedName
return (
<span className="targetInputOrValue">
{inputIsActive || !target.formule || isEmpty(target.formule) ? (
<Field
name={target.dottedName}
onBlur={event => event.preventDefault()}
component={
{ '€': CurrencyField, '%': DebouncedPercentageField }[
serialiseUnit(target.unit)
]
}
{...(inputIsActive ? { autoFocus: true } : {})}
language={language}
/>
) : (
<TargetValue
{...{
targets,
target,
activeInput,
setActiveInput,
firstStepCompleted
}}
/>
)}
</span>
)
}
)
const TargetValue = connect(
state => ({
blurValue: analysisWithDefaultsSelector(state)?.cache.inversionFail
}),
dispatch => ({
setFormValue: (field, name) => dispatch(change('conversation', field, name))
})
)(
class TargetValue extends Component {
render() {
let { targets, target, blurValue } = this.props
let targetWithValue =
targets && targets.find(propEq('dottedName', target.dottedName)),
value = targetWithValue && targetWithValue.nodeValue
return (
<div
className={classNames({
editable: target.question,
attractClick: target.question && isNil(target.nodeValue)
})}
style={blurValue ? { filter: 'blur(3px)' } : {}}
{...(target.question ? { tabIndex: 0 } : {})}
onClick={this.showField(value)}
onFocus={this.showField(value)}>
<AnimatedTargetValue value={value} />
</div>
)
}
showField(value) {
let { target, setFormValue, activeInput, setActiveInput } = this.props
return () => {
if (!target.question) return
if (value != null && !Number.isNaN(value))
setFormValue(target.dottedName, Math.round(value) + '')
if (activeInput) setFormValue(activeInput, '')
setActiveInput(target.dottedName)
}
}
}
)