317 lines
8.3 KiB
TypeScript
317 lines
8.3 KiB
TypeScript
import { setActiveTarget, updateSituation } from 'Actions/actions'
|
|
import { T } from 'Components'
|
|
import InputSuggestions from 'Components/conversation/InputSuggestions'
|
|
import PeriodSwitch from 'Components/PeriodSwitch'
|
|
import RuleLink from 'Components/RuleLink'
|
|
import { ThemeColoursContext } from 'Components/utils/withColours'
|
|
import { SitePathsContext } from 'Components/utils/withSitePaths'
|
|
import { formatCurrency } from 'Engine/format'
|
|
import { encodeRuleName } from 'Engine/rules'
|
|
import { isEmpty, isNil } from 'ramda'
|
|
import React, { useContext, useEffect, useState } from 'react'
|
|
import emoji from 'react-easy-emoji'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { useDispatch, useSelector } from 'react-redux'
|
|
import { Link } from 'react-router-dom'
|
|
import { RootState } from 'Reducers/rootReducer'
|
|
import {
|
|
analysisWithDefaultsSelector,
|
|
situationSelector,
|
|
useTarget
|
|
} from 'Selectors/analyseSelectors'
|
|
import { Rule } from 'Types/rule'
|
|
import Animate from 'Ui/animate'
|
|
import AnimatedTargetValue from 'Ui/AnimatedTargetValue'
|
|
import CurrencyInput from './CurrencyInput/CurrencyInput'
|
|
import './TargetSelection.css'
|
|
|
|
export default function TargetSelection() {
|
|
const [initialRender, setInitialRender] = useState(true)
|
|
const analysis = useSelector(analysisWithDefaultsSelector)
|
|
const objectifs = useSelector(
|
|
(state: RootState) => state.simulation?.config.objectifs || []
|
|
)
|
|
const secondaryObjectives = useSelector(
|
|
(state: RootState) =>
|
|
state.simulation?.config['objectifs secondaires'] || []
|
|
)
|
|
const situation = useSelector(situationSelector)
|
|
const dispatch = useDispatch()
|
|
const colours = useContext(ThemeColoursContext)
|
|
|
|
const targets =
|
|
analysis?.targets.filter(
|
|
t =>
|
|
!secondaryObjectives.includes(t.dottedName) &&
|
|
t.dottedName !== 'contrat salarié . aides employeur'
|
|
) || []
|
|
|
|
useEffect(() => {
|
|
// Initialize defaultValue for target that can't be computed
|
|
// TODO: this logic shouldn't be here
|
|
targets
|
|
.filter(
|
|
target =>
|
|
(!target.formule || isEmpty(target.formule)) &&
|
|
(!isNil(target.defaultValue) ||
|
|
!isNil(target.explanation?.defaultValue)) &&
|
|
!situation[target.dottedName]
|
|
)
|
|
|
|
.forEach(target => {
|
|
dispatch(
|
|
updateSituation(
|
|
target.dottedName,
|
|
!isNil(target.defaultValue)
|
|
? target.defaultValue
|
|
: target.explanation?.defaultValue
|
|
)
|
|
)
|
|
})
|
|
|
|
setInitialRender(false)
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [])
|
|
|
|
return (
|
|
<div id="targetSelection">
|
|
{((typeof objectifs[0] === 'string'
|
|
? [{ objectifs }]
|
|
: objectifs) as any).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
|
|
{...{
|
|
targets: targets.filter(({ dottedName }) =>
|
|
groupTargets.includes(dottedName)
|
|
),
|
|
initialRender
|
|
}}
|
|
/>
|
|
</section>
|
|
</React.Fragment>
|
|
)
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
let Targets = ({ 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
|
|
}}
|
|
/>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
)
|
|
|
|
const Target = ({ target, initialRender }) => {
|
|
const activeInput = useSelector((state: RootState) => state.activeTargetInput)
|
|
const dispatch = useDispatch()
|
|
|
|
const isActiveInput = activeInput === target.dottedName
|
|
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
|
|
}}
|
|
/>
|
|
{isSmallTarget && (
|
|
<span
|
|
style={{
|
|
flex: 1,
|
|
borderBottom: '1px dashed #ffffff91',
|
|
marginLeft: '1rem'
|
|
}}
|
|
/>
|
|
)}
|
|
<TargetInputOrValue
|
|
{...{
|
|
target,
|
|
isActiveInput,
|
|
isSmallTarget
|
|
}}
|
|
/>
|
|
</div>
|
|
{isActiveInput && (
|
|
<Animate.fromTop>
|
|
<InputSuggestions
|
|
suggestions={target.suggestions}
|
|
onFirstClick={value => {
|
|
dispatch(updateSituation(target.dottedName, value))
|
|
}}
|
|
rulePeriod={target.période}
|
|
colouredBackground={true}
|
|
/>
|
|
</Animate.fromTop>
|
|
)}
|
|
</div>
|
|
</Animate.appear>
|
|
</li>
|
|
)
|
|
}
|
|
|
|
let Header = ({ target }) => {
|
|
const sitePaths = useContext(SitePathsContext)
|
|
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>
|
|
)
|
|
}
|
|
|
|
type TargetInputOrValueProps = {
|
|
target: Rule
|
|
isActiveInput: boolean
|
|
isSmallTarget: boolean
|
|
}
|
|
|
|
let TargetInputOrValue = ({
|
|
target,
|
|
isActiveInput,
|
|
isSmallTarget
|
|
}: TargetInputOrValueProps) => {
|
|
const { language } = useTranslation().i18n
|
|
const colors = useContext(ThemeColoursContext)
|
|
const dispatch = useDispatch()
|
|
const situationValue = Math.round(
|
|
useSelector(situationSelector)[target.dottedName]
|
|
)
|
|
const targetWithValue = useTarget(target.dottedName)
|
|
const value = targetWithValue?.nodeValue
|
|
? Math.round(targetWithValue?.nodeValue)
|
|
: undefined
|
|
const inversionFail = useSelector(
|
|
(state: RootState) =>
|
|
analysisWithDefaultsSelector(state)?.cache.inversionFail
|
|
)
|
|
const blurValue = inversionFail && !isActiveInput && value
|
|
|
|
return (
|
|
<span
|
|
className="targetInputOrValue"
|
|
style={blurValue ? { filter: 'blur(3px)' } : {}}
|
|
>
|
|
{target.question ? (
|
|
<>
|
|
{!isActiveInput && <AnimatedTargetValue value={value} />}
|
|
<CurrencyInput
|
|
style={{
|
|
color: colors.textColour,
|
|
borderColor: colors.textColour
|
|
}}
|
|
debounce={600}
|
|
name={target.dottedName}
|
|
value={situationValue || value}
|
|
className={
|
|
isActiveInput || isNil(value) ? 'targetInput' : 'editableTarget'
|
|
}
|
|
onChange={evt =>
|
|
dispatch(
|
|
updateSituation(target.dottedName, Number(evt.target.value))
|
|
)
|
|
}
|
|
onFocus={() => {
|
|
if (isSmallTarget) return
|
|
dispatch(setActiveTarget(target.dottedName))
|
|
}}
|
|
language={language}
|
|
/>
|
|
<span className="targetInputBottomBorder">
|
|
{formatCurrency(value, language)}
|
|
</span>
|
|
</>
|
|
) : (
|
|
<span>
|
|
{Number.isNaN(value) ? '—' : formatCurrency(value, language)}
|
|
</span>
|
|
)}
|
|
{target.dottedName.includes('prix du travail') && <AidesGlimpse />}
|
|
</span>
|
|
)
|
|
}
|
|
|
|
function AidesGlimpse() {
|
|
const aides = useTarget('contrat salarié . aides employeur')
|
|
const { language } = useTranslation().i18n
|
|
|
|
// Dans le cas où il n'y a qu'une seule aide à l'embauche qui s'applique, nous
|
|
// faisons un lien direct vers cette aide, plutôt qu'un lien vers la liste qui
|
|
// est une somme des aides qui sont toutes nulle sauf l'aide active.
|
|
const aidesNode = aides.explanation
|
|
const aidesDetail = aides.explanation.formule.explanation.explanation
|
|
const aidesNotNul = aidesDetail.filter(node => node.nodeValue !== 0)
|
|
const aideLink = aidesNotNul.length === 1 ? aidesNotNul[0] : aidesNode
|
|
|
|
if (!aides?.nodeValue) return null
|
|
return (
|
|
<Animate.appear>
|
|
<div className="aidesGlimpse">
|
|
<RuleLink {...aideLink}>
|
|
<T>en incluant</T>{' '}
|
|
<strong>
|
|
<AnimatedTargetValue value={aides.nodeValue}>
|
|
<span>{formatCurrency(aides.nodeValue, language)}</span>
|
|
</AnimatedTargetValue>
|
|
</strong>{' '}
|
|
<T>d'aides</T> {emoji(aides.explanation.icons)}
|
|
</RuleLink>
|
|
</div>
|
|
</Animate.appear>
|
|
)
|
|
}
|