mon-entreprise/source/components/TargetSelection.tsx

326 lines
8.7 KiB
TypeScript

import { setActiveTarget, updateSituation } from 'Actions/actions'
import InputSuggestions from 'Components/conversation/InputSuggestions'
import PeriodSwitch from 'Components/PeriodSwitch'
import RuleLink from 'Components/RuleLink'
import { ThemeColorsContext } from 'Components/utils/colors'
import {
EngineContext,
useEvaluation,
useInversionFail
} from 'Components/utils/EngineContext'
import { SitePathsContext } from 'Components/utils/SitePathsContext'
import { formatCurrency, formatValue } from 'Engine/format'
import { EvaluatedRule, EvaluatedNode } from 'Engine/types'
import { isNil } from 'ramda'
import React, { useCallback, useContext, useEffect, useState } from 'react'
import emoji from 'react-easy-emoji'
import { Trans, useTranslation } from 'react-i18next'
import { useDispatch, useSelector } from 'react-redux'
import { Link, useLocation } from 'react-router-dom'
import { RootState, SimulationConfig } from 'Reducers/rootReducer'
import { DottedName, ParsedRule } from 'Rules'
import {
situationSelector,
targetUnitSelector
} from 'Selectors/simulationSelectors'
import Animate from 'Ui/animate'
import AnimatedTargetValue from 'Ui/AnimatedTargetValue'
import CurrencyInput from './CurrencyInput/CurrencyInput'
import './TargetSelection.css'
export default function TargetSelection({ showPeriodSwitch = true }) {
const objectifs = useSelector(
(state: RootState) => state.simulation?.config.objectifs || []
)
const colors = useContext(ThemeColorsContext)
return (
<div id="targetSelection">
{((typeof objectifs[0] === 'string'
? [{ objectifs }]
: objectifs) as any).map(
({ icône, objectifs: targets, nom }, index: number) => (
<React.Fragment key={nom || '0'}>
<div style={{ display: 'flex', alignItems: 'end' }}>
<div style={{ flex: 1 }}>
{nom && (
<h2 style={{ marginBottom: 0 }}>
{emoji(icône)} <Trans>{nom}</Trans>
</h2>
)}
</div>
{index === 0 && showPeriodSwitch && <PeriodSwitch />}
</div>
<section
className="ui__ plain card"
style={{
marginTop: '.6em',
color: colors.textColor,
background: `linear-gradient(
60deg,
${colors.darkColor} 0%,
${colors.color} 100%
)`
}}
>
<ul className="targets">
{' '}
{targets.map(target => (
<Target key={target} dottedName={target} />
))}
</ul>
</section>
</React.Fragment>
)
)}
</div>
)
}
type TargetProps = {
dottedName: DottedName
}
const Target = ({ dottedName }: TargetProps) => {
const activeInput = useSelector((state: RootState) => state.activeTargetInput)
const target = useEvaluation(dottedName, {
unit: useSelector(targetUnitSelector)
})
const dispatch = useDispatch()
const onSuggestionClick = useCallback(
value => {
dispatch(updateSituation(target.dottedName, value))
},
[target.dottedName, dispatch]
)
const isSmallTarget = !!target.question !== !!target.formule
if (
target.nodeValue === false ||
(isSmallTarget && !target.question && !target.nodeValue)
) {
return null
}
const isActiveInput = activeInput === target.dottedName
return (
<li
key={target.name}
className={isSmallTarget ? 'small-target' : undefined}
>
<Animate.appear unless={!isSmallTarget}>
<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>
<div css="display: flex; justify-content: flex-end">
<InputSuggestions
suggestions={target.suggestions}
onFirstClick={onSuggestionClick}
unit={target.unit}
/>
</div>
</Animate.fromTop>
)}
</div>
</Animate.appear>
</li>
)
}
let Header = ({ target }: { target: ParsedRule }) => {
const sitePaths = useContext(SitePathsContext)
const { t } = useTranslation()
const { pathname } = useLocation()
// TODO : Super hacky, we want to amend one label in the covid simulator, but
// because the label is fetched from the global state we have to do a hack
// here based on the URL.
const hackyShowPeriod = pathname === sitePaths.coronavirus
return (
<span className="header">
<span className="texts">
<span className="optionTitle">
<Link
to={{
pathname: sitePaths.documentation.rule(target.dottedName),
state: { useDefaultValues: true }
}}
>
{target.title || target.name}
{hackyShowPeriod && ' ' + t('mensuel')}
</Link>
</span>
<p className="ui__ notice">{target.summary}</p>
</span>
</span>
)
}
type TargetInputOrValueProps = {
target: EvaluatedRule<DottedName>
isActiveInput: boolean
isSmallTarget: boolean
}
function TargetInputOrValue({
target,
isActiveInput,
isSmallTarget
}: TargetInputOrValueProps) {
const { language } = useTranslation().i18n
const colors = useContext(ThemeColorsContext)
const dispatch = useDispatch()
const situationValue = useSelector(situationSelector)[target.dottedName]
const targetUnit = useSelector(targetUnitSelector)
const engine = useContext(EngineContext)
const value =
typeof situationValue === 'string'
? Math.round(
engine.evaluate(situationValue, { unit: targetUnit })
.nodeValue as number
)
: situationValue != null
? situationValue
: target?.nodeValue != null
? Math.round(+target.nodeValue)
: undefined
const blurValue = useInversionFail() && !isActiveInput
const onChange = useCallback(
evt =>
dispatch(
updateSituation(target.dottedName, +evt.target.value + ' ' + targetUnit)
),
[targetUnit, target, dispatch]
)
return (
<span
className="targetInputOrValue"
style={blurValue ? { filter: 'blur(3px)' } : {}}
>
{target.question ? (
<>
{!isActiveInput && <AnimatedTargetValue value={value} />}
<CurrencyInput
style={{
color: colors.textColor,
borderColor: colors.textColor
}}
debounce={600}
name={target.dottedName}
value={value}
className={
isActiveInput ||
isNil(value) ||
(target.question && isSmallTarget)
? 'targetInput'
: 'editableTarget'
}
onChange={onChange}
onFocus={() => {
if (isSmallTarget) return
dispatch(setActiveTarget(target.dottedName))
}}
language={language}
/>
<span className="targetInputBottomBorder">
{formatCurrency(value, language)}
</span>
</>
) : (
<span>
{value && Number.isNaN(value) ? '—' : formatCurrency(value, language)}
</span>
)}
{target.dottedName.includes('prix du travail') && <AidesGlimpse />}
{target.dottedName === 'contrat salarié . rémunération . net' && (
<TitreRestaurant />
)}
</span>
)
}
function TitreRestaurant() {
const targetUnit = useSelector(targetUnitSelector)
const titresRestaurant = useEvaluation(
'contrat salarié . frais professionnels . titres-restaurant . montant',
{ unit: targetUnit }
)
const { language } = useTranslation().i18n
if (!titresRestaurant?.nodeValue) return null
return (
<Animate.fromTop>
<div className="aidesGlimpse">
<RuleLink {...titresRestaurant}>
+{' '}
<strong>
{formatValue({
nodeValue: titresRestaurant.nodeValue,
unit: '€',
language
})}
</strong>{' '}
<Trans>en titres-restaurant</Trans> {emoji(' 🍽')}
</RuleLink>
</div>
</Animate.fromTop>
)
}
function AidesGlimpse() {
const targetUnit = useSelector(targetUnitSelector)
const aides = useEvaluation('contrat salarié . aides employeur', {
unit: targetUnit
})
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 aidesDetail = aides?.formule.explanation.explanation
const aidesNotNul = aidesDetail?.filter(node => node.nodeValue !== false)
const aideLink = aidesNotNul?.length === 1 ? aidesNotNul[0] : aides
if (!aides?.nodeValue) return null
return (
<Animate.fromTop>
<div className="aidesGlimpse">
<RuleLink {...aideLink}>
<Trans>en incluant</Trans>{' '}
<strong>
<span>
{formatValue({
nodeValue: aides.nodeValue,
unit: '€',
language
})}
</span>
</strong>{' '}
<Trans>d'aides</Trans> {emoji(aides?.icons ?? '')}
</RuleLink>
</div>
</Animate.fromTop>
)
}