Revert "Revert "Refacto : séparation claire du moteur et de l'application 🔥""
This reverts commit 8c7ab52a4f
.
pull/993/head
parent
c2249929c9
commit
7ccc4ce4e3
|
@ -2,9 +2,6 @@
|
|||
"editor.formatOnSave": true,
|
||||
"spellright.language": ["fr", "en"],
|
||||
"spellright.documentTypes": ["yaml", "git-commit", "markdown"],
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": true
|
||||
},
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"editor.tabSize": 2
|
||||
}
|
||||
|
|
|
@ -50,6 +50,7 @@ jobs:
|
|||
steps:
|
||||
- install
|
||||
- run: |
|
||||
git config --global core.quotepath false
|
||||
yarn test
|
||||
yarn test-regressions
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import chai from 'chai'
|
||||
import Enzyme from 'enzyme'
|
||||
import Adapter from 'enzyme-adapter-react-16'
|
||||
import chai from 'chai'
|
||||
import sinonChai from 'sinon-chai'
|
||||
|
||||
chai.use(sinonChai)
|
||||
|
|
|
@ -1,4 +1,13 @@
|
|||
const fr = Cypress.env('language') === 'fr'
|
||||
const testText = (selector, text) =>
|
||||
cy.get(`[data-test-id=${selector}]`).should($span => {
|
||||
const displayedText = $span
|
||||
.text()
|
||||
.trim()
|
||||
.replace(/[\s]/g, ' ')
|
||||
console.log(displayedText, text)
|
||||
expect(displayedText).to.eq(text)
|
||||
})
|
||||
|
||||
describe('Page covid-19', function() {
|
||||
if (!fr) {
|
||||
|
@ -11,12 +20,12 @@ describe('Page covid-19', function() {
|
|||
it('should display 100% de prise en charge pour un SMIC', () => {
|
||||
cy.get('input.currencyInput__input').click()
|
||||
cy.contains('SMIC').click()
|
||||
cy.contains('Soit 100% du revenu net')
|
||||
cy.contains('Soit 0% du coût habituel')
|
||||
testText('comparaison-net', 'Soit 100 % du revenu net')
|
||||
testText('comparaison-total', 'Soit 0 % du coût habituel')
|
||||
})
|
||||
it('should display 85% de prise en charge pour un salaire médian', () => {
|
||||
it('should display 85 % de prise en charge pour un salaire médian', () => {
|
||||
cy.contains('salaire médian').click()
|
||||
cy.contains('Soit 85% du revenu net')
|
||||
cy.contains('Soit 0% du coût habituel')
|
||||
testText('comparaison-net', 'Soit 85 % du revenu net')
|
||||
testText('comparaison-total', 'Soit 0 % du coût habituel')
|
||||
})
|
||||
})
|
||||
|
|
|
@ -20,7 +20,7 @@ describe('Simulateurs', function() {
|
|||
}
|
||||
cy.get(inputSelector).each((testedInput, i) => {
|
||||
cy.wrap(testedInput).type('{selectall}60000')
|
||||
cy.wait(600)
|
||||
cy.wait(800)
|
||||
cy.contains('Cotisations')
|
||||
cy.get(inputSelector).each(($input, j) => {
|
||||
const val = $input.val().replace(/[\s,.]/g, '')
|
||||
|
@ -41,7 +41,7 @@ describe('Simulateurs', function() {
|
|||
if (['indépendant', 'assimilé-salarié'].includes(simulateur)) {
|
||||
cy.get(chargeInputSelector).type('{selectall}6000')
|
||||
}
|
||||
cy.wait(600)
|
||||
cy.wait(800)
|
||||
cy.contains('€ / mois').click()
|
||||
cy.get(inputSelector)
|
||||
.first()
|
||||
|
@ -108,7 +108,7 @@ describe('Simulateurs', function() {
|
|||
cy.get(inputSelector)
|
||||
.first()
|
||||
.type('{selectall}5000')
|
||||
cy.wait(600)
|
||||
cy.wait(800)
|
||||
cy.get(inputSelector).each($input => {
|
||||
const val = +$input.val().replace(/[\s,.]/g, '')
|
||||
expect(val).not.to.be.below(4000)
|
||||
|
|
|
@ -62,7 +62,7 @@
|
|||
/>
|
||||
<!-- data-helmet pour que React Helmet puisse écraser ce meta par défaut -->
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css?family=Roboto:300,400,400i,500,600|Montserrat:400,600"
|
||||
href="https://fonts.googleapis.com/css?family=Roboto:400,400i,600|Montserrat:400,600"
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
/>
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { ThemeColorsProvider } from 'Components/utils/colors'
|
||||
import { SitePathProvider, SitePaths } from 'Components/utils/withSitePaths'
|
||||
import { EngineProvider } from 'Components/utils/EngineContext'
|
||||
import { SitePathProvider, SitePaths } from 'Components/utils/SitePathsContext'
|
||||
import { TrackerProvider } from 'Components/utils/withTracker'
|
||||
import Engine from 'Engine'
|
||||
import { createBrowserHistory } from 'history'
|
||||
import { AvailableLangs } from 'i18n'
|
||||
import i18next from 'i18next'
|
||||
|
@ -12,6 +14,7 @@ import reducers, { RootState } from 'Reducers/rootReducer'
|
|||
import { applyMiddleware, compose, createStore, Middleware, Store } from 'redux'
|
||||
import thunk from 'redux-thunk'
|
||||
import Tracker from 'Tracker'
|
||||
import { Rules } from './rules'
|
||||
import { inIframe } from './utils'
|
||||
|
||||
declare global {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { SitePaths } from 'Components/utils/withSitePaths'
|
||||
import { SitePaths } from 'Components/utils/SitePathsContext'
|
||||
import { History } from 'history'
|
||||
import { RootState, SimulationConfig } from 'Reducers/rootReducer'
|
||||
import { ThunkAction } from 'redux-thunk'
|
||||
|
@ -12,13 +12,12 @@ export type Action =
|
|||
| UpdateAction
|
||||
| SetSimulationConfigAction
|
||||
| DeletePreviousSimulationAction
|
||||
| SetExempleAction
|
||||
| ExplainVariableAction
|
||||
| UpdateSituationAction
|
||||
| HideControlAction
|
||||
| LoadPreviousSimulationAction
|
||||
| SetSituationBranchAction
|
||||
| UpdateDefaultUnitAction
|
||||
| UpdateTargetUnitAction
|
||||
| SetActiveTargetAction
|
||||
| CompanyStatusAction
|
||||
|
||||
|
@ -46,18 +45,6 @@ type DeletePreviousSimulationAction = {
|
|||
type: 'DELETE_PREVIOUS_SIMULATION'
|
||||
}
|
||||
|
||||
type SetExempleAction =
|
||||
| {
|
||||
type: 'SET_EXAMPLE'
|
||||
name: null
|
||||
}
|
||||
| {
|
||||
type: 'SET_EXAMPLE'
|
||||
name: string
|
||||
situation: object
|
||||
dottedName: DottedName
|
||||
}
|
||||
|
||||
type ResetSimulationAction = ReturnType<typeof resetSimulation>
|
||||
type UpdateAction = ReturnType<typeof updateSituation>
|
||||
type UpdateSituationAction = ReturnType<typeof updateSituation>
|
||||
|
@ -66,14 +53,14 @@ type SetSituationBranchAction = ReturnType<typeof setSituationBranch>
|
|||
type SetActiveTargetAction = ReturnType<typeof setActiveTarget>
|
||||
type HideControlAction = ReturnType<typeof hideControl>
|
||||
type ExplainVariableAction = ReturnType<typeof explainVariable>
|
||||
type UpdateDefaultUnitAction = ReturnType<typeof updateUnit>
|
||||
type UpdateTargetUnitAction = ReturnType<typeof updateUnit>
|
||||
|
||||
export const resetSimulation = () =>
|
||||
({
|
||||
type: 'RESET_SIMULATION'
|
||||
} as const)
|
||||
|
||||
export const goToQuestion = (question: string) =>
|
||||
export const goToQuestion = (question: DottedName) =>
|
||||
({
|
||||
type: 'STEP_ACTION',
|
||||
name: 'unfold',
|
||||
|
@ -99,7 +86,7 @@ export const setSituationBranch = (id: number) =>
|
|||
} as const)
|
||||
|
||||
export const setSimulationConfig = (
|
||||
config: Object,
|
||||
config: SimulationConfig,
|
||||
useCompanyDetails: boolean = false
|
||||
): ThunkResult<void> => (dispatch, getState, { history }): void => {
|
||||
if (getState().simulation?.config === config) {
|
||||
|
@ -134,26 +121,17 @@ export const updateSituation = (fieldName: DottedName, value: unknown) =>
|
|||
value
|
||||
} as const)
|
||||
|
||||
export const updateUnit = (defaultUnit: string) =>
|
||||
export const updateUnit = (targetUnit: string) =>
|
||||
({
|
||||
type: 'UPDATE_DEFAULT_UNIT',
|
||||
defaultUnit
|
||||
type: 'UPDATE_TARGET_UNIT',
|
||||
targetUnit
|
||||
} as const)
|
||||
|
||||
export function setExample(
|
||||
name: string,
|
||||
situation: Situation,
|
||||
dottedName: DottedName
|
||||
) {
|
||||
return { type: 'SET_EXAMPLE', name, situation, dottedName } as const
|
||||
}
|
||||
|
||||
export const goBackToSimulation = (): ThunkResult<void> => (
|
||||
dispatch,
|
||||
_,
|
||||
getState,
|
||||
{ history }
|
||||
) => {
|
||||
dispatch({ type: 'SET_EXAMPLE', name: null })
|
||||
const url = getState().simulation?.url
|
||||
url && history.push(url)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react'
|
||||
import emoji from 'react-easy-emoji'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { firstStepCompletedSelector } from 'Selectors/analyseSelectors'
|
||||
import { firstStepCompletedSelector } from 'Selectors/simulationSelectors'
|
||||
import Animate from 'Ui/animate'
|
||||
import './Banner.css'
|
||||
|
||||
|
|
|
@ -4,31 +4,34 @@ import emoji from 'react-easy-emoji'
|
|||
import { animated, config, useSpring } from 'react-spring'
|
||||
import useDisplayOnIntersecting from 'Components/utils/useDisplayOnIntersecting'
|
||||
import { ThemeColorsContext } from 'Components/utils/colors'
|
||||
import { formatValue } from 'Engine/format'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const ANIMATION_SPRING = config.gentle
|
||||
|
||||
let ChartItemBar = ({ styles, color, numberToPlot, unit }) => (
|
||||
<div className="distribution-chart__bar-container">
|
||||
<animated.div
|
||||
className="distribution-chart__bar"
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
flex: styles.flex
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
css={`
|
||||
font-weight: bold;
|
||||
margin-left: 1rem;
|
||||
color: var(--textColorOnWhite);
|
||||
`}
|
||||
>
|
||||
<Value maximumFractionDigits={0} unit={unit}>
|
||||
{numberToPlot}
|
||||
</Value>
|
||||
let ChartItemBar = ({ styles, color, numberToPlot, unit }) => {
|
||||
const language = useTranslation().i18n.language
|
||||
return (
|
||||
<div className="distribution-chart__bar-container">
|
||||
<animated.div
|
||||
className="distribution-chart__bar"
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
flex: styles.flex
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
css={`
|
||||
font-weight: bold;
|
||||
margin-left: 1rem;
|
||||
color: var(--textColorOnWhite);
|
||||
`}
|
||||
>
|
||||
{formatValue({ nodeValue: numberToPlot, unit, precision: 0, language })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)
|
||||
}
|
||||
let BranchIcône = ({ icône }) => (
|
||||
<div className="distribution-chart__legend">
|
||||
<span className="distribution-chart__icon">{emoji(icône)}</span>
|
||||
|
|
|
@ -1,24 +1,22 @@
|
|||
import { goToQuestion, hideControl } from 'Actions/actions'
|
||||
import { useControls, useInversionFail } from 'Components/utils/EngineContext'
|
||||
import { makeJsx } from 'Engine/evaluation'
|
||||
import React from 'react'
|
||||
import emoji from 'react-easy-emoji'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { RootState } from 'Reducers/rootReducer'
|
||||
import { analysisWithDefaultsSelector } from 'Selectors/analyseSelectors'
|
||||
import animate from 'Ui/animate'
|
||||
import './Controls.css'
|
||||
import { Markdown } from './utils/markdown'
|
||||
import { ScrollToElement } from './utils/Scroll'
|
||||
import { answeredQuestionsSelector } from 'Selectors/simulationSelectors'
|
||||
|
||||
export default function Controls() {
|
||||
const { t } = useTranslation()
|
||||
const foldedSteps = useSelector(
|
||||
(state: RootState) => state.simulation?.foldedSteps
|
||||
)
|
||||
const analysis = useSelector(analysisWithDefaultsSelector)
|
||||
const controls = analysis?.controls
|
||||
const inversionFail = analysis?.cache._meta.inversionFail
|
||||
const answeredQuestions = useSelector(answeredQuestionsSelector)
|
||||
const controls = useControls()
|
||||
const inversionFail = useInversionFail()
|
||||
const hiddenControls = useSelector(
|
||||
(state: RootState) => state.simulation?.hiddenControls
|
||||
)
|
||||
|
@ -56,7 +54,7 @@ export default function Controls() {
|
|||
<span id="controlExplanation">{makeJsx(evaluated)}</span>
|
||||
)}
|
||||
|
||||
{solution && !foldedSteps?.includes(solution.cible) && (
|
||||
{solution && !answeredQuestions?.includes(solution.cible) && (
|
||||
<div>
|
||||
<button
|
||||
key={solution.cible}
|
||||
|
|
|
@ -47,12 +47,12 @@ describe('CurrencyInput', () => {
|
|||
expect(input.instance().value).to.equal('0,5')
|
||||
})
|
||||
|
||||
it('should not accept negative number', () => {
|
||||
it('should accept negative number', () => {
|
||||
let onChange = spy()
|
||||
const input = getInput(<CurrencyInput onChange={onChange} />)
|
||||
input.simulate('change', { target: { value: '-12', focus: () => {} } })
|
||||
expect(onChange).to.have.been.calledWith(
|
||||
match.hasNested('target.value', '12')
|
||||
match.hasNested('target.value', '-12')
|
||||
)
|
||||
})
|
||||
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import classnames from 'classnames'
|
||||
import { currencyFormat } from 'Engine/format'
|
||||
import React, { useRef, useState } from 'react'
|
||||
import React, { useMemo, useRef, useState } from 'react'
|
||||
import NumberFormat, { NumberFormatProps } from 'react-number-format'
|
||||
import { debounce } from '../../utils'
|
||||
import './CurrencyInput.css'
|
||||
|
||||
type CurrencyInputProps = NumberFormatProps & {
|
||||
value?: string | number
|
||||
value?: string | number | null
|
||||
debounce?: number
|
||||
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void
|
||||
currencySymbol?: string
|
||||
|
@ -25,8 +25,12 @@ export default function CurrencyInput({
|
|||
}: CurrencyInputProps) {
|
||||
const [initialValue, setInitialValue] = useState(valueProp)
|
||||
const [currentValue, setCurrentValue] = useState(valueProp)
|
||||
const onChangeDebounced = useRef(
|
||||
debounceTimeout && onChange ? debounce(debounceTimeout, onChange) : onChange
|
||||
const onChangeDebounced = useMemo(
|
||||
() =>
|
||||
debounceTimeout && onChange
|
||||
? debounce(debounceTimeout, onChange)
|
||||
: onChange,
|
||||
[onChange, debounceTimeout]
|
||||
)
|
||||
// We need some mutable reference because the <NumberFormat /> component doesn't provide
|
||||
// the DOM `event` in its custom `onValueChange` handler
|
||||
|
@ -54,7 +58,7 @@ export default function CurrencyInput({
|
|||
value: nextValue.current
|
||||
}
|
||||
nextValue.current = ''
|
||||
onChangeDebounced.current?.(event)
|
||||
onChangeDebounced?.(event)
|
||||
}
|
||||
|
||||
const {
|
||||
|
@ -89,10 +93,10 @@ export default function CurrencyInput({
|
|||
}
|
||||
onValueChange={({ value }) => {
|
||||
setCurrentValue(value)
|
||||
nextValue.current = value.toString().replace('-', '')
|
||||
nextValue.current = value.toString().replace(/^0+/, '')
|
||||
}}
|
||||
onChange={handleChange}
|
||||
value={currentValue.toString().replace('.', decimalSeparator)}
|
||||
value={currentValue?.toString().replace('.', decimalSeparator)}
|
||||
autoComplete="off"
|
||||
/>
|
||||
{!isCurrencyPrefixed && <> €</>}
|
||||
|
|
|
@ -1,35 +1,50 @@
|
|||
import React from 'react'
|
||||
import { ThemeColorsContext } from 'Components/utils/colors'
|
||||
import { EngineContext } from 'Components/utils/EngineContext'
|
||||
import useDisplayOnIntersecting from 'Components/utils/useDisplayOnIntersecting'
|
||||
import { formatValue } from 'Engine/format'
|
||||
import { add, max } from 'ramda'
|
||||
import { default as React, default as React, useContext } from 'react'
|
||||
import emoji from 'react-easy-emoji'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { DottedName } from 'Rules'
|
||||
import { parsedRulesSelector } from 'Selectors/analyseSelectors'
|
||||
import répartitionSelector from 'Selectors/repartitionSelectors'
|
||||
import { targetUnitSelector } from 'Selectors/simulationSelectors'
|
||||
import BarChartBranch from './BarChart'
|
||||
import './Distribution.css'
|
||||
import './PaySlip'
|
||||
import { getCotisationsBySection } from './PaySlip'
|
||||
import RuleLink from './RuleLink'
|
||||
import BarChartBranch from './BarChart'
|
||||
|
||||
export default function Distribution() {
|
||||
const distribution = useSelector(répartitionSelector) as any
|
||||
const targetUnit = useSelector(targetUnitSelector)
|
||||
const engine = useContext(EngineContext)
|
||||
const distribution = (getCotisationsBySection(
|
||||
useContext(EngineContext).getParsedRules()
|
||||
).map(([section, cotisations]) => [
|
||||
section,
|
||||
cotisations
|
||||
.map(c => engine.evaluate(c, { unit: targetUnit }))
|
||||
.reduce(
|
||||
(acc, evaluation) => acc + ((evaluation?.nodeValue as number) || 0),
|
||||
0
|
||||
)
|
||||
]) as Array<[DottedName, number]>)
|
||||
.filter(([, value]) => value > 0)
|
||||
.sort(([, a], [, b]) => b - a)
|
||||
|
||||
if (!Object.values(distribution).length) {
|
||||
return null
|
||||
}
|
||||
const maximum = distribution.map(([, value]) => value).reduce(max, 0)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="distribution-chart__container">
|
||||
{distribution.répartition.map(
|
||||
([brancheDottedName, { partPatronale, partSalariale }]) => (
|
||||
<DistributionBranch
|
||||
key={brancheDottedName}
|
||||
dottedName={brancheDottedName}
|
||||
value={partPatronale + partSalariale}
|
||||
maximum={distribution.maximum}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
<div className="distribution-chart__container">
|
||||
{distribution.map(([sectionName, value]) => (
|
||||
<DistributionBranch
|
||||
key={sectionName}
|
||||
dottedName={sectionName}
|
||||
value={value}
|
||||
maximum={maximum}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -37,6 +52,7 @@ type DistributionBranchProps = {
|
|||
dottedName: DottedName
|
||||
value: number
|
||||
maximum: number
|
||||
|
||||
icon?: string
|
||||
}
|
||||
|
||||
|
@ -46,15 +62,16 @@ export function DistributionBranch({
|
|||
icon,
|
||||
maximum
|
||||
}: DistributionBranchProps) {
|
||||
const rules = useSelector(parsedRulesSelector)
|
||||
const branch = rules[dottedName]
|
||||
const rules = useContext(EngineContext).getParsedRules()
|
||||
const branche = rules[dottedName]
|
||||
|
||||
return (
|
||||
<BarChartBranch
|
||||
value={value}
|
||||
maximum={maximum}
|
||||
title={<RuleLink {...branch} />}
|
||||
icon={icon ?? branch.icons}
|
||||
description={branch.summary}
|
||||
title={<RuleLink {...branche} />}
|
||||
icon={icon ?? branche.icons}
|
||||
description={branche.summary}
|
||||
unit="€"
|
||||
/>
|
||||
)
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
*/
|
||||
|
||||
.situationValue {
|
||||
display: none;
|
||||
text-decoration: underline white;
|
||||
border-bottom-left-radius: 3px;
|
||||
font-weight: 400;
|
||||
|
@ -14,14 +13,6 @@
|
|||
display: flex;
|
||||
align-items: baseline;
|
||||
}
|
||||
#rule-rules .name > .situationValue {
|
||||
border-bottom: 2px solid white;
|
||||
padding-left: 0.4em;
|
||||
}
|
||||
|
||||
#rule-rules.showValues .situationValue {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.node.inlineExpression:not(.comparison) {
|
||||
padding-left: 0;
|
||||
|
@ -42,10 +33,6 @@
|
|||
text-align: right;
|
||||
}
|
||||
|
||||
#rule-rules section {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
#declenchement > ul {
|
||||
padding: 0;
|
||||
list-style-type: none;
|
||||
|
@ -67,7 +54,7 @@
|
|||
padding: 0.2em 0.8em;
|
||||
display: inline-block;
|
||||
color: white !important;
|
||||
font-weight: 500;
|
||||
font-weight: 600;
|
||||
border-bottom-right-radius: 6px;
|
||||
}
|
||||
.inlineMecanism .name {
|
||||
|
@ -122,7 +109,7 @@
|
|||
}
|
||||
|
||||
.percentage .name {
|
||||
font-weight: 500;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.inlineExpression .operator {
|
||||
|
@ -142,23 +129,15 @@
|
|||
border: 1px solid;
|
||||
max-width: 100%;
|
||||
border-radius: 3px;
|
||||
padding: 1em;
|
||||
padding-top: 1em;
|
||||
padding: 1rem;
|
||||
position: relative;
|
||||
flex: 1;
|
||||
}
|
||||
#rule-rules.showValues .mecanism {
|
||||
padding-bottom: 2em;
|
||||
}
|
||||
.mecanism-result {
|
||||
position: absolute;
|
||||
display: none;
|
||||
bottom: 0px;
|
||||
right: 0;
|
||||
}
|
||||
#rule-rules.showValues .mecanism-result {
|
||||
display: initial;
|
||||
}
|
||||
.mecanism .mecanism {
|
||||
flex: initial;
|
||||
}
|
|
@ -1,11 +1,8 @@
|
|||
import classNames from 'classnames'
|
||||
import { makeJsx } from 'Engine/evaluation'
|
||||
import { any, identity, path } from 'ramda'
|
||||
import React from 'react'
|
||||
import { Trans } from 'react-i18next'
|
||||
import './Algorithm.css'
|
||||
// The showValues prop is passed as a context. It used to be delt in CSS (not(.showValues) display: none), both coexist right now
|
||||
import { ShowValuesProvider } from './ShowValuesContext'
|
||||
|
||||
let Conditions = ({
|
||||
'rendu non applicable': disabledBy,
|
||||
|
@ -54,33 +51,32 @@ function ShowIfDisabled({ dependency }) {
|
|||
)
|
||||
}
|
||||
|
||||
export default function Algorithm({ rule, showValues }) {
|
||||
export default function Algorithm({ rule }) {
|
||||
let formula =
|
||||
rule['formule'] ||
|
||||
rule.formule ||
|
||||
(rule.category === 'variable' && rule.explanation.formule),
|
||||
displayFormula =
|
||||
formula &&
|
||||
!!Object.keys(formula).length &&
|
||||
!path(['formule', 'explanation', 'une possibilité'], rule) &&
|
||||
formula.explanation?.category !== 'number'
|
||||
!(formula.explanation.constant && rule.nodeValue)
|
||||
return (
|
||||
<div id="algorithm">
|
||||
<section id="rule-rules" className={classNames({ showValues })}>
|
||||
<ShowValuesProvider value={showValues}>
|
||||
<Conditions {...rule} />
|
||||
{displayFormula && (
|
||||
<section id="formule">
|
||||
<h2>
|
||||
<Trans>Calcul</Trans>
|
||||
</h2>
|
||||
<div style={{ display: 'flex', justifyContent: 'center' }}>
|
||||
{makeJsx(formula)}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</ShowValuesProvider>
|
||||
{makeJsx(rule['rendu non applicable'])}
|
||||
</section>
|
||||
</div>
|
||||
<>
|
||||
<Conditions {...rule} />
|
||||
{displayFormula && (
|
||||
<>
|
||||
<h2>Comment cette donnée est-elle calculée ?</h2>
|
||||
<div
|
||||
className={
|
||||
formula.explanation.constant || formula.explanation.operator
|
||||
? 'mecanism'
|
||||
: ''
|
||||
}
|
||||
>
|
||||
{makeJsx(formula)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
import classNames from 'classnames'
|
||||
import React from 'react'
|
||||
import { Trans } from 'react-i18next'
|
||||
|
||||
export default function Examples({ rule, setCurrentExample, currentExample }) {
|
||||
let { examples } = rule
|
||||
|
||||
if (!examples) return null
|
||||
return (
|
||||
<>
|
||||
<h2>
|
||||
<Trans i18nKey="examples">Exemples</Trans>{' '}
|
||||
</h2>
|
||||
<ul>
|
||||
{examples.map(ex => (
|
||||
<Example
|
||||
key={ex.nom}
|
||||
{...{ ex, rule, currentExample, setCurrentExample }}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{currentExample && (
|
||||
<button
|
||||
className="ui__ button small"
|
||||
onClick={() => setCurrentExample(null)}
|
||||
>
|
||||
<Trans i18nKey="cancelExample">Enlever l'exemple</Trans>
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
let Example = ({
|
||||
ex: { nom, situation },
|
||||
rule,
|
||||
currentExample,
|
||||
setCurrentExample
|
||||
}) => {
|
||||
let selected = currentExample && currentExample.name == nom
|
||||
return (
|
||||
<li key={nom}>
|
||||
<button
|
||||
onClick={() =>
|
||||
selected
|
||||
? setCurrentExample(null)
|
||||
: setCurrentExample(nom, situation, rule.dottedName)
|
||||
}
|
||||
className={classNames('ui__ button small', {
|
||||
selected
|
||||
})}
|
||||
>
|
||||
{nom}
|
||||
</button>
|
||||
</li>
|
||||
)
|
||||
}
|
|
@ -1,18 +1,19 @@
|
|||
import { ThemeColorsContext } from 'Components/utils/colors'
|
||||
import { SitePathsContext } from 'Components/utils/withSitePaths'
|
||||
import { SitePathsContext } from 'Components/utils/SitePathsContext'
|
||||
import React, { useContext } from 'react'
|
||||
import emoji from 'react-easy-emoji'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { DottedName } from 'Rules'
|
||||
import { parsedRulesSelector } from 'Selectors/analyseSelectors'
|
||||
import { capitalise0 } from '../../utils'
|
||||
import './Namespace.css'
|
||||
import { EngineContext } from 'Components/utils/EngineContext'
|
||||
|
||||
export default function Namespace({ dottedName }: { dottedName: DottedName }) {
|
||||
const sitePaths = useContext(SitePathsContext)
|
||||
const colors = useContext(ThemeColorsContext)
|
||||
const flatRules = useSelector(parsedRulesSelector)
|
||||
const rules = useContext(EngineContext).getParsedRules()
|
||||
|
||||
return (
|
||||
<ul id="namespace">
|
||||
{dottedName
|
||||
|
@ -27,7 +28,7 @@ export default function Namespace({ dottedName }: { dottedName: DottedName }) {
|
|||
)
|
||||
.map((fragments: string[]) => {
|
||||
let ruleName = fragments.join(' . ') as DottedName,
|
||||
rule = flatRules[ruleName]
|
||||
rule = rules[ruleName]
|
||||
if (!rule) {
|
||||
throw new Error(
|
||||
`Attention, il se peut que la règle ${ruleName}, ait été définie avec un namespace qui n'existe pas.`
|
|
@ -1,12 +1,9 @@
|
|||
.references {
|
||||
font-size: 100%;
|
||||
list-style: none;
|
||||
padding-left: 1em;
|
||||
color: #333350;
|
||||
padding: 0;
|
||||
}
|
||||
.references a {
|
||||
color: inherit;
|
||||
width: 40%;
|
||||
flex: 1;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
|
@ -20,15 +17,6 @@
|
|||
text-align: left;
|
||||
font-style: italic;
|
||||
}
|
||||
.references .url {
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
background: #333350;
|
||||
border-radius: 0.4em;
|
||||
font-style: italic;
|
||||
padding: 0.05em 0.6em;
|
||||
font-size: 95%;
|
||||
}
|
||||
.references .imageWrapper {
|
||||
width: 6rem;
|
||||
height: 3rem;
|
|
@ -23,16 +23,28 @@ function Ref({ name, link }: RefProps) {
|
|||
refData = (refKey && references[refKey]) || {},
|
||||
domain = cleanDomain(link)
|
||||
return (
|
||||
<li key={name}>
|
||||
<li
|
||||
css={`
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
`}
|
||||
key={name}
|
||||
>
|
||||
<span className="imageWrapper">
|
||||
{refData.image && (
|
||||
<img src={require('Images/références/' + refData.image)} />
|
||||
)}
|
||||
</span>
|
||||
<a href={link} target="_blank">
|
||||
<a
|
||||
href={link}
|
||||
target="_blank"
|
||||
css={`
|
||||
margin-right: 1rem;
|
||||
`}
|
||||
>
|
||||
{capitalise0(name)}
|
||||
</a>
|
||||
<span className="url">{domain}</span>
|
||||
<span className="ui__ label">{domain}</span>
|
||||
</li>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,199 @@
|
|||
import { ThemeColorsContext } from 'Components/utils/colors'
|
||||
import { EngineContext, useEvaluation } from 'Components/utils/EngineContext'
|
||||
import { SitePathsContext } from 'Components/utils/SitePathsContext'
|
||||
import { formatValue } from 'Engine/format'
|
||||
import mecanisms from 'Engine/mecanisms.yaml'
|
||||
import { serializeUnit } from 'Engine/units'
|
||||
import { filter, isEmpty } from 'ramda'
|
||||
import React, { Suspense, useContext, useState } from 'react'
|
||||
import emoji from 'react-easy-emoji'
|
||||
import { Helmet } from 'react-helmet'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { Link } from 'react-router-dom'
|
||||
import Animate from 'Ui/animate'
|
||||
import { AttachDictionary } from '../AttachDictionary'
|
||||
import RuleLink from '../RuleLink'
|
||||
import { Markdown } from '../utils/markdown'
|
||||
import Algorithm from './Algorithm'
|
||||
import Examples from './Examples'
|
||||
import RuleHeader from './Header'
|
||||
import References from './References'
|
||||
import { UseDefaultValuesContext } from './UseDefaultValuesContext'
|
||||
|
||||
let LazySource = React.lazy(() => import('./RuleSource'))
|
||||
|
||||
export default AttachDictionary(mecanisms)(function Rule({ dottedName }) {
|
||||
const [currentExample, setCurrentExample] = useState(null)
|
||||
const rules = useContext(EngineContext).getParsedRules()
|
||||
const useDefaultValues = useContext(UseDefaultValuesContext)
|
||||
const rule = useEvaluation(dottedName, { useDefaultValues })
|
||||
const [viewSource, setViewSource] = useState(false)
|
||||
const { t, i18n } = useTranslation()
|
||||
let { type, name, acronyme, title, description, question, icon } = rule,
|
||||
namespaceRules = filter(
|
||||
rule =>
|
||||
rule.dottedName.startsWith(dottedName) &&
|
||||
rule.dottedName.split(' . ').length ===
|
||||
dottedName.split(' . ').length + 1,
|
||||
rules
|
||||
)
|
||||
|
||||
const renderReferences = ({ références: refs }) =>
|
||||
refs ? (
|
||||
<div>
|
||||
<h2>
|
||||
<Trans>Références</Trans>
|
||||
</h2>
|
||||
<References refs={refs} />
|
||||
</div>
|
||||
) : null
|
||||
|
||||
return (
|
||||
<div id="rule">
|
||||
<Animate.fromBottom>
|
||||
<Helmet
|
||||
title={title}
|
||||
meta={[
|
||||
{
|
||||
name: 'description',
|
||||
content: description
|
||||
}
|
||||
]}
|
||||
/>
|
||||
<RuleHeader
|
||||
{...{
|
||||
dottedName,
|
||||
type,
|
||||
description,
|
||||
question,
|
||||
flatRule: rule,
|
||||
name,
|
||||
acronyme,
|
||||
title,
|
||||
icon
|
||||
}}
|
||||
/>
|
||||
{(rule.nodeValue || rule.defaultValue || rule.unit) && (
|
||||
<>
|
||||
<p
|
||||
className="ui__ lead card light-bg"
|
||||
css={`
|
||||
display: inline-block;
|
||||
`}
|
||||
>
|
||||
{rule.nodeValue != null && (
|
||||
<>
|
||||
{formatValue({ ...rule, language: i18n.language })}
|
||||
<br />
|
||||
</>
|
||||
)}
|
||||
{rule.defaultValue?.nodeValue != null && (
|
||||
<>
|
||||
<small>
|
||||
Valeur par défaut :{' '}
|
||||
{formatValue({
|
||||
...rule.defaultValue,
|
||||
language: i18n.language
|
||||
})}
|
||||
</small>
|
||||
<br />
|
||||
</>
|
||||
)}
|
||||
{rule.nodeValue == null && !rule.defaultValue?.unit && rule.unit && (
|
||||
<>
|
||||
<small>Unité : {serializeUnit(rule.unit)}</small>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Algorithm rule={rule} />
|
||||
|
||||
{viewSource === dottedName ? (
|
||||
<Suspense fallback={<div>Chargement du code source...</div>}>
|
||||
<LazySource dottedName={dottedName} />
|
||||
</Suspense>
|
||||
) : (
|
||||
<div
|
||||
css={`
|
||||
text-align: right;
|
||||
margin-top: 1rem;
|
||||
`}
|
||||
>
|
||||
<button
|
||||
className="ui__ small simple button"
|
||||
onClick={() => setViewSource(dottedName)}
|
||||
>
|
||||
{emoji('✍️')} {t('Afficher la description publicode')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{rule['rend non applicable'] && (
|
||||
<>
|
||||
<h3>
|
||||
<Trans>Rend non applicable les règles suivantes</Trans> :{' '}
|
||||
</h3>
|
||||
<ul>
|
||||
{rule['rend non applicable'].map(ruleName => (
|
||||
<li key={ruleName}>
|
||||
<RuleLink dottedName={ruleName} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
{rule.note && (
|
||||
<>
|
||||
<h3>Note</h3>
|
||||
<div className="ui__ notice">
|
||||
<Markdown source={rule.note} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{renderReferences(rule)}
|
||||
<Examples
|
||||
currentExample={currentExample}
|
||||
rule={rule}
|
||||
setCurrentExample={setCurrentExample}
|
||||
/>
|
||||
|
||||
{!isEmpty(namespaceRules) && (
|
||||
<NamespaceRulesList {...{ namespaceRules }} />
|
||||
)}
|
||||
</Animate.fromBottom>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
function NamespaceRulesList({ namespaceRules }) {
|
||||
const colors = useContext(ThemeColorsContext)
|
||||
const sitePaths = useContext(SitePathsContext)
|
||||
const useDefaultValues = useContext(UseDefaultValuesContext)
|
||||
return (
|
||||
<section>
|
||||
<h2>
|
||||
<Trans>Pages associées</Trans>
|
||||
</h2>
|
||||
<ul>
|
||||
{Object.values(namespaceRules).map(r => (
|
||||
<li key={r.name}>
|
||||
<Link
|
||||
style={{
|
||||
color: colors.textColorOnWhite,
|
||||
textDecoration: 'underline'
|
||||
}}
|
||||
to={{
|
||||
pathname: sitePaths.documentation.rule(r.dottedName),
|
||||
state: { useDefaultValues }
|
||||
}}
|
||||
>
|
||||
{r.title || r.name}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
import { ParsedRule } from 'Engine/types'
|
||||
import { safeDump } from 'js-yaml'
|
||||
import React from 'react'
|
||||
import emoji from 'react-easy-emoji'
|
||||
import rules from 'Rules'
|
||||
import PublicodeHighlighter from '../ui/PublicodeHighlighter'
|
||||
|
||||
|
@ -11,13 +10,15 @@ export default function RuleSource({ dottedName }: RuleSourceProps) {
|
|||
let source = rules[dottedName]
|
||||
|
||||
return (
|
||||
<div id="RuleSource" className="ui__ container">
|
||||
<h2>
|
||||
{emoji('⚙️ ')}
|
||||
Code source <br />
|
||||
<code>{dottedName}</code>
|
||||
</h2>
|
||||
<section>
|
||||
<h3>Source publicode</h3>
|
||||
<PublicodeHighlighter source={safeDump({ [dottedName]: source })} />
|
||||
</div>
|
||||
<p className="ui__ notice">
|
||||
Ci-dessus la règle d'origine, écrite en publicode. Publicode est un
|
||||
langage déclaratif développé par beta.gouv.fr en partenariat avec
|
||||
l'Acoss pour encoder les algorithmes d'intérêt public.{' '}
|
||||
<a href="https://publi.codes">En savoir plus.</a>
|
||||
</p>
|
||||
</section>
|
||||
)
|
||||
}
|
|
@ -1,12 +1,12 @@
|
|||
import SearchBar from 'Components/SearchBar'
|
||||
import React from 'react'
|
||||
import React, { useContext } from 'react'
|
||||
import { Trans } from 'react-i18next'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { parsedRulesSelector } from 'Selectors/analyseSelectors'
|
||||
import './RulesList.css'
|
||||
import { EngineContext } from 'Components/utils/EngineContext'
|
||||
|
||||
export default function RulesList() {
|
||||
const rules = useSelector(parsedRulesSelector)
|
||||
const rules = useContext(EngineContext).getParsedRules()
|
||||
return (
|
||||
<div id="RulesList" className="ui__ container">
|
||||
<h1>
|
|
@ -0,0 +1,3 @@
|
|||
import { createContext } from 'react'
|
||||
|
||||
export const UseDefaultValuesContext = createContext<boolean>(true)
|
|
@ -0,0 +1,31 @@
|
|||
import RulePage from 'Components/RulePage'
|
||||
import { EngineProvider } from 'Components/utils/EngineContext'
|
||||
import Engine from 'Engine'
|
||||
import React from 'react'
|
||||
import { Route, Switch } from 'react-router'
|
||||
import { DottedName } from 'Rules'
|
||||
import RulesList from './RulesList'
|
||||
import { UseDefaultValuesContext } from './UseDefaultValuesContext'
|
||||
|
||||
type DocumentationProps<Names extends string> = {
|
||||
basePath: string
|
||||
engine: Engine<Names>
|
||||
useDefaultValues?: boolean
|
||||
}
|
||||
|
||||
export default function Documentation({
|
||||
basePath,
|
||||
engine,
|
||||
useDefaultValues = false
|
||||
}: DocumentationProps<DottedName>) {
|
||||
return (
|
||||
<EngineProvider value={engine}>
|
||||
<UseDefaultValuesContext.Provider value={useDefaultValues}>
|
||||
<Switch>
|
||||
<Route exact path={basePath} component={RulesList} />
|
||||
<Route path={basePath + '/:name+'} component={RulePage} />
|
||||
</Switch>
|
||||
</UseDefaultValuesContext.Provider>
|
||||
</EngineProvider>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
import Engine from 'Engine'
|
||||
import { formatValue } from 'Engine/format'
|
||||
import { EvaluatedNode } from 'Engine/types'
|
||||
import React, { useContext } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { DottedName } from 'Rules'
|
||||
import { coerceArray } from '../utils'
|
||||
import RuleLink from './RuleLink'
|
||||
import { EngineContext } from './utils/EngineContext'
|
||||
|
||||
export type ValueProps = {
|
||||
expression: string
|
||||
unit?: string
|
||||
displayedUnit?: string
|
||||
precision?: number
|
||||
engine?: Engine<DottedName>
|
||||
linkToRule?: boolean
|
||||
} & React.HTMLProps<HTMLSpanElement>
|
||||
|
||||
export default function Value({
|
||||
expression,
|
||||
unit,
|
||||
displayedUnit,
|
||||
precision,
|
||||
engine,
|
||||
linkToRule = true,
|
||||
...props
|
||||
}: ValueProps) {
|
||||
const { language } = useTranslation().i18n
|
||||
if (expression === null) {
|
||||
throw new TypeError('expression cannot be null')
|
||||
}
|
||||
const evaluation = (engine ?? useContext(EngineContext)).evaluate(
|
||||
expression,
|
||||
{ unit }
|
||||
)
|
||||
const nodeValue = evaluation.nodeValue
|
||||
const value = formatValue({
|
||||
nodeValue,
|
||||
unit:
|
||||
displayedUnit ?? (evaluation as EvaluatedNode<DottedName, number>).unit,
|
||||
language,
|
||||
precision
|
||||
})
|
||||
if ('dottedName' in evaluation && linkToRule) {
|
||||
return (
|
||||
<RuleLink {...evaluation}>
|
||||
<span {...props}>{value}</span>
|
||||
</RuleLink>
|
||||
)
|
||||
}
|
||||
return <span {...props}>{value}</span>
|
||||
}
|
||||
|
||||
type ConditionProps = {
|
||||
expression: string | string[]
|
||||
children: React.ReactNode
|
||||
}
|
||||
export function Condition({ expression, children }: ConditionProps) {
|
||||
const engine = useContext(EngineContext)
|
||||
if (!coerceArray(expression).every(expr => engine.evaluate(expr).nodeValue)) {
|
||||
return null
|
||||
}
|
||||
return <>{children}</>
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
ul#mecanisms {
|
||||
margin: 3em auto;
|
||||
}
|
||||
|
||||
#mecanisms .warning {
|
||||
color: #e74c3c;
|
||||
}
|
|
@ -3,7 +3,7 @@ import emoji from 'react-easy-emoji'
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
import { icons } from './ui/SocialIcon'
|
||||
import { SitePathsContext } from './utils/withSitePaths'
|
||||
import { SitePathsContext } from './utils/SitePathsContext'
|
||||
|
||||
export default function MoreInfosOnUs() {
|
||||
const { pathname } = useLocation()
|
||||
|
|
|
@ -31,6 +31,9 @@
|
|||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.payslip__container h5 {
|
||||
margin: 0;
|
||||
}
|
||||
.payslip__container h5 {
|
||||
margin: 0;
|
||||
}
|
||||
|
@ -41,6 +44,13 @@
|
|||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.payslip__container span {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.payslip__cotisationsSection h4:not(:first-child) {
|
||||
text-align: right;
|
||||
}
|
||||
|
|
|
@ -1,28 +1,66 @@
|
|||
import { ThemeColorsContext } from 'Components/utils/colors'
|
||||
import Value from 'Components/Value'
|
||||
import { getRuleFromAnalysis } from 'Engine/ruleUtils'
|
||||
import { useEvaluation, EngineContext } from 'Components/utils/EngineContext'
|
||||
import Value from 'Components/EngineValue'
|
||||
import { formatValue } from 'Engine/format'
|
||||
import React, { Fragment, useContext } from 'react'
|
||||
import { Trans } from 'react-i18next'
|
||||
import { useSelector } from 'react-redux'
|
||||
import {
|
||||
analysisWithDefaultsSelector,
|
||||
parsedRulesSelector
|
||||
} from 'Selectors/analyseSelectors'
|
||||
import { analysisToCotisationsSelector } from 'Selectors/ficheDePaieSelectors'
|
||||
import { DottedName } from 'Rules'
|
||||
import './PaySlip.css'
|
||||
import { Line, SalaireBrutSection, SalaireNetSection } from './PaySlipSections'
|
||||
import RuleLink from './RuleLink'
|
||||
|
||||
export default function PaySlip() {
|
||||
const { lightestColor } = useContext(ThemeColorsContext)
|
||||
const cotisations = useSelector(analysisToCotisationsSelector)
|
||||
const analysis = useSelector(analysisWithDefaultsSelector)
|
||||
const parsedRules = useSelector(parsedRulesSelector)
|
||||
let getRule = getRuleFromAnalysis(analysis)
|
||||
export const SECTION_ORDER = [
|
||||
'protection sociale . santé',
|
||||
'protection sociale . accidents du travail et maladies professionnelles',
|
||||
'protection sociale . retraite',
|
||||
'protection sociale . famille',
|
||||
'protection sociale . assurance chômage',
|
||||
'protection sociale . formation',
|
||||
'protection sociale . transport',
|
||||
'protection sociale . autres'
|
||||
] as const
|
||||
|
||||
const heuresSupplémentaires = getRule(
|
||||
'contrat salarié . temps de travail . heures supplémentaires'
|
||||
)
|
||||
type Section = typeof SECTION_ORDER[number]
|
||||
|
||||
function getSection(rule): Section {
|
||||
const section = ('protection sociale . ' +
|
||||
(rule.cotisation?.branche ?? rule.taxe?.branche)) as Section
|
||||
if (SECTION_ORDER.includes(section)) {
|
||||
return section
|
||||
}
|
||||
return 'protection sociale . autres'
|
||||
}
|
||||
|
||||
export function getCotisationsBySection(
|
||||
parsedRules
|
||||
): Array<[Section, DottedName[]]> {
|
||||
const cotisations = [
|
||||
...parsedRules['contrat salarié . cotisations . patronales'].formule
|
||||
.explanation.explanation,
|
||||
...parsedRules['contrat salarié . cotisations . salariales'].formule
|
||||
.explanation.explanation
|
||||
]
|
||||
.map(cotisation => cotisation.dottedName)
|
||||
.filter(Boolean)
|
||||
.reduce((acc, cotisation) => {
|
||||
const sectionName = getSection(parsedRules[cotisation])
|
||||
return {
|
||||
...acc,
|
||||
[sectionName]: (acc[sectionName] ?? new Set()).add(cotisation)
|
||||
}
|
||||
}, {}) as Record<Section, Set<DottedName>>
|
||||
|
||||
return Object.entries(cotisations)
|
||||
.map(([section, dottedNames]) => [section, [...dottedNames.values()]])
|
||||
.sort(
|
||||
([a], [b]) =>
|
||||
SECTION_ORDER.indexOf(a as Section) -
|
||||
SECTION_ORDER.indexOf(b as Section)
|
||||
) as Array<[Section, DottedName[]]>
|
||||
}
|
||||
|
||||
export default function PaySlip() {
|
||||
const parsedRules = useContext(EngineContext).getParsedRules()
|
||||
const cotisationsBySection = getCotisationsBySection(parsedRules)
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -38,20 +76,18 @@ export default function PaySlip() {
|
|||
>
|
||||
<div className="payslip__salarySection">
|
||||
<Line
|
||||
rule={getRule('contrat salarié . temps de travail')}
|
||||
unit="heures/mois"
|
||||
maximumFractionDigits={1}
|
||||
rule="contrat salarié . temps de travail"
|
||||
displayedUnit="heures/mois"
|
||||
precision={1}
|
||||
/>
|
||||
<Line
|
||||
rule="contrat salarié . temps de travail . heures supplémentaires"
|
||||
displayedUnit="heures/mois"
|
||||
precision={1}
|
||||
/>
|
||||
{!!heuresSupplémentaires?.nodeValue && (
|
||||
<Line
|
||||
rule={heuresSupplémentaires}
|
||||
unit="heures/mois"
|
||||
maximumFractionDigits={1}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<SalaireBrutSection getRule={getRule} />
|
||||
<SalaireBrutSection />
|
||||
{/* Section cotisations */}
|
||||
<div className="payslip__cotisationsSection">
|
||||
<h4>
|
||||
|
@ -63,34 +99,15 @@ export default function PaySlip() {
|
|||
<h4>
|
||||
<Trans>Part salarié</Trans>
|
||||
</h4>
|
||||
{cotisations.map(([brancheDottedName, cotisationList]) => {
|
||||
let branche = parsedRules[brancheDottedName]
|
||||
{cotisationsBySection.map(([sectionDottedName, cotisations]) => {
|
||||
let section = parsedRules[sectionDottedName]
|
||||
return (
|
||||
<Fragment key={branche.dottedName}>
|
||||
<Fragment key={section.dottedName}>
|
||||
<h5 className="payslip__cotisationTitle">
|
||||
<RuleLink {...branche} />
|
||||
<RuleLink {...section} />
|
||||
</h5>
|
||||
{cotisationList.map(cotisation => (
|
||||
<Fragment key={cotisation.dottedName}>
|
||||
<RuleLink
|
||||
style={{ backgroundColor: lightestColor }}
|
||||
{...cotisation}
|
||||
/>
|
||||
<Value
|
||||
nilValueSymbol="—"
|
||||
unit="€"
|
||||
customCSS="background-color: var(--lightestColor)"
|
||||
>
|
||||
{cotisation.montant.partPatronale}
|
||||
</Value>
|
||||
<Value
|
||||
nilValueSymbol="—"
|
||||
unit="€"
|
||||
customCSS="background-color: var(--lightestColor)"
|
||||
>
|
||||
{cotisation.montant.partSalariale}
|
||||
</Value>
|
||||
</Fragment>
|
||||
{cotisations.map(cotisation => (
|
||||
<Cotisation key={cotisation} dottedName={cotisation} />
|
||||
))}
|
||||
</Fragment>
|
||||
)
|
||||
|
@ -101,23 +118,57 @@ export default function PaySlip() {
|
|||
<Trans>Total des retenues</Trans>
|
||||
</div>
|
||||
<Value
|
||||
nilValueSymbol="—"
|
||||
{...getRule('contrat salarié . cotisations . patronales')}
|
||||
unit="€"
|
||||
expression="contrat salarié . cotisations . patronales"
|
||||
displayedUnit="€"
|
||||
className="payslip__total"
|
||||
/>
|
||||
<Value
|
||||
nilValueSymbol="—"
|
||||
{...getRule('contrat salarié . cotisations . salariales')}
|
||||
unit="€"
|
||||
expression="contrat salarié . cotisations . salariales"
|
||||
displayedUnit="€"
|
||||
className="payslip__total"
|
||||
/>
|
||||
{/* Salaire chargé */}
|
||||
<Line rule={getRule('contrat salarié . rémunération . total')} />
|
||||
<Line rule="contrat salarié . rémunération . total" />
|
||||
<span />
|
||||
</div>
|
||||
{/* Section salaire net */}
|
||||
<SalaireNetSection getRule={getRule} />
|
||||
<SalaireNetSection />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Cotisation({ dottedName }: { dottedName: DottedName }) {
|
||||
const parsedRules = useContext(EngineContext).getParsedRules()
|
||||
|
||||
const partSalariale = useEvaluation(
|
||||
'contrat salarié . cotisations . salariales'
|
||||
)?.formule.explanation.explanation.find(
|
||||
cotisation => cotisation.dottedName === dottedName
|
||||
)
|
||||
const partPatronale = useEvaluation(
|
||||
'contrat salarié . cotisations . patronales'
|
||||
)?.formule.explanation.explanation.find(
|
||||
cotisation => cotisation.dottedName === dottedName
|
||||
)
|
||||
if (!partPatronale?.nodeValue && !partSalariale?.nodeValue) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<RuleLink
|
||||
{...parsedRules[dottedName]}
|
||||
style={{ backgroundColor: 'var(--lightestColor)' }}
|
||||
/>
|
||||
<span style={{ backgroundColor: 'var(--lightestColor)' }}>
|
||||
{partPatronale?.nodeValue
|
||||
? formatValue({ ...partPatronale, unit: '€' })
|
||||
: '–'}
|
||||
</span>
|
||||
<span style={{ backgroundColor: 'var(--lightestColor)' }}>
|
||||
{partSalariale?.nodeValue
|
||||
? formatValue({ ...partSalariale, unit: '€' })
|
||||
: '–'}
|
||||
</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,125 +1,94 @@
|
|||
import Value from 'Components/Value'
|
||||
import { EvaluatedRule } from 'Engine/types'
|
||||
import React from 'react'
|
||||
import { EngineContext } from 'Components/utils/EngineContext'
|
||||
import Value, { ValueProps, Condition } from 'Components/EngineValue'
|
||||
import React, { useContext } from 'react'
|
||||
import { Trans } from 'react-i18next'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { DottedName } from 'Rules'
|
||||
import { defaultUnitSelector } from 'Selectors/analyseSelectors'
|
||||
import { coerceArray } from '../utils'
|
||||
import RuleLink from './RuleLink'
|
||||
|
||||
export let SalaireBrutSection = ({
|
||||
getRule
|
||||
}: {
|
||||
getRule: (rule: DottedName) => EvaluatedRule
|
||||
}) => {
|
||||
let avantagesEnNature = getRule(
|
||||
'contrat salarié . rémunération . avantages en nature'
|
||||
),
|
||||
indemnitésSalarié = getRule('contrat salarié . CDD . indemnités salarié'),
|
||||
remboursementDeFrais = getRule('contrat salarié . frais professionnels'),
|
||||
heuresSupplémentaires = getRule(
|
||||
'contrat salarié . rémunération . heures supplémentaires'
|
||||
),
|
||||
salaireDeBase = getRule('contrat salarié . rémunération . brut de base'),
|
||||
rémunérationBrute = getRule('contrat salarié . rémunération . brut'),
|
||||
chômagePartielIndemnité = getRule(
|
||||
'contrat salarié . activité partielle . indemnités'
|
||||
),
|
||||
chômagePartielAbsence = getRule(
|
||||
'contrat salarié . activité partielle . retrait absence'
|
||||
),
|
||||
primes = getRule('contrat salarié . rémunération . primes')
|
||||
export let SalaireBrutSection = () => {
|
||||
return (
|
||||
<div className="payslip__salarySection">
|
||||
<h4 className="payslip__salaryTitle">
|
||||
<Trans>Salaire</Trans>
|
||||
</h4>
|
||||
<Line rule={salaireDeBase} />
|
||||
{!!avantagesEnNature?.nodeValue && (
|
||||
<Line
|
||||
rule={getRule(
|
||||
'contrat salarié . rémunération . avantages en nature . montant'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{chômagePartielIndemnité?.nodeValue && (
|
||||
<>
|
||||
<Line rule={chômagePartielAbsence} />
|
||||
<Line rule={chômagePartielIndemnité} />
|
||||
</>
|
||||
)}
|
||||
{!!heuresSupplémentaires?.nodeValue && (
|
||||
<Line rule={heuresSupplémentaires} />
|
||||
)}
|
||||
{!!primes?.nodeValue && <Line rule={primes} />}
|
||||
{!!remboursementDeFrais?.nodeValue && (
|
||||
<Line rule={remboursementDeFrais} />
|
||||
)}
|
||||
{!!indemnitésSalarié?.nodeValue && <Line rule={indemnitésSalarié} />}
|
||||
{rémunérationBrute.nodeValue !== salaireDeBase.nodeValue && (
|
||||
<Line rule={rémunérationBrute} />
|
||||
)}
|
||||
<Line rule="contrat salarié . rémunération . brut de base" />
|
||||
<Line rule="contrat salarié . rémunération . avantages en nature . montant" />
|
||||
<Line rule="contrat salarié . activité partielle . retrait absence" />
|
||||
<Line rule="contrat salarié . activité partielle . indemnités" />
|
||||
<Line rule="contrat salarié . rémunération . heures supplémentaires" />
|
||||
<Line rule="contrat salarié . rémunération . heures complémentaires" />
|
||||
<Line rule="contrat salarié . rémunération . primes" />
|
||||
<Line rule="contrat salarié . frais professionnels" />
|
||||
<Line rule="contrat salarié . CDD . indemnités salarié" />
|
||||
<Condition expression="contrat salarié . rémunération . brut de base != contrat salarié . rémunération . brut">
|
||||
<Line rule="contrat salarié . rémunération . brut" />
|
||||
</Condition>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export let Line = ({ rule, className = '', ...props }) => {
|
||||
const defaultUnit = useSelector(defaultUnitSelector)
|
||||
return (
|
||||
<>
|
||||
<RuleLink {...rule} className={className} />
|
||||
<Value
|
||||
{...rule}
|
||||
nilValueSymbol="—"
|
||||
defaultUnit={defaultUnit}
|
||||
unit="€"
|
||||
className={className}
|
||||
{...props}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export let SalaireNetSection = ({ getRule }) => {
|
||||
let avantagesEnNature = getRule(
|
||||
'contrat salarié . rémunération . avantages en nature . montant'
|
||||
)
|
||||
let impôt = getRule('impôt')
|
||||
let netImposable = getRule('contrat salarié . rémunération . net imposable')
|
||||
const retenueTitresRestaurant = getRule(
|
||||
'contrat salarié . frais professionnels . titres-restaurant . montant'
|
||||
)
|
||||
export let SalaireNetSection = () => {
|
||||
return (
|
||||
<div className="payslip__salarySection">
|
||||
<h4 className="payslip__salaryTitle">
|
||||
<Trans>Salaire net</Trans>
|
||||
</h4>
|
||||
{netImposable && <Line rule={netImposable} />}
|
||||
{(avantagesEnNature?.nodeValue || retenueTitresRestaurant?.nodeValue) && (
|
||||
<Line
|
||||
rule={getRule('contrat salarié . rémunération . net de cotisations')}
|
||||
/>
|
||||
)}
|
||||
{!!avantagesEnNature?.nodeValue && (
|
||||
<Line negative rule={avantagesEnNature} />
|
||||
)}
|
||||
{!!retenueTitresRestaurant?.nodeValue && (
|
||||
<Line negative rule={retenueTitresRestaurant} />
|
||||
)}
|
||||
|
||||
<Line rule="contrat salarié . rémunération . net imposable" />
|
||||
<Condition
|
||||
expression={[
|
||||
'contrat salarié . rémunération . avantages en nature',
|
||||
'contrat salarié . frais professionnels . titres-restaurant'
|
||||
]}
|
||||
>
|
||||
<Line rule="contrat salarié . rémunération . net de cotisations" />
|
||||
</Condition>
|
||||
<Line
|
||||
rule={getRule('contrat salarié . rémunération . net')}
|
||||
negative
|
||||
rule="contrat salarié . rémunération . avantages en nature . montant"
|
||||
/>
|
||||
<Line
|
||||
negative
|
||||
rule="contrat salarié . frais professionnels . titres-restaurant . montant"
|
||||
/>
|
||||
<Line
|
||||
rule="contrat salarié . rémunération . net"
|
||||
className="payslip__total"
|
||||
/>
|
||||
{!!impôt && (
|
||||
<>
|
||||
<Line negative rule={impôt} />
|
||||
<Line
|
||||
className="payslip__total"
|
||||
rule={getRule('contrat salarié . rémunération . net après impôt')}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Condition expression="impôt">
|
||||
<Line negative rule="impôt" unit="€/mois" />
|
||||
<Line
|
||||
className="payslip__total"
|
||||
rule="contrat salarié . rémunération . net après impôt"
|
||||
/>
|
||||
</Condition>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type LineProps = {
|
||||
rule: DottedName
|
||||
negative?: boolean
|
||||
} & Omit<ValueProps, 'expression'>
|
||||
|
||||
export function Line({
|
||||
rule,
|
||||
displayedUnit = '€',
|
||||
negative = false,
|
||||
className,
|
||||
...props
|
||||
}: LineProps) {
|
||||
const parsedRules = useContext(EngineContext).getParsedRules()
|
||||
return (
|
||||
<Condition expression={rule}>
|
||||
<RuleLink {...parsedRules[rule]} className={className} />
|
||||
<Value
|
||||
linkToRule={false}
|
||||
expression={(negative ? '- ' : '') + rule}
|
||||
displayedUnit={displayedUnit}
|
||||
className={className}
|
||||
{...props}
|
||||
/>
|
||||
</Condition>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { formatValue } from 'Engine/format'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { debounce as debounceFn } from '../utils'
|
||||
import './PercentageField.css'
|
||||
|
||||
|
@ -9,6 +10,7 @@ export default function PercentageField({ onChange, value, debounce = 0 }) {
|
|||
debounce ? debounceFn(debounce, onChange) : onChange,
|
||||
[debounce, onChange]
|
||||
)
|
||||
const language = useTranslation().i18n.language
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
@ -28,7 +30,8 @@ export default function PercentageField({ onChange, value, debounce = 0 }) {
|
|||
/>
|
||||
<span style={{ display: 'inline-block', width: '3em' }}>
|
||||
{formatValue({
|
||||
value: localValue,
|
||||
nodeValue: localValue,
|
||||
language,
|
||||
unit: '%'
|
||||
})}
|
||||
</span>
|
||||
|
|
|
@ -3,13 +3,13 @@ import { parseUnit, serializeUnit } from 'Engine/units'
|
|||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { defaultUnitSelector } from 'Selectors/analyseSelectors'
|
||||
import { targetUnitSelector } from 'Selectors/simulationSelectors'
|
||||
import './PeriodSwitch.css'
|
||||
|
||||
export default function PeriodSwitch() {
|
||||
const dispatch = useDispatch()
|
||||
const language = useTranslation().i18n.language
|
||||
const currentUnit = useSelector(defaultUnitSelector)
|
||||
const currentUnit = useSelector(targetUnitSelector)
|
||||
|
||||
let units = ['€/mois', '€/an']
|
||||
return (
|
||||
|
|
|
@ -3,15 +3,15 @@ import React from 'react'
|
|||
import { Trans } from 'react-i18next'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { RootState } from 'Reducers/rootReducer'
|
||||
import { noUserInputSelector } from 'Selectors/analyseSelectors'
|
||||
import { LinkButton } from 'Ui/Button'
|
||||
import Banner from './Banner'
|
||||
import { firstStepCompletedSelector } from 'Selectors/simulationSelectors'
|
||||
|
||||
export default function PreviousSimulationBanner() {
|
||||
const previousSimulation = useSelector(
|
||||
(state: RootState) => state.previousSimulation
|
||||
)
|
||||
const newSimulationStarted = !useSelector(noUserInputSelector)
|
||||
const newSimulationStarted = useSelector(firstStepCompletedSelector)
|
||||
const dispatch = useDispatch()
|
||||
|
||||
return (
|
||||
|
|
|
@ -5,20 +5,20 @@ import { Trans } from 'react-i18next'
|
|||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { RootState } from 'Reducers/rootReducer'
|
||||
import { DottedName } from 'Rules'
|
||||
|
||||
import { useNextQuestions } from './utils/useNextQuestion'
|
||||
import {
|
||||
currentQuestionSelector,
|
||||
nextStepsSelector
|
||||
} from 'Selectors/analyseSelectors'
|
||||
answeredQuestionsSelector,
|
||||
currentQuestionSelector
|
||||
} from 'Selectors/simulationSelectors'
|
||||
|
||||
export default function QuickLinks() {
|
||||
const currentQuestion = useSelector(currentQuestionSelector)
|
||||
const nextSteps = useSelector(nextStepsSelector)
|
||||
const nextSteps = useNextQuestions()
|
||||
const quickLinks = useSelector(
|
||||
(state: RootState) => state.simulation?.config.questions?.["à l'affiche"]
|
||||
)
|
||||
const quickLinksToHide = useSelector(
|
||||
(state: RootState) => state.simulation?.foldedSteps || []
|
||||
)
|
||||
const quickLinksToHide = useSelector(answeredQuestionsSelector)
|
||||
const dispatch = useDispatch()
|
||||
|
||||
if (!quickLinks) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { ThemeColorsContext } from 'Components/utils/colors'
|
||||
import { SitePathsContext } from 'Components/utils/withSitePaths'
|
||||
import { SitePathsContext } from 'Components/utils/SitePathsContext'
|
||||
import { nameLeaf } from 'Engine/ruleUtils'
|
||||
import { ParsedRule } from 'Engine/types'
|
||||
import React, { useContext } from 'react'
|
||||
|
@ -25,10 +25,14 @@ export default function RuleLink({
|
|||
const sitePaths = useContext(SitePathsContext)
|
||||
const { color } = useContext(ThemeColorsContext)
|
||||
const newPath = sitePaths.documentation.rule(dottedName)
|
||||
|
||||
return (
|
||||
<Link
|
||||
to={newPath}
|
||||
to={{
|
||||
pathname: newPath,
|
||||
state: {
|
||||
useDefaultValues: true
|
||||
}
|
||||
}}
|
||||
title={title}
|
||||
style={{ color, ...style }}
|
||||
className={className}
|
||||
|
|
|
@ -6,10 +6,3 @@
|
|||
margin-bottom: 0.6rem;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
#situationBranch {
|
||||
color: white;
|
||||
background: #333;
|
||||
border-radius: 0.3em;
|
||||
padding: 0.2em 0.6em;
|
||||
}
|
||||
|
|
|
@ -1,34 +1,30 @@
|
|||
import { goBackToSimulation } from 'Actions/actions'
|
||||
import { ScrollToTop } from 'Components/utils/Scroll'
|
||||
import { decodeRuleName } from 'Engine/ruleUtils'
|
||||
import React from 'react'
|
||||
import React, { useContext } from 'react'
|
||||
import { Trans } from 'react-i18next'
|
||||
import { connect, useSelector } from 'react-redux'
|
||||
import { Redirect, useParams } from 'react-router-dom'
|
||||
import { DottedName } from 'Rules'
|
||||
import {
|
||||
noUserInputSelector,
|
||||
parsedRulesSelector,
|
||||
situationBranchNameSelector
|
||||
} from 'Selectors/analyseSelectors'
|
||||
import Rule from './rule/Rule'
|
||||
|
||||
import Rule from './Documentation/Rule'
|
||||
import './RulePage.css'
|
||||
import SearchButton from './SearchButton'
|
||||
import { EngineContext } from './utils/EngineContext'
|
||||
import { firstStepCompletedSelector } from 'Selectors/simulationSelectors'
|
||||
|
||||
export default function RulePage() {
|
||||
const parsedRules = useSelector(parsedRulesSelector)
|
||||
const brancheName = useSelector(situationBranchNameSelector)
|
||||
const valuesToShow = !useSelector(noUserInputSelector)
|
||||
const parsedRules = useContext(EngineContext).getParsedRules()
|
||||
const valuesToShow = useSelector(firstStepCompletedSelector)
|
||||
const { name } = useParams()
|
||||
const decodedRuleName = decodeRuleName(name ?? '')
|
||||
|
||||
const renderRule = (dottedName: DottedName) => {
|
||||
return (
|
||||
<div id="RulePage">
|
||||
<ScrollToTop key={brancheName + dottedName} />
|
||||
<ScrollToTop key={dottedName} />
|
||||
<div className="rule-page__header">
|
||||
{valuesToShow ? <BackToSimulation /> : <span />}
|
||||
{brancheName && <span id="situationBranch">{brancheName}</span>}
|
||||
<SearchButton />
|
||||
</div>
|
||||
<Rule dottedName={dottedName} />
|
||||
|
|
|
@ -2,29 +2,20 @@ import Distribution from 'Components/Distribution'
|
|||
import PaySlip from 'Components/PaySlip'
|
||||
import StackedBarChart from 'Components/StackedBarChart'
|
||||
import { ThemeColorsContext } from 'Components/utils/colors'
|
||||
import { getRuleFromAnalysis } from 'Engine/ruleUtils'
|
||||
import { useEvaluation, useInversionFail } from 'Components/utils/EngineContext'
|
||||
import React, { useContext, useRef } from 'react'
|
||||
import emoji from 'react-easy-emoji'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { RootState } from 'Reducers/rootReducer'
|
||||
import {
|
||||
analysisWithDefaultsSelector,
|
||||
defaultUnitSelector
|
||||
} from 'Selectors/analyseSelectors'
|
||||
import * as Animate from 'Ui/animate'
|
||||
import { answeredQuestionsSelector } from 'Selectors/simulationSelectors'
|
||||
|
||||
export default function SalaryExplanation() {
|
||||
const showDistributionFirst = useSelector(
|
||||
(state: RootState) => !state.simulation?.foldedSteps.length
|
||||
)
|
||||
const analysis = useSelector(analysisWithDefaultsSelector)
|
||||
const inversionFail = analysis?.cache._meta.inversionFail
|
||||
const showDistributionFirst = !useSelector(answeredQuestionsSelector).length
|
||||
const distributionRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// We can't provide an explanation if the engine has failed to run the
|
||||
// simulation.
|
||||
if (inversionFail) {
|
||||
if (useInversionFail()) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
|
@ -83,11 +74,16 @@ export default function SalaryExplanation() {
|
|||
}
|
||||
|
||||
function RevenueRepatitionSection() {
|
||||
const analysis = useSelector(analysisWithDefaultsSelector)
|
||||
const getRule = getRuleFromAnalysis(analysis)
|
||||
const { t } = useTranslation()
|
||||
const { palettes } = useContext(ThemeColorsContext)
|
||||
|
||||
const data = useEvaluation(
|
||||
[
|
||||
'contrat salarié . rémunération . net après impôt',
|
||||
'impôt',
|
||||
'contrat salarié . cotisations'
|
||||
],
|
||||
{ unit: '€/mois' }
|
||||
)
|
||||
return (
|
||||
<section>
|
||||
<h2>
|
||||
|
@ -96,18 +92,17 @@ function RevenueRepatitionSection() {
|
|||
<StackedBarChart
|
||||
data={[
|
||||
{
|
||||
...getRule('contrat salarié . rémunération . net après impôt'),
|
||||
...data[0],
|
||||
title: t('Revenu disponible'),
|
||||
color: palettes[0][0]
|
||||
},
|
||||
{
|
||||
...getRule('impôt'),
|
||||
...data[1],
|
||||
title: t('impôt'),
|
||||
color: palettes[1][0]
|
||||
},
|
||||
{
|
||||
...getRule('contrat salarié . cotisations'),
|
||||
|
||||
...data[2],
|
||||
color: palettes[1][1]
|
||||
}
|
||||
]}
|
||||
|
@ -117,15 +112,10 @@ function RevenueRepatitionSection() {
|
|||
}
|
||||
|
||||
function PaySlipSection() {
|
||||
const unit = useSelector(defaultUnitSelector)
|
||||
return (
|
||||
<section>
|
||||
<h2>
|
||||
{unit?.endsWith('mois') ? (
|
||||
<Trans>Fiche de paie</Trans>
|
||||
) : (
|
||||
<Trans>Détail annuel des cotisations</Trans>
|
||||
)}
|
||||
<Trans>Fiche de paie</Trans>
|
||||
</h2>
|
||||
<PaySlip />
|
||||
</section>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { setSimulationConfig, setSituationBranch } from 'Actions/actions'
|
||||
import { setSimulationConfig } from 'Actions/actions'
|
||||
import {
|
||||
defineDirectorStatus,
|
||||
isAutoentrepreneur
|
||||
|
@ -6,34 +6,24 @@ import {
|
|||
import classnames from 'classnames'
|
||||
import Conversation from 'Components/conversation/Conversation'
|
||||
import SeeAnswersButton from 'Components/conversation/SeeAnswersButton'
|
||||
import PeriodSwitch from 'Components/PeriodSwitch'
|
||||
import ComparaisonConfig from 'Components/simulationConfigs/rémunération-dirigeant.yaml'
|
||||
import { SitePathsContext } from 'Components/utils/withSitePaths'
|
||||
import Value from 'Components/Value'
|
||||
import { getRuleFromAnalysis } from 'Engine/ruleUtils'
|
||||
import Value from 'Components/EngineValue'
|
||||
import dirigeantComparaison from 'Components/simulationConfigs/rémunération-dirigeant.yaml'
|
||||
import Engine from 'Engine'
|
||||
import revenusSVG from 'Images/revenus.svg'
|
||||
import { default as React, useCallback, useContext, useState } from 'react'
|
||||
import {
|
||||
default as React,
|
||||
useCallback,
|
||||
useContext,
|
||||
useMemo,
|
||||
useState
|
||||
} from 'react'
|
||||
import emoji from 'react-easy-emoji'
|
||||
import { Trans } from 'react-i18next'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { RootState } from 'Reducers/rootReducer'
|
||||
import { DottedName } from 'Rules'
|
||||
import {
|
||||
analysisWithDefaultsSelector,
|
||||
branchAnalyseSelector
|
||||
} from 'Selectors/analyseSelectors'
|
||||
import Animate from 'Ui/animate'
|
||||
import { situationSelector } from 'Selectors/simulationSelectors'
|
||||
import InfoBulle from 'Ui/InfoBulle'
|
||||
import './SchemeComparaison.css'
|
||||
|
||||
let getBranchIndex = (branch: string) =>
|
||||
({ assimilé: 0, indépendant: 1, 'auto-entrepreneur': 2 }[branch])
|
||||
|
||||
let getRuleFrom = analyses => (branch: string, dottedName: DottedName) => {
|
||||
let i = getBranchIndex(branch)
|
||||
return getRuleFromAnalysis(analyses[i])(dottedName)
|
||||
}
|
||||
import { EngineContext } from './utils/EngineContext'
|
||||
|
||||
type SchemeComparaisonProps = {
|
||||
hideAutoEntrepreneur?: boolean
|
||||
|
@ -45,27 +35,50 @@ export default function SchemeComparaison({
|
|||
hideAssimiléSalarié = false
|
||||
}: SchemeComparaisonProps) {
|
||||
const dispatch = useDispatch()
|
||||
dispatch(setSimulationConfig(ComparaisonConfig))
|
||||
|
||||
const analyses = useSelector(analysisWithDefaultsSelector)
|
||||
const plafondAutoEntrepreneurDépassé = useSelector((state: RootState) =>
|
||||
branchAnalyseSelector(state, {
|
||||
situationBranchName: 'Auto-entrepreneur'
|
||||
}).controls?.find(
|
||||
dispatch(setSimulationConfig(dirigeantComparaison))
|
||||
const plafondAutoEntrepreneurDépassé = useContext(EngineContext)
|
||||
.controls()
|
||||
.find(
|
||||
({ test }) =>
|
||||
test.includes && test.includes('base des cotisations > plafond')
|
||||
)
|
||||
)
|
||||
|
||||
let getRule = getRuleFrom(analyses)
|
||||
const [showMore, setShowMore] = useState(false)
|
||||
const [conversationStarted, setConversationStarted] = useState(
|
||||
!!getRule('assimilé', 'revenu net après impôt')?.nodeValue
|
||||
!!Object.keys(useSelector(situationSelector)).length
|
||||
)
|
||||
const startConversation = useCallback(() => setConversationStarted(true), [
|
||||
setConversationStarted
|
||||
])
|
||||
|
||||
const parsedRules = useContext(EngineContext).getParsedRules()
|
||||
const situation = useSelector(situationSelector)
|
||||
const displayResult =
|
||||
useSelector(situationSelector)['entreprise . charges'] != undefined
|
||||
const assimiléEngine = useMemo(
|
||||
() =>
|
||||
new Engine(parsedRules).setSituation({
|
||||
...situation,
|
||||
dirigeant: "'assimilé salarié'"
|
||||
}),
|
||||
[situation]
|
||||
)
|
||||
const autoEntrepreneurEngine = useMemo(
|
||||
() =>
|
||||
new Engine(parsedRules).setSituation({
|
||||
...situation,
|
||||
dirigeant: "'auto-entrepreneur'"
|
||||
}),
|
||||
[situation]
|
||||
)
|
||||
const indépendantEngine = useMemo(
|
||||
() =>
|
||||
new Engine(parsedRules).setSituation({
|
||||
...situation,
|
||||
dirigeant: "'indépendant'"
|
||||
}),
|
||||
[situation]
|
||||
)
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
|
@ -292,16 +305,6 @@ export default function SchemeComparaison({
|
|||
</div>
|
||||
</Trans>
|
||||
)}
|
||||
{conversationStarted && (
|
||||
<>
|
||||
<Trans i18nKey="comparaisonRégimes.période">
|
||||
<h3 className="legend">Unité</h3>
|
||||
</Trans>
|
||||
<div className="AS-indep-et-auto" style={{ alignSelf: 'start' }}>
|
||||
<PeriodSwitch />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="all colored">
|
||||
{!conversationStarted ? (
|
||||
<>
|
||||
|
@ -325,244 +328,185 @@ export default function SchemeComparaison({
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
{conversationStarted &&
|
||||
!!getRule('assimilé', 'revenu net après impôt')?.nodeValue && (
|
||||
<>
|
||||
<Trans i18nKey="comparaisonRégimes.revenuNetApresImpot">
|
||||
<h3 className="legend">Revenu net après impôt</h3>
|
||||
</Trans>
|
||||
<div className="AS">
|
||||
<Animate.appear className="ui__ plain card">
|
||||
<RuleValueLink
|
||||
branch="assimilé"
|
||||
rule="revenu net après impôt"
|
||||
/>
|
||||
</Animate.appear>
|
||||
</div>
|
||||
<div className="indep">
|
||||
<Animate.appear className="ui__ plain card">
|
||||
<RuleValueLink
|
||||
branch="indépendant"
|
||||
rule="revenu net après impôt"
|
||||
/>
|
||||
</Animate.appear>
|
||||
</div>
|
||||
<div className="auto">
|
||||
<Animate.appear
|
||||
className={classnames(
|
||||
'ui__ plain card',
|
||||
plafondAutoEntrepreneurDépassé && 'disabled'
|
||||
)}
|
||||
>
|
||||
{plafondAutoEntrepreneurDépassé ? (
|
||||
'Plafond de CA dépassé'
|
||||
) : (
|
||||
<RuleValueLink
|
||||
branch="auto-entrepreneur"
|
||||
rule="revenu net après impôt"
|
||||
/>
|
||||
)}
|
||||
</Animate.appear>
|
||||
</div>
|
||||
<Trans i18nKey="comparaisonRégimes.revenuNetAvantImpot">
|
||||
<h3 className="legend">
|
||||
Revenu net de cotisations <small>(avant impôts)</small>
|
||||
</h3>
|
||||
</Trans>
|
||||
<div className="AS">
|
||||
<RuleValueLink
|
||||
branch="assimilé"
|
||||
rule="revenus net de cotisations"
|
||||
/>
|
||||
</div>
|
||||
<div className="indep">
|
||||
<RuleValueLink
|
||||
branch="indépendant"
|
||||
rule="dirigeant . indépendant . revenu net de cotisations"
|
||||
/>
|
||||
</div>
|
||||
<div className="auto">
|
||||
{plafondAutoEntrepreneurDépassé ? (
|
||||
'—'
|
||||
) : (
|
||||
<RuleValueLink
|
||||
branch="auto-entrepreneur"
|
||||
rule="dirigeant . auto-entrepreneur . net de cotisations"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{displayResult && (
|
||||
<>
|
||||
<Trans i18nKey="comparaisonRégimes.revenuNetAvantImpot">
|
||||
<h3 className="legend">
|
||||
<Trans i18nKey="comparaisonRégimes.retraiteEstimation.legend">
|
||||
<span>Pension de retraite</span>
|
||||
<small>(avant impôts)</small>
|
||||
</Trans>
|
||||
Revenu net de cotisations <small>(avant impôts)</small>
|
||||
</h3>
|
||||
<div className="AS">
|
||||
<span>
|
||||
<RuleValueLink
|
||||
branch="assimilé"
|
||||
rule="protection sociale . retraite"
|
||||
</Trans>
|
||||
<div className="AS">
|
||||
<Value
|
||||
linkToRule={false}
|
||||
engine={assimiléEngine}
|
||||
precision={0}
|
||||
unit="€/an"
|
||||
expression="contrat salarié . rémunération . net"
|
||||
/>
|
||||
</div>
|
||||
<div className="indep">
|
||||
<Value
|
||||
linkToRule={false}
|
||||
engine={indépendantEngine}
|
||||
precision={0}
|
||||
expression="dirigeant . indépendant . revenu net de cotisations"
|
||||
/>
|
||||
</div>
|
||||
<div className="auto">
|
||||
<>
|
||||
{plafondAutoEntrepreneurDépassé && 'Plafond de CA dépassé'}
|
||||
<Value
|
||||
linkToRule={false}
|
||||
engine={autoEntrepreneurEngine}
|
||||
precision={0}
|
||||
className={''}
|
||||
unit="€/an"
|
||||
expression="dirigeant . auto-entrepreneur . net de cotisations"
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
<h3 className="legend">
|
||||
<Trans i18nKey="comparaisonRégimes.retraiteEstimation.legend">
|
||||
<span>Pension de retraite</span>
|
||||
<small>(avant impôts)</small>
|
||||
</Trans>
|
||||
</h3>
|
||||
<div className="AS">
|
||||
<Value
|
||||
linkToRule={false}
|
||||
engine={assimiléEngine}
|
||||
precision={0}
|
||||
expression="protection sociale . retraite"
|
||||
/>{' '}
|
||||
<InfoBulle>
|
||||
<Trans i18nKey="comparaisonRégimes.retraiteEstimation.infobulles.AS">
|
||||
Pension calculée pour 172 trimestres cotisés au régime général
|
||||
sans variations de revenus.
|
||||
</Trans>
|
||||
</InfoBulle>
|
||||
</div>
|
||||
<div className="indep">
|
||||
<Value
|
||||
linkToRule={false}
|
||||
engine={indépendantEngine}
|
||||
precision={0}
|
||||
expression="protection sociale . retraite"
|
||||
/>{' '}
|
||||
<InfoBulle>
|
||||
<Trans i18nKey="comparaisonRégimes.retraiteEstimation.infobulles.indep">
|
||||
Pension calculée pour 172 trimestres cotisés au régime des
|
||||
indépendants sans variations de revenus.
|
||||
</Trans>
|
||||
</InfoBulle>
|
||||
</div>
|
||||
<div className="auto">
|
||||
{plafondAutoEntrepreneurDépassé ? (
|
||||
'—'
|
||||
) : (
|
||||
<>
|
||||
<Value
|
||||
linkToRule={false}
|
||||
engine={autoEntrepreneurEngine}
|
||||
precision={0}
|
||||
expression="protection sociale . retraite"
|
||||
/>{' '}
|
||||
<InfoBulle>
|
||||
<Trans i18nKey="comparaisonRégimes.retraiteEstimation.infobulles.AS">
|
||||
Pension calculée pour 172 trimestres cotisés au régime
|
||||
général sans variations de revenus.
|
||||
<Trans i18nKey="comparaisonRégimes.retraiteEstimation.infobulles.auto">
|
||||
Pension calculée pour 172 trimestres cotisés en
|
||||
auto-entrepreneur sans variations de revenus.
|
||||
</Trans>
|
||||
</InfoBulle>
|
||||
</span>
|
||||
</div>
|
||||
<div className="indep">
|
||||
{getRule('indépendant', 'protection sociale . retraite')
|
||||
.isApplicable !== false ? (
|
||||
<span>
|
||||
<RuleValueLink
|
||||
branch="indépendant"
|
||||
rule="protection sociale . retraite"
|
||||
/>{' '}
|
||||
<InfoBulle>
|
||||
<Trans i18nKey="comparaisonRégimes.retraiteEstimation.infobulles.indep">
|
||||
Pension calculée pour 172 trimestres cotisés au régime
|
||||
des indépendants sans variations de revenus.
|
||||
</Trans>
|
||||
</InfoBulle>
|
||||
</span>
|
||||
) : (
|
||||
<span className="ui__ notice">
|
||||
<Trans>Pas implémenté</Trans>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="auto">
|
||||
{plafondAutoEntrepreneurDépassé ? (
|
||||
'—'
|
||||
) : getRule(
|
||||
'auto-entrepreneur',
|
||||
'protection sociale . retraite'
|
||||
).isApplicable !== false ? (
|
||||
<span>
|
||||
<RuleValueLink
|
||||
branch="auto-entrepreneur"
|
||||
rule="protection sociale . retraite"
|
||||
/>{' '}
|
||||
<InfoBulle>
|
||||
<Trans i18nKey="comparaisonRégimes.retraiteEstimation.infobulles.auto">
|
||||
Pension calculée pour 172 trimestres cotisés en
|
||||
auto-entrepreneur sans variations de revenus.
|
||||
</Trans>
|
||||
</InfoBulle>
|
||||
</span>
|
||||
) : (
|
||||
<span className="ui__ notice">
|
||||
<Trans>Pas implémenté</Trans>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<Trans i18nKey="comparaisonRégimes.trimestreValidés">
|
||||
<h3 className="legend">
|
||||
Nombre de trimestres validés <small>(pour la retraite)</small>
|
||||
</h3>
|
||||
</Trans>
|
||||
<div className="AS">
|
||||
<RuleValueLink
|
||||
branch="assimilé"
|
||||
rule="protection sociale . retraite . trimestres validés par an"
|
||||
appendText={<Trans>trimestres</Trans>}
|
||||
unit={null}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<Trans i18nKey="comparaisonRégimes.trimestreValidés">
|
||||
<h3 className="legend">
|
||||
Nombre de trimestres validés <small>(pour la retraite)</small>
|
||||
</h3>
|
||||
</Trans>
|
||||
<div className="AS">
|
||||
<Value
|
||||
linkToRule={false}
|
||||
engine={assimiléEngine}
|
||||
precision={0}
|
||||
displayedUnit="trimestre"
|
||||
expression="protection sociale . retraite . trimestres validés"
|
||||
/>
|
||||
</div>
|
||||
<div className="indep">
|
||||
<Value
|
||||
linkToRule={false}
|
||||
engine={indépendantEngine}
|
||||
precision={0}
|
||||
expression="protection sociale . retraite . trimestres validés"
|
||||
displayedUnit="trimestre"
|
||||
/>
|
||||
</div>
|
||||
<div className="auto">
|
||||
{plafondAutoEntrepreneurDépassé ? (
|
||||
'—'
|
||||
) : (
|
||||
<Value
|
||||
linkToRule={false}
|
||||
engine={autoEntrepreneurEngine}
|
||||
precision={0}
|
||||
expression="protection sociale . retraite . trimestres validés"
|
||||
displayedUnit="trimestres"
|
||||
/>
|
||||
</div>
|
||||
<div className="indep">
|
||||
<RuleValueLink
|
||||
branch="indépendant"
|
||||
rule="protection sociale . retraite . trimestres validés par an"
|
||||
appendText={<Trans>trimestres</Trans>}
|
||||
unit={null}
|
||||
)}
|
||||
</div>
|
||||
<Trans i18nKey="comparaisonRégimes.indemnités">
|
||||
<h3 className="legend">
|
||||
Indemnités journalières <small>(en cas d'arrêt maladie)</small>
|
||||
</h3>
|
||||
</Trans>
|
||||
<div className="AS">
|
||||
<span>
|
||||
<Value
|
||||
linkToRule={false}
|
||||
engine={assimiléEngine}
|
||||
precision={0}
|
||||
expression="protection sociale . santé . indemnités journalières"
|
||||
/>
|
||||
</div>
|
||||
<div className="auto">
|
||||
{plafondAutoEntrepreneurDépassé ? (
|
||||
'—'
|
||||
) : (
|
||||
<RuleValueLink
|
||||
branch="auto-entrepreneur"
|
||||
rule="protection sociale . retraite . trimestres validés par an"
|
||||
appendText={<Trans>trimestres</Trans>}
|
||||
unit={null}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Trans i18nKey="comparaisonRégimes.indemnités">
|
||||
<h3 className="legend">
|
||||
Indemnités journalières{' '}
|
||||
<small>(en cas d'arrêt maladie)</small>
|
||||
</h3>
|
||||
</Trans>
|
||||
<div className="AS">
|
||||
</span>
|
||||
<small>
|
||||
(
|
||||
<Value
|
||||
linkToRule={false}
|
||||
engine={assimiléEngine}
|
||||
precision={0}
|
||||
expression="protection sociale . accidents du travail et maladies professionnelles"
|
||||
/>{' '}
|
||||
<Trans>
|
||||
pour les accidents de trajet/travail et maladie pro
|
||||
</Trans>
|
||||
)
|
||||
</small>
|
||||
</div>
|
||||
<div className="indep">
|
||||
<Value
|
||||
linkToRule={false}
|
||||
engine={indépendantEngine}
|
||||
precision={0}
|
||||
expression="protection sociale . santé . indemnités journalières"
|
||||
/>
|
||||
</div>
|
||||
<div className="auto">
|
||||
{plafondAutoEntrepreneurDépassé ? (
|
||||
'—'
|
||||
) : (
|
||||
<span>
|
||||
<RuleValueLink
|
||||
branch="assimilé"
|
||||
appendText={
|
||||
<>
|
||||
/ <Trans>jour</Trans>
|
||||
</>
|
||||
}
|
||||
rule="protection sociale . santé . indemnités journalières"
|
||||
<Value
|
||||
linkToRule={false}
|
||||
engine={autoEntrepreneurEngine}
|
||||
precision={0}
|
||||
expression="protection sociale . santé . indemnités journalières"
|
||||
/>
|
||||
</span>
|
||||
<small>
|
||||
(
|
||||
<RuleValueLink
|
||||
branch="assimilé"
|
||||
rule="protection sociale . accidents du travail et maladies professionnelles"
|
||||
/>{' '}
|
||||
<Trans>
|
||||
pour les accidents de trajet/travail et maladie pro
|
||||
</Trans>
|
||||
)
|
||||
</small>
|
||||
</div>
|
||||
<div className="indep">
|
||||
<span>
|
||||
{getRule(
|
||||
'indépendant',
|
||||
'protection sociale . santé . indemnités journalières'
|
||||
).isApplicable !== false ? (
|
||||
<span>
|
||||
<RuleValueLink
|
||||
appendText={
|
||||
<>
|
||||
/ <Trans>jour</Trans>
|
||||
</>
|
||||
}
|
||||
branch="indépendant"
|
||||
rule="protection sociale . santé . indemnités journalières"
|
||||
/>
|
||||
</span>
|
||||
) : (
|
||||
<span className="ui__ notice">
|
||||
<Trans>Pas implémenté</Trans>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="auto">
|
||||
{plafondAutoEntrepreneurDépassé ? (
|
||||
'—'
|
||||
) : (
|
||||
<span>
|
||||
<RuleValueLink
|
||||
branch="auto-entrepreneur"
|
||||
rule="protection sociale . santé . indemnités journalières"
|
||||
appendText={
|
||||
<>
|
||||
/ <Trans>jour</Trans>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="ui__ container">
|
||||
<br />
|
||||
|
@ -622,35 +566,3 @@ export default function SchemeComparaison({
|
|||
</>
|
||||
)
|
||||
}
|
||||
|
||||
type RuleValueLinkProps = {
|
||||
branch: string
|
||||
rule: DottedName
|
||||
appendText?: React.ReactNode
|
||||
unit?: null | string
|
||||
}
|
||||
|
||||
function RuleValueLink({
|
||||
branch,
|
||||
rule: dottedName,
|
||||
appendText,
|
||||
unit
|
||||
}: RuleValueLinkProps) {
|
||||
const dispatch = useDispatch()
|
||||
const analyses = useSelector(analysisWithDefaultsSelector)
|
||||
const sitePaths = useContext(SitePathsContext)
|
||||
let rule = getRuleFrom(analyses)(branch, dottedName)
|
||||
return !rule ? null : (
|
||||
<Link
|
||||
onClick={() => dispatch(setSituationBranch(getBranchIndex(branch)))}
|
||||
to={sitePaths.documentation.rule(rule.dottedName)}
|
||||
>
|
||||
<Value
|
||||
maximumFractionDigits={0}
|
||||
{...rule}
|
||||
unit={unit != null ? unit : '€'}
|
||||
/>
|
||||
{appendText && <> {appendText}</>}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { SitePathsContext } from 'Components/utils/withSitePaths'
|
||||
import { SitePathsContext } from 'Components/utils/SitePathsContext'
|
||||
import { parentName } from 'Engine/ruleUtils'
|
||||
import { ParsedRule, ParsedRules } from 'Engine/types'
|
||||
import { pick, sortBy, take } from 'ramda'
|
||||
|
@ -10,6 +10,7 @@ import { DottedName } from 'Rules'
|
|||
import Worker from 'worker-loader!./SearchBar.worker.js'
|
||||
import { capitalise0 } from '../utils'
|
||||
import './SearchBar.css'
|
||||
import { UseDefaultValuesContext } from './Documentation/UseDefaultValuesContext'
|
||||
|
||||
const worker = new Worker()
|
||||
|
||||
|
@ -34,15 +35,16 @@ export default function SearchBar({
|
|||
let [focusElem, setFocusElem] = useState(-1)
|
||||
const { i18n } = useTranslation()
|
||||
const history = useHistory()
|
||||
|
||||
const useDefaultValues = useContext(UseDefaultValuesContext)
|
||||
const handleKeyDown = e => {
|
||||
if (e.key === 'Enter' && results.length > 0) {
|
||||
finallyCallback && finallyCallback()
|
||||
history.push(
|
||||
sitePaths.documentation.rule(
|
||||
history.push({
|
||||
pathname: sitePaths.documentation.rule(
|
||||
results[focusElem > 0 ? focusElem : 0].dottedName
|
||||
)
|
||||
)
|
||||
),
|
||||
state: { useDefaultValues }
|
||||
})
|
||||
}
|
||||
|
||||
if (
|
||||
|
@ -191,7 +193,12 @@ export default function SearchBar({
|
|||
return (
|
||||
<>
|
||||
{formattedResults.length === 0 && (
|
||||
<Link to={sitePaths.documentation.rule(dottedName)}>
|
||||
<Link
|
||||
to={{
|
||||
pathname: sitePaths.documentation.rule(dottedName),
|
||||
state: { useDefaultValues }
|
||||
}}
|
||||
>
|
||||
{title || capitalise0(name) || ''}
|
||||
</Link>
|
||||
)}
|
||||
|
@ -202,7 +209,12 @@ export default function SearchBar({
|
|||
|
||||
return (
|
||||
<Link
|
||||
to={sitePaths.documentation.rule(dottedName)}
|
||||
to={{
|
||||
pathname: sitePaths.documentation.rule(dottedName),
|
||||
state: {
|
||||
useDefaultValues
|
||||
}
|
||||
}}
|
||||
key={resultIndex}
|
||||
>
|
||||
<Highlighter text={formattedResult.formatted.title} />
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import React, { useEffect, useState, useContext } from 'react'
|
||||
import emoji from 'react-easy-emoji'
|
||||
import { Trans } from 'react-i18next'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { parsedRulesSelector } from 'Selectors/analyseSelectors'
|
||||
import Overlay from './Overlay'
|
||||
import { EngineContext } from 'Components/utils/EngineContext'
|
||||
import SearchBar from './SearchBar'
|
||||
|
||||
type SearchButtonProps = {
|
||||
|
@ -11,7 +11,7 @@ type SearchButtonProps = {
|
|||
}
|
||||
|
||||
export default function SearchButton({ invisibleButton }: SearchButtonProps) {
|
||||
const rules = useSelector(parsedRulesSelector)
|
||||
const rules = useContext(EngineContext).getParsedRules()
|
||||
const [visible, setVisible] = useState(false)
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import Warning from 'Components/ui/WarningBlock'
|
||||
import React from 'react'
|
||||
import { Trans } from 'react-i18next'
|
||||
import { SitePaths } from './utils/withSitePaths'
|
||||
import { SitePaths } from './utils/SitePathsContext'
|
||||
|
||||
type SimulateurWarningProps = {
|
||||
simulateur: Exclude<keyof SitePaths['simulateurs'], 'index'>
|
||||
|
|
|
@ -9,8 +9,8 @@ import TargetSelection from 'Components/TargetSelection'
|
|||
import React from 'react'
|
||||
import { Trans } from 'react-i18next'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { firstStepCompletedSelector } from 'Selectors/analyseSelectors'
|
||||
import { simulationProgressSelector } from 'Selectors/progressSelectors'
|
||||
import { firstStepCompletedSelector } from 'Selectors/simulationSelectors'
|
||||
import { useSimulationProgress } from 'Components/utils/useNextQuestion'
|
||||
import * as Animate from 'Ui/animate'
|
||||
import Progress from 'Ui/Progress'
|
||||
|
||||
|
@ -28,7 +28,7 @@ export default function Simulation({
|
|||
showPeriodSwitch
|
||||
}: SimulationProps) {
|
||||
const firstStepCompleted = useSelector(firstStepCompletedSelector)
|
||||
const progress = useSelector(simulationProgressSelector)
|
||||
const progress = useSimulationProgress()
|
||||
return (
|
||||
<>
|
||||
<TargetSelection showPeriodSwitch={showPeriodSwitch} />
|
||||
|
|
|
@ -62,7 +62,7 @@
|
|||
|
||||
#targetSelection .optionTitle {
|
||||
font-size: 115%;
|
||||
font-weight: 500;
|
||||
font-weight: 600;
|
||||
}
|
||||
#targetSelection .optionTitle a {
|
||||
color: inherit;
|
||||
|
@ -169,7 +169,7 @@
|
|||
font-style: italic;
|
||||
color: #c0392b;
|
||||
background: yellow;
|
||||
font-weight: 500;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Autre idée pour styler les checkboxes https://codepen.io/KenanYusuf/pen/PZKEKd */
|
||||
|
|
|
@ -3,11 +3,16 @@ import InputSuggestions from 'Components/conversation/InputSuggestions'
|
|||
import PeriodSwitch from 'Components/PeriodSwitch'
|
||||
import RuleLink from 'Components/RuleLink'
|
||||
import { ThemeColorsContext } from 'Components/utils/colors'
|
||||
import { SitePathsContext } from 'Components/utils/withSitePaths'
|
||||
import { formatCurrency } from 'Engine/format'
|
||||
import { ParsedRule } from 'Engine/types'
|
||||
import { isEmpty, isNil } from 'ramda'
|
||||
import React, { useContext, useEffect, useState } from 'react'
|
||||
import {
|
||||
EngineContext,
|
||||
useEvaluation,
|
||||
useInversionFail
|
||||
} from 'Components/utils/EngineContext'
|
||||
import { SitePathsContext } from 'Components/utils/SitePathsContext'
|
||||
import { formatCurrency, formatValue } from 'Engine/format'
|
||||
import { EvaluatedRule } 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'
|
||||
|
@ -15,10 +20,9 @@ import { Link, useLocation } from 'react-router-dom'
|
|||
import { RootState } from 'Reducers/rootReducer'
|
||||
import { DottedName } from 'Rules'
|
||||
import {
|
||||
analysisWithDefaultsSelector,
|
||||
situationSelector,
|
||||
useTarget
|
||||
} from 'Selectors/analyseSelectors'
|
||||
targetUnitSelector
|
||||
} from 'Selectors/simulationSelectors'
|
||||
import Animate from 'Ui/animate'
|
||||
import AnimatedTargetValue from 'Ui/AnimatedTargetValue'
|
||||
import CurrencyInput from './CurrencyInput/CurrencyInput'
|
||||
|
@ -26,48 +30,12 @@ import './TargetSelection.css'
|
|||
|
||||
export default function TargetSelection({ showPeriodSwitch = true }) {
|
||||
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 colors = useContext(ThemeColorsContext)
|
||||
|
||||
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
|
||||
}, [])
|
||||
|
@ -77,7 +45,7 @@ export default function TargetSelection({ showPeriodSwitch = true }) {
|
|||
{((typeof objectifs[0] === 'string'
|
||||
? [{ objectifs }]
|
||||
: objectifs) as any).map(
|
||||
({ icône, objectifs: groupTargets, nom }, index) => (
|
||||
({ icône, objectifs: targets, nom }, index) => (
|
||||
<React.Fragment key={nom || '0'}>
|
||||
<div style={{ display: 'flex', alignItems: 'end' }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
|
@ -101,14 +69,16 @@ export default function TargetSelection({ showPeriodSwitch = true }) {
|
|||
)`
|
||||
}}
|
||||
>
|
||||
<Targets
|
||||
{...{
|
||||
targets: targets.filter(({ dottedName }) =>
|
||||
groupTargets.includes(dottedName)
|
||||
),
|
||||
initialRender
|
||||
}}
|
||||
/>
|
||||
<ul className="targets">
|
||||
{' '}
|
||||
{targets.map(target => (
|
||||
<Target
|
||||
key={target}
|
||||
dottedName={target}
|
||||
initialRender={initialRender}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
</React.Fragment>
|
||||
)
|
||||
|
@ -117,36 +87,25 @@ export default function TargetSelection({ showPeriodSwitch = true }) {
|
|||
)
|
||||
}
|
||||
|
||||
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 }) => {
|
||||
type TargetProps = {
|
||||
dottedName: DottedName
|
||||
initialRender: boolean
|
||||
}
|
||||
const Target = ({ dottedName, initialRender }: TargetProps) => {
|
||||
const activeInput = useSelector((state: RootState) => state.activeTargetInput)
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const isActiveInput = activeInput === target.dottedName
|
||||
const target = useEvaluation(dottedName, {
|
||||
unit: useSelector(targetUnitSelector)
|
||||
})
|
||||
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}
|
||||
|
@ -186,7 +145,7 @@ const Target = ({ target, initialRender }) => {
|
|||
onFirstClick={value => {
|
||||
dispatch(updateSituation(target.dottedName, value))
|
||||
}}
|
||||
unit={target.defaultUnit}
|
||||
unit={target.unit}
|
||||
/>
|
||||
</div>
|
||||
</Animate.fromTop>
|
||||
|
@ -209,7 +168,12 @@ let Header = ({ target }) => {
|
|||
<span className="header">
|
||||
<span className="texts">
|
||||
<span className="optionTitle">
|
||||
<Link to={sitePaths.documentation.rule(target.dottedName)}>
|
||||
<Link
|
||||
to={{
|
||||
pathname: sitePaths.documentation.rule(target.dottedName),
|
||||
state: { useDefaultValues: true }
|
||||
}}
|
||||
>
|
||||
{target.title || target.name}
|
||||
{hackyShowPeriod && ' ' + t('mensuel')}
|
||||
</Link>
|
||||
|
@ -221,7 +185,7 @@ let Header = ({ target }) => {
|
|||
}
|
||||
|
||||
type TargetInputOrValueProps = {
|
||||
target: ParsedRule<DottedName>
|
||||
target: EvaluatedRule<DottedName>
|
||||
isActiveInput: boolean
|
||||
isSmallTarget: boolean
|
||||
}
|
||||
|
@ -235,16 +199,29 @@ function TargetInputOrValue({
|
|||
const colors = useContext(ThemeColorsContext)
|
||||
const dispatch = useDispatch()
|
||||
const situationValue = useSelector(situationSelector)[target.dottedName]
|
||||
|
||||
const targetWithValue = useTarget(target.dottedName)
|
||||
const inversionFail = useSelector(analysisWithDefaultsSelector)?.cache._meta
|
||||
.inversionFail
|
||||
const targetUnit = useSelector(targetUnitSelector)
|
||||
const engine = useContext(EngineContext)
|
||||
const value =
|
||||
targetWithValue?.nodeValue != null && !inversionFail
|
||||
? Math.round(targetWithValue.nodeValue)
|
||||
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 = inversionFail && !isActiveInput
|
||||
|
||||
const blurValue = useInversionFail() && !isActiveInput
|
||||
|
||||
const onChange = useCallback(
|
||||
evt =>
|
||||
dispatch(
|
||||
updateSituation(target.dottedName, +evt.target.value + ' ' + targetUnit)
|
||||
),
|
||||
[targetUnit, target, dispatch]
|
||||
)
|
||||
return (
|
||||
<span
|
||||
className="targetInputOrValue"
|
||||
|
@ -260,15 +237,15 @@ function TargetInputOrValue({
|
|||
}}
|
||||
debounce={600}
|
||||
name={target.dottedName}
|
||||
value={situationValue ? Math.round(situationValue) : value}
|
||||
value={value}
|
||||
className={
|
||||
isActiveInput || isNil(value) ? 'targetInput' : 'editableTarget'
|
||||
}
|
||||
onChange={evt =>
|
||||
dispatch(
|
||||
updateSituation(target.dottedName, Number(evt.target.value))
|
||||
)
|
||||
isActiveInput ||
|
||||
isNil(value) ||
|
||||
(target.question && isSmallTarget)
|
||||
? 'targetInput'
|
||||
: 'editableTarget'
|
||||
}
|
||||
onChange={onChange}
|
||||
onFocus={() => {
|
||||
if (isSmallTarget) return
|
||||
dispatch(setActiveTarget(target.dottedName))
|
||||
|
@ -292,8 +269,10 @@ function TargetInputOrValue({
|
|||
)
|
||||
}
|
||||
function TitreRestaurant() {
|
||||
const titresRestaurant = useTarget(
|
||||
'contrat salarié . frais professionnels . titres-restaurant . montant'
|
||||
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
|
||||
|
@ -303,7 +282,11 @@ function TitreRestaurant() {
|
|||
<RuleLink {...titresRestaurant}>
|
||||
+{' '}
|
||||
<strong>
|
||||
{formatCurrency(titresRestaurant.nodeValue, language)}
|
||||
{formatValue({
|
||||
nodeValue: titresRestaurant.nodeValue,
|
||||
unit: '€',
|
||||
language
|
||||
})}
|
||||
</strong>{' '}
|
||||
<Trans>en titres-restaurant</Trans> {emoji(' 🍽')}
|
||||
</RuleLink>
|
||||
|
@ -312,16 +295,18 @@ function TitreRestaurant() {
|
|||
)
|
||||
}
|
||||
function AidesGlimpse() {
|
||||
const aides = useTarget('contrat salarié . aides employeur')
|
||||
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 aidesNode = aides?.explanation
|
||||
const aidesDetail = aides?.explanation.formule.explanation.explanation
|
||||
const aidesDetail = aides?.formule.explanation.explanation
|
||||
const aidesNotNul = aidesDetail?.filter(node => node.nodeValue !== false)
|
||||
const aideLink = aidesNotNul?.length === 1 ? aidesNotNul[0] : aidesNode
|
||||
const aideLink = aidesNotNul?.length === 1 ? aidesNotNul[0] : aides
|
||||
|
||||
if (!aides?.nodeValue) return null
|
||||
return (
|
||||
|
@ -330,11 +315,15 @@ function AidesGlimpse() {
|
|||
<RuleLink {...aideLink}>
|
||||
<Trans>en incluant</Trans>{' '}
|
||||
<strong>
|
||||
<AnimatedTargetValue value={aides.nodeValue}>
|
||||
<span>{formatCurrency(aides.nodeValue, language)}</span>
|
||||
</AnimatedTargetValue>
|
||||
<span>
|
||||
{formatValue({
|
||||
nodeValue: aides.nodeValue,
|
||||
unit: '€',
|
||||
language
|
||||
})}
|
||||
</span>
|
||||
</strong>{' '}
|
||||
<Trans>d'aides</Trans> {emoji(aides.explanation?.icons ?? '')}
|
||||
<Trans>d'aides</Trans> {emoji(aides?.icons ?? '')}
|
||||
</RuleLink>
|
||||
</div>
|
||||
</Animate.fromTop>
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
#targets {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#targets > .icon {
|
||||
margin: 0 0.6em;
|
||||
font-size: 200%;
|
||||
color: var(--color);
|
||||
}
|
||||
#targets .value {
|
||||
font-size: 180%;
|
||||
}
|
||||
#targets .unit {
|
||||
}
|
||||
|
||||
#targets .explanation {
|
||||
font-size: 150%;
|
||||
text-decoration: none;
|
||||
line-height: 0;
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
import { ThemeColorsContext } from 'Components/utils/colors'
|
||||
import { SitePathsContext } from 'Components/utils/withSitePaths'
|
||||
import React, { useContext } from 'react'
|
||||
import emoji from 'react-easy-emoji'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { analysisWithDefaultsSelector } from 'Selectors/analyseSelectors'
|
||||
import './Targets.css'
|
||||
|
||||
export default function Targets() {
|
||||
const colors = useContext(ThemeColorsContext)
|
||||
const sitePaths = useContext(SitePathsContext)
|
||||
const analysis = useSelector(analysisWithDefaultsSelector)
|
||||
let { nodeValue, unité: unit, dottedName } = analysis.targets[0]
|
||||
return (
|
||||
<div id="targets">
|
||||
<span className="icon">→</span>
|
||||
<span className="content" style={{ color: colors.textColor }}>
|
||||
<span className="figure">
|
||||
<span className="value">{nodeValue?.toFixed(1)}</span>{' '}
|
||||
<span className="unit">{unit}</span>
|
||||
</span>
|
||||
<Link
|
||||
title="Quel est calcul ?"
|
||||
style={{ color: colors.color }}
|
||||
to={sitePaths.documentation.rule(dottedName)}
|
||||
className="explanation"
|
||||
>
|
||||
{emoji('📖')}
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -1,85 +0,0 @@
|
|||
import classnames from 'classnames'
|
||||
import { formatValue, formatValueOptions } from 'Engine/format'
|
||||
import { EvaluatedRule } from 'Engine/types'
|
||||
import { Unit } from 'Engine/units'
|
||||
import React from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
|
||||
// let booleanTranslations = { true: '✅', false: '❌' }
|
||||
|
||||
let booleanTranslations = {
|
||||
fr: { true: 'Oui', false: 'Non' },
|
||||
en: { true: 'Yes', false: 'No' }
|
||||
}
|
||||
|
||||
let style = customStyle => `
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
|
||||
${customStyle}
|
||||
`
|
||||
|
||||
export type ValueProps = Partial<
|
||||
Pick<EvaluatedRule, 'nodeValue'> &
|
||||
Pick<
|
||||
formatValueOptions,
|
||||
'maximumFractionDigits' | 'minimumFractionDigits'
|
||||
> & {
|
||||
nilValueSymbol: string
|
||||
children: number
|
||||
negative: boolean
|
||||
unit: string | Unit
|
||||
customCSS: string
|
||||
className?: string
|
||||
}
|
||||
>
|
||||
|
||||
export default function Value({
|
||||
nodeValue: value,
|
||||
unit,
|
||||
nilValueSymbol,
|
||||
maximumFractionDigits,
|
||||
minimumFractionDigits,
|
||||
children,
|
||||
className,
|
||||
negative,
|
||||
customCSS = ''
|
||||
}: ValueProps) {
|
||||
const { language } = useTranslation().i18n
|
||||
|
||||
/* Either an entire rule object is passed, or just the right attributes and the value as a JSX child*/
|
||||
let nodeValue = value === undefined ? children : value
|
||||
|
||||
if (
|
||||
(nilValueSymbol !== undefined && nodeValue === 0) ||
|
||||
(nodeValue && Number.isNaN(nodeValue)) ||
|
||||
nodeValue === null
|
||||
)
|
||||
return (
|
||||
<span css={style(customCSS)} className={classnames('value', className)}>
|
||||
-
|
||||
</span>
|
||||
)
|
||||
let valueType = typeof nodeValue,
|
||||
formattedValue =
|
||||
valueType === 'string' ? (
|
||||
<Trans>{nodeValue}</Trans>
|
||||
) : valueType === 'object' ? (
|
||||
(nodeValue as any).nom
|
||||
) : valueType === 'boolean' ? (
|
||||
booleanTranslations[language][nodeValue]
|
||||
) : nodeValue !== undefined ? (
|
||||
formatValue({
|
||||
minimumFractionDigits,
|
||||
maximumFractionDigits,
|
||||
language,
|
||||
unit,
|
||||
value: nodeValue
|
||||
})
|
||||
) : null
|
||||
return nodeValue == undefined ? null : (
|
||||
<span css={style(customCSS)} className={classnames('value', className)}>
|
||||
{negative ? '-' : ''}
|
||||
{formattedValue}
|
||||
</span>
|
||||
)
|
||||
}
|
|
@ -1,16 +1,16 @@
|
|||
import { explainVariable } from 'Actions/actions'
|
||||
import Overlay from 'Components/Overlay'
|
||||
import { Markdown } from 'Components/utils/markdown'
|
||||
import React from 'react'
|
||||
import React, { useContext } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { RootState } from 'Reducers/rootReducer'
|
||||
import { parsedRulesSelector } from 'Selectors/analyseSelectors'
|
||||
import References from '../rule/References'
|
||||
import References from '../Documentation/References'
|
||||
import './Aide.css'
|
||||
import { EngineContext } from 'Components/utils/EngineContext'
|
||||
|
||||
export default function Aide() {
|
||||
const explained = useSelector((state: RootState) => state.explainedVariable)
|
||||
const rules = useSelector(parsedRulesSelector)
|
||||
const rules = useContext(EngineContext).getParsedRules()
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const stopExplaining = () => dispatch(explainVariable())
|
||||
|
@ -29,9 +29,7 @@ export default function Aide() {
|
|||
`}
|
||||
>
|
||||
<h2>{rule.title}</h2>
|
||||
<p>
|
||||
<Markdown source={text} />
|
||||
</p>
|
||||
<Markdown source={text} />
|
||||
{refs && (
|
||||
<div>
|
||||
<References refs={refs} />
|
||||
|
|
|
@ -1,18 +1,14 @@
|
|||
import { goToQuestion, resetSimulation } from 'Actions/actions'
|
||||
import Overlay from 'Components/Overlay'
|
||||
import Value from 'Components/Value'
|
||||
import { getRuleFromAnalysis } from 'Engine/ruleUtils'
|
||||
import { useEvaluation } from 'Components/utils/EngineContext'
|
||||
import { useNextQuestions } from 'Components/utils/useNextQuestion'
|
||||
import { formatValue } from 'Engine/format'
|
||||
import React from 'react'
|
||||
import emoji from 'react-easy-emoji'
|
||||
import { Trans } from 'react-i18next'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { RootState } from 'Reducers/rootReducer'
|
||||
import { createSelector } from 'reselect'
|
||||
import {
|
||||
analysisWithDefaultsSelector,
|
||||
nextStepsSelector
|
||||
} from 'Selectors/analyseSelectors'
|
||||
import { softCatch } from '../../utils'
|
||||
import { DottedName } from 'Rules'
|
||||
import { answeredQuestionsSelector } from 'Selectors/simulationSelectors'
|
||||
import './AnswerList.css'
|
||||
|
||||
type AnswerListProps = {
|
||||
|
@ -21,46 +17,60 @@ type AnswerListProps = {
|
|||
|
||||
export default function AnswerList({ onClose }: AnswerListProps) {
|
||||
const dispatch = useDispatch()
|
||||
const { folded, next } = useSelector(stepsToRules)
|
||||
const answeredQuestions = useSelector(answeredQuestionsSelector)
|
||||
const nextSteps = useNextQuestions()
|
||||
|
||||
return (
|
||||
<Overlay onClose={onClose} className="answer-list">
|
||||
<h2>
|
||||
{emoji('📋 ')}
|
||||
<Trans>Mes réponses</Trans>
|
||||
<small css="margin-left: 2em; img {font-size: .8em}">
|
||||
{emoji('🗑')}{' '}
|
||||
<button
|
||||
className="ui__ simple small button"
|
||||
onClick={() => {
|
||||
dispatch(resetSimulation())
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
<Trans>Tout effacer</Trans>
|
||||
</button>
|
||||
</small>
|
||||
</h2>
|
||||
<StepsTable {...{ rules: folded, onClose }} />
|
||||
{next.length > 0 && (
|
||||
{!!answeredQuestions.length && (
|
||||
<>
|
||||
<h2>
|
||||
{emoji('📋 ')}
|
||||
<Trans>Mes réponses</Trans>
|
||||
<small css="margin-left: 2em; img {font-size: .8em}">
|
||||
{emoji('🗑')}{' '}
|
||||
<button
|
||||
className="ui__ simple small button"
|
||||
onClick={() => {
|
||||
dispatch(resetSimulation())
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
<Trans>Tout effacer</Trans>
|
||||
</button>
|
||||
</small>
|
||||
</h2>
|
||||
<StepsTable {...{ rules: answeredQuestions, onClose }} />
|
||||
</>
|
||||
)}
|
||||
{!!nextSteps.length && (
|
||||
<>
|
||||
<h2>
|
||||
{emoji('🔮 ')}
|
||||
<Trans>Prochaines questions</Trans>
|
||||
</h2>
|
||||
<StepsTable {...{ rules: next, onClose }} />
|
||||
<StepsTable {...{ rules: nextSteps, onClose }} />
|
||||
</>
|
||||
)}
|
||||
</Overlay>
|
||||
)
|
||||
}
|
||||
|
||||
function StepsTable({ rules, onClose }) {
|
||||
function StepsTable({
|
||||
rules,
|
||||
onClose
|
||||
}: {
|
||||
rules: Array<DottedName>
|
||||
onClose: () => void
|
||||
}) {
|
||||
const dispatch = useDispatch()
|
||||
const evaluatedRules = useEvaluation(rules)
|
||||
const language = useTranslation().i18n.language
|
||||
return (
|
||||
<table>
|
||||
<tbody>
|
||||
{rules
|
||||
.filter(rule => rule.nodeValue !== undefined)
|
||||
{evaluatedRules
|
||||
.filter(rule => rule.isApplicable !== false)
|
||||
.map(rule => (
|
||||
<tr
|
||||
key={rule.dottedName}
|
||||
|
@ -89,7 +99,7 @@ function StepsTable({ rules, onClose }) {
|
|||
font-size: inherit;
|
||||
width: 100%;
|
||||
text-align: start;
|
||||
font-weight: 500;
|
||||
font-weight: 600;
|
||||
> span {
|
||||
border-bottom-color: var(--textColorOnWhite);
|
||||
padding: 0.05em 0em;
|
||||
|
@ -98,7 +108,7 @@ function StepsTable({ rules, onClose }) {
|
|||
`}
|
||||
>
|
||||
<span className="answerContent">
|
||||
<Value {...rule} />
|
||||
{formatValue({ ...rule, language })}
|
||||
</span>
|
||||
</span>{' '}
|
||||
</td>
|
||||
|
@ -108,17 +118,3 @@ function StepsTable({ rules, onClose }) {
|
|||
</table>
|
||||
)
|
||||
}
|
||||
|
||||
const stepsToRules = createSelector(
|
||||
(state: RootState) => state.simulation?.foldedSteps || [],
|
||||
nextStepsSelector,
|
||||
analysisWithDefaultsSelector,
|
||||
(folded, nextSteps, analysis) => ({
|
||||
folded: folded
|
||||
.map(softCatch(getRuleFromAnalysis(analysis)))
|
||||
.filter(Boolean),
|
||||
next: nextSteps
|
||||
.map(softCatch(getRuleFromAnalysis(analysis)))
|
||||
.filter(Boolean)
|
||||
})
|
||||
)
|
||||
|
|
|
@ -1,20 +1,22 @@
|
|||
import { goToQuestion, validateStepWithValue } from 'Actions/actions'
|
||||
import QuickLinks from 'Components/QuickLinks'
|
||||
import RuleInput from 'Engine/RuleInput'
|
||||
import React from 'react'
|
||||
import React, { useContext, useEffect } from 'react'
|
||||
import emoji from 'react-easy-emoji'
|
||||
import { Trans } from 'react-i18next'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { RootState } from 'Reducers/rootReducer'
|
||||
import {
|
||||
currentQuestionSelector,
|
||||
nextStepsSelector,
|
||||
parsedRulesSelector
|
||||
} from 'Selectors/analyseSelectors'
|
||||
import * as Animate from 'Ui/animate'
|
||||
import Aide from './Aide'
|
||||
import './conversation.css'
|
||||
import FormDecorator from './FormDecorator'
|
||||
import { useNextQuestions } from 'Components/utils/useNextQuestion'
|
||||
import { EngineContext } from 'Components/utils/EngineContext'
|
||||
import PreviousAnswers from 'sites/mon-entreprise.fr/pages/Créer/GuideStatut/PreviousAnswers'
|
||||
import {
|
||||
answeredQuestionsSelector,
|
||||
currentQuestionSelector
|
||||
} from 'Selectors/simulationSelectors'
|
||||
|
||||
export type ConversationProps = {
|
||||
customEndMessages?: React.ReactNode
|
||||
|
@ -22,18 +24,15 @@ export type ConversationProps = {
|
|||
|
||||
export default function Conversation({ customEndMessages }: ConversationProps) {
|
||||
const dispatch = useDispatch()
|
||||
const rules = useSelector(parsedRulesSelector)
|
||||
const currentQuestion = useSelector(currentQuestionSelector)
|
||||
const previousAnswers = useSelector(
|
||||
(state: RootState) => state.simulation?.foldedSteps || []
|
||||
)
|
||||
const nextSteps = useSelector(nextStepsSelector)
|
||||
const rules = useContext(EngineContext).getParsedRules()
|
||||
const currentQuestion = useNextQuestions()[0]
|
||||
|
||||
const previousAnswers = useSelector(answeredQuestionsSelector)
|
||||
const setDefault = () =>
|
||||
dispatch(
|
||||
validateStepWithValue(
|
||||
currentQuestion,
|
||||
rules[currentQuestion].defaultValue
|
||||
rules[currentQuestion]['par défaut']
|
||||
)
|
||||
)
|
||||
const goToPrevious = () =>
|
||||
|
@ -45,35 +44,31 @@ export default function Conversation({ customEndMessages }: ConversationProps) {
|
|||
}
|
||||
const DecoratedInputComponent = FormDecorator(RuleInput)
|
||||
|
||||
return rules && nextSteps.length ? (
|
||||
return currentQuestion ? (
|
||||
<>
|
||||
<Aide />
|
||||
<div tabIndex={0} style={{ outline: 'none' }} onKeyDown={handleKeyDown}>
|
||||
{currentQuestion && (
|
||||
<React.Fragment key={currentQuestion}>
|
||||
<Animate.fadeIn>
|
||||
<DecoratedInputComponent dottedName={currentQuestion} />
|
||||
</Animate.fadeIn>
|
||||
<div className="ui__ answer-group">
|
||||
{previousAnswers.length > 0 && (
|
||||
<>
|
||||
<button
|
||||
onClick={goToPrevious}
|
||||
className="ui__ simple small push-left button"
|
||||
>
|
||||
← <Trans>Précédent</Trans>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<Animate.fadeIn>
|
||||
<DecoratedInputComponent dottedName={currentQuestion} />
|
||||
</Animate.fadeIn>
|
||||
<div className="ui__ answer-group">
|
||||
{previousAnswers.length > 0 && (
|
||||
<>
|
||||
<button
|
||||
onClick={setDefault}
|
||||
className="ui__ simple small push-right button"
|
||||
onClick={goToPrevious}
|
||||
className="ui__ simple small push-left button"
|
||||
>
|
||||
<Trans>Passer</Trans> →
|
||||
← <Trans>Précédent</Trans>
|
||||
</button>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
onClick={setDefault}
|
||||
className="ui__ simple small push-right button"
|
||||
>
|
||||
<Trans>Passer</Trans> →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<QuickLinks />
|
||||
</>
|
||||
|
|
|
@ -4,15 +4,15 @@ import emoji from 'react-easy-emoji'
|
|||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { RootState } from 'Reducers/rootReducer'
|
||||
import { DottedName } from 'Rules'
|
||||
import { parsedRulesSelector } from 'Selectors/analyseSelectors'
|
||||
import { TrackerContext } from '../utils/withTracker'
|
||||
import './Explicable.css'
|
||||
import { EngineContext } from 'Components/utils/EngineContext'
|
||||
|
||||
export default function Explicable({ dottedName }: { dottedName: DottedName }) {
|
||||
const rules = useContext(EngineContext).getParsedRules()
|
||||
const tracker = useContext(TrackerContext)
|
||||
const dispatch = useDispatch()
|
||||
const explained = useSelector((state: RootState) => state.explainedVariable)
|
||||
const rules = useSelector(parsedRulesSelector)
|
||||
|
||||
// Rien à expliquer ici, ce n'est pas une règle
|
||||
if (dottedName == null) return null
|
||||
|
|
|
@ -1,12 +1,9 @@
|
|||
import { updateSituation } from 'Actions/actions'
|
||||
import { updateSituation, goToQuestion } from 'Actions/actions'
|
||||
import Explicable from 'Components/conversation/Explicable'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import React, { useContext } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import {
|
||||
parsedRulesSelector,
|
||||
situationSelector
|
||||
} from 'Selectors/analyseSelectors'
|
||||
import { situationSelector } from 'Selectors/simulationSelectors'
|
||||
import { EngineContext } from 'Components/utils/EngineContext'
|
||||
|
||||
/*
|
||||
This higher order component wraps "Form" components (e.g. Question.js), that represent user inputs,
|
||||
|
@ -20,9 +17,8 @@ export default function FormDecorator(RenderField) {
|
|||
return function FormStep({ dottedName }) {
|
||||
const dispatch = useDispatch()
|
||||
const situation = useSelector(situationSelector)
|
||||
const rules = useSelector(parsedRulesSelector)
|
||||
const rules = useContext(EngineContext).getParsedRules()
|
||||
|
||||
const language = useTranslation().i18n.language
|
||||
const submit = source =>
|
||||
dispatch({
|
||||
type: 'STEP_ACTION',
|
||||
|
@ -31,6 +27,7 @@ export default function FormDecorator(RenderField) {
|
|||
source
|
||||
})
|
||||
const setFormValue = value => {
|
||||
dispatch(goToQuestion(dottedName))
|
||||
dispatch(updateSituation(dottedName, value))
|
||||
}
|
||||
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
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,
|
||||
themeColors: { color, textColorOnWhite }
|
||||
}) => (
|
||||
<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 ' + color,
|
||||
color: textColorOnWhite
|
||||
}
|
||||
}
|
||||
>
|
||||
{text}
|
||||
</h1>
|
||||
</div>
|
||||
)
|
|
@ -33,6 +33,7 @@ export default function Input({
|
|||
onChange(value)
|
||||
}}
|
||||
onSecondClick={() => onSubmit && onSubmit('suggestion')}
|
||||
unit={unit}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -41,7 +42,7 @@ export default function Input({
|
|||
autoFocus={autoFocus}
|
||||
className="suffixed"
|
||||
id={'step-' + dottedName}
|
||||
placeholder={defaultValue}
|
||||
placeholder={defaultValue?.nodeValue ?? defaultValue}
|
||||
thousandSeparator={thousandSeparator}
|
||||
decimalSeparator={decimalSeparator}
|
||||
allowEmptyFormatting={true}
|
||||
|
|
|
@ -1,15 +1,12 @@
|
|||
import { Rule } from 'Engine/types'
|
||||
import { toPairs } from 'ramda'
|
||||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { defaultUnitSelector } from 'Selectors/analyseSelectors'
|
||||
import { convertUnit, parseUnit, Unit } from '../../engine/units'
|
||||
import { serializeUnit, Unit } from '../../engine/units'
|
||||
|
||||
type InputSuggestionsProps = {
|
||||
suggestions?: Rule['suggestions']
|
||||
onFirstClick: (val: number | string) => void
|
||||
onSecondClick?: (val: number | string) => void
|
||||
suggestions?: Record<string, number>
|
||||
onFirstClick: (val: string) => void
|
||||
onSecondClick?: (val: string) => void
|
||||
unit?: Unit
|
||||
}
|
||||
|
||||
|
@ -21,27 +18,25 @@ export default function InputSuggestions({
|
|||
}: InputSuggestionsProps) {
|
||||
const [suggestion, setSuggestion] = useState<string | number>()
|
||||
const { t } = useTranslation()
|
||||
const defaultUnit = parseUnit(useSelector(defaultUnitSelector) ?? '')
|
||||
if (!suggestions) return null
|
||||
|
||||
return (
|
||||
<div css="display: flex; align-items: baseline; ">
|
||||
<small>Suggestions :</small>
|
||||
|
||||
{toPairs(suggestions).map(([text, value]: [string, string | number]) => {
|
||||
value =
|
||||
unit && typeof value === 'number'
|
||||
? convertUnit(unit, defaultUnit, value)
|
||||
: value
|
||||
{toPairs(suggestions).map(([text, value]: [string, number]) => {
|
||||
const valueWithUnit: string = `${value} ${
|
||||
unit ? serializeUnit(unit)?.replace(' / ', '/') : ''
|
||||
}`
|
||||
return (
|
||||
<button
|
||||
className="ui__ link-button"
|
||||
key={value}
|
||||
css="margin: 0 0.4rem !important"
|
||||
onClick={() => {
|
||||
onFirstClick(value)
|
||||
if (suggestion !== value) setSuggestion(value)
|
||||
else onSecondClick && onSecondClick(value)
|
||||
onFirstClick(valueWithUnit)
|
||||
if (suggestion !== value) setSuggestion(valueWithUnit)
|
||||
else onSecondClick && onSecondClick(valueWithUnit)
|
||||
}}
|
||||
title={t('cliquez pour insérer cette suggestion')}
|
||||
>
|
||||
|
|
|
@ -88,7 +88,7 @@ export default function Question({
|
|||
<li key={dottedName} className="variantLeaf">
|
||||
<RadioLabel
|
||||
{...{
|
||||
value: relativeDottedName(dottedName),
|
||||
value: `'${relativeDottedName(dottedName)}'`,
|
||||
label: title,
|
||||
dottedName,
|
||||
currentValue,
|
||||
|
|
|
@ -6,20 +6,15 @@ import Answers from './AnswerList'
|
|||
import './conversation.css'
|
||||
|
||||
export default function SeeAnswersButton() {
|
||||
const arePreviousAnswers = !!useSelector(
|
||||
(state: RootState) => state.simulation?.foldedSteps.length
|
||||
)
|
||||
const [showAnswerModal, setShowAnswerModal] = useState(false)
|
||||
return (
|
||||
<>
|
||||
{arePreviousAnswers && (
|
||||
<button
|
||||
className="ui__ small simple button "
|
||||
onClick={() => setShowAnswerModal(true)}
|
||||
>
|
||||
<Trans>Modifier mes réponses</Trans>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="ui__ small simple button "
|
||||
onClick={() => setShowAnswerModal(true)}
|
||||
>
|
||||
<Trans>Voir toutes les questions</Trans>
|
||||
</button>
|
||||
{showAnswerModal && <Answers onClose={() => setShowAnswerModal(false)} />}
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -47,16 +47,14 @@ export default function Select({ onChange, onSubmit }) {
|
|||
tauxVersementTransport(option.code)
|
||||
.then(({ taux }) => {
|
||||
// serialize to not mix our data schema and the API response's
|
||||
onChange(
|
||||
JSON.stringify({
|
||||
...option,
|
||||
...(taux != undefined
|
||||
? {
|
||||
'taux du versement transport': taux
|
||||
}
|
||||
: {})
|
||||
})
|
||||
)
|
||||
onChange({
|
||||
...option,
|
||||
...(taux != undefined
|
||||
? {
|
||||
'taux du versement transport': taux
|
||||
}
|
||||
: {})
|
||||
})
|
||||
onSubmit()
|
||||
})
|
||||
.catch(error => {
|
|
@ -1,79 +0,0 @@
|
|||
import { setExample } from 'Actions/actions'
|
||||
import classNames from 'classnames'
|
||||
import { compose } from 'ramda'
|
||||
import React from 'react'
|
||||
import { Trans } from 'react-i18next'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
export default compose(
|
||||
connect(
|
||||
state => ({
|
||||
parsedRules: state.parsedRules,
|
||||
themeColors: state.themeColors
|
||||
}),
|
||||
dispatch => ({
|
||||
setExample: compose(dispatch, setExample)
|
||||
})
|
||||
)
|
||||
)(function Examples({
|
||||
situationExists,
|
||||
rule,
|
||||
themeColors,
|
||||
setExample,
|
||||
currentExample
|
||||
}) {
|
||||
let { examples } = rule
|
||||
|
||||
if (!examples) return null
|
||||
return (
|
||||
<>
|
||||
<h2>
|
||||
<Trans i18nKey="examples">Exemples</Trans>{' '}
|
||||
<small>
|
||||
<Trans i18nKey="clickexample">
|
||||
Cliquez sur un exemple pour le tester
|
||||
</Trans>
|
||||
</small>
|
||||
</h2>
|
||||
<ul>
|
||||
{examples.map(ex => (
|
||||
<Example
|
||||
key={ex.nom}
|
||||
{...{ ex, rule, currentExample, setExample, themeColors }}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{situationExists && currentExample && (
|
||||
<button className="ui__ button small" onClick={() => setExample(null)}>
|
||||
<Trans i18nKey="cancelExample">Revenir à votre situation</Trans>
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
let Example = ({
|
||||
ex: { nom, situation },
|
||||
rule,
|
||||
currentExample,
|
||||
setExample
|
||||
}) => {
|
||||
let selected = currentExample && currentExample.name == nom
|
||||
return (
|
||||
<li key={nom}>
|
||||
<button
|
||||
onClick={() =>
|
||||
selected
|
||||
? setExample(null)
|
||||
: setExample(nom, situation, rule.dottedName)
|
||||
}
|
||||
className={classNames('ui__ button small', {
|
||||
selected
|
||||
})}
|
||||
>
|
||||
{nom}
|
||||
</button>
|
||||
</li>
|
||||
)
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
.reportErrorContainer {
|
||||
text-align: center;
|
||||
padding: 0.3em 0.6em;
|
||||
margin: 3em auto 0;
|
||||
}
|
||||
.reportError {
|
||||
color: #c0392b;
|
||||
font-size: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
.reportError:hover {
|
||||
color: #b53527;
|
||||
}
|
||||
|
||||
#notes {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
h2 small {
|
||||
font-size: 75%;
|
||||
}
|
||||
|
||||
|
||||
#rule #ruleDefault {
|
||||
text-align: center;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
#toggleRuleSource {
|
||||
margin-top: 1em;
|
||||
}
|
|
@ -1,239 +0,0 @@
|
|||
import { ThemeColorsContext } from 'Components/utils/colors'
|
||||
import { SitePathsContext } from 'Components/utils/withSitePaths'
|
||||
import Value from 'Components/Value'
|
||||
import mecanisms from 'Engine/mecanisms.yaml'
|
||||
import { filter, isEmpty } from 'ramda'
|
||||
import React, { Suspense, useContext, useState } from 'react'
|
||||
import emoji from 'react-easy-emoji'
|
||||
import { Helmet } from 'react-helmet'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { Link } from 'react-router-dom'
|
||||
import {
|
||||
exampleAnalysisSelector,
|
||||
noUserInputSelector,
|
||||
parsedRulesSelector,
|
||||
ruleAnalysisSelector
|
||||
} from 'Selectors/analyseSelectors'
|
||||
import Animate from 'Ui/animate'
|
||||
import { AttachDictionary } from '../AttachDictionary'
|
||||
import RuleLink from '../RuleLink'
|
||||
import { Markdown } from '../utils/markdown'
|
||||
import Algorithm from './Algorithm'
|
||||
import Examples from './Examples'
|
||||
import RuleHeader from './Header'
|
||||
import References from './References'
|
||||
import './Rule.css'
|
||||
|
||||
let LazySource = React.lazy(() => import('./RuleSource'))
|
||||
|
||||
export default AttachDictionary(mecanisms)(function Rule({ dottedName }) {
|
||||
const currentExample = useSelector(state => state.currentExample)
|
||||
const rules = useSelector(parsedRulesSelector)
|
||||
const valuesToShow = !useSelector(noUserInputSelector)
|
||||
const analysedRule = useSelector(state =>
|
||||
ruleAnalysisSelector(state, { dottedName })
|
||||
)
|
||||
const analysedExample = useSelector(state =>
|
||||
exampleAnalysisSelector(state, { dottedName })
|
||||
)
|
||||
const sitePaths = useContext(SitePathsContext)
|
||||
const [viewSource, setViewSource] = useState(false)
|
||||
const { t } = useTranslation()
|
||||
|
||||
let rule = rules[dottedName]
|
||||
let { type, name, acronyme, title, description, question, icon } = rule,
|
||||
namespaceRules = filter(
|
||||
rule =>
|
||||
rule.dottedName.startsWith(dottedName) &&
|
||||
rule.dottedName.split(' . ').length ===
|
||||
dottedName.split(' . ').length + 1,
|
||||
rules
|
||||
)
|
||||
let displayedRule = analysedExample || analysedRule
|
||||
const renderToggleSourceButton = () => {
|
||||
return (
|
||||
<button
|
||||
id="toggleRuleSource"
|
||||
className="ui__ link-button"
|
||||
onClick={() => setViewSource(!viewSource)}
|
||||
>
|
||||
{emoji(
|
||||
viewSource
|
||||
? `📖 ${t('Revenir à la documentation')}`
|
||||
: `✍️ ${t('Voir le code source')}`
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
const renderReferences = ({ références: refs }) =>
|
||||
refs ? (
|
||||
<div>
|
||||
<h2>
|
||||
<Trans>Références</Trans>
|
||||
</h2>
|
||||
<References refs={refs} />
|
||||
</div>
|
||||
) : null
|
||||
|
||||
return (
|
||||
<>
|
||||
{viewSource ? (
|
||||
<>
|
||||
{renderToggleSourceButton()}
|
||||
<Suspense fallback={<div>Chargement du code source...</div>}>
|
||||
<LazySource dottedName={dottedName} />
|
||||
</Suspense>
|
||||
</>
|
||||
) : (
|
||||
<div id="rule">
|
||||
<Animate.fromBottom>
|
||||
<Helmet
|
||||
title={title}
|
||||
meta={[
|
||||
{
|
||||
name: 'description',
|
||||
content: description
|
||||
}
|
||||
]}
|
||||
/>
|
||||
<RuleHeader
|
||||
{...{
|
||||
dottedName,
|
||||
type,
|
||||
description,
|
||||
question,
|
||||
flatRule: rule,
|
||||
name,
|
||||
acronyme,
|
||||
title,
|
||||
icon,
|
||||
valuesToShow
|
||||
}}
|
||||
/>
|
||||
|
||||
<section id="rule-content">
|
||||
<div
|
||||
id="ruleValue"
|
||||
css={`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
|
||||
> .value {
|
||||
font-size: 220%;
|
||||
}
|
||||
|
||||
margin: 0.6em 0;
|
||||
> * {
|
||||
margin: 0 0.6em;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<Value
|
||||
{...displayedRule}
|
||||
nilValueSymbol={displayedRule.parentDependencies.some(
|
||||
parent => parent?.nodeValue == false
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{displayedRule.defaultValue != null && (
|
||||
<div id="ruleDefault">
|
||||
par défaut :{' '}
|
||||
<Value
|
||||
{...displayedRule}
|
||||
nodeValue={displayedRule.defaultValue}
|
||||
unit={displayedRule.unit || displayedRule.defaultUnit}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!valuesToShow && (
|
||||
<div style={{ textAlign: 'center', marginTop: '1em' }}>
|
||||
<Link
|
||||
className="ui__ cta plain button"
|
||||
target="_parent"
|
||||
to={
|
||||
dottedName.includes('contrat salarié')
|
||||
? sitePaths.simulateurs.salarié
|
||||
: dottedName.includes('auto-entrepreneur')
|
||||
? sitePaths.simulateurs['auto-entrepreneur']
|
||||
: dottedName.includes('indépendant')
|
||||
? sitePaths.simulateurs.indépendant
|
||||
: // otherwise
|
||||
sitePaths.simulateurs.index
|
||||
}
|
||||
>
|
||||
<Trans>Faire une simulation</Trans>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
<Algorithm
|
||||
rule={displayedRule}
|
||||
showValues={valuesToShow || currentExample}
|
||||
/>
|
||||
{displayedRule['rend non applicable'] && (
|
||||
<section id="non-applicable">
|
||||
<h3>
|
||||
<Trans>Rend non applicable les règles suivantes</Trans> :{' '}
|
||||
</h3>
|
||||
<ul>
|
||||
{displayedRule['rend non applicable'].map(ruleName => (
|
||||
<li key={ruleName}>
|
||||
<RuleLink dottedName={ruleName} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
{rule.note && (
|
||||
<section id="notes">
|
||||
<h3>Note : </h3>
|
||||
<Markdown source={rule.note} />
|
||||
</section>
|
||||
)}
|
||||
<Examples
|
||||
currentExample={currentExample}
|
||||
situationExists={valuesToShow}
|
||||
rule={displayedRule}
|
||||
/>
|
||||
{!isEmpty(namespaceRules) && (
|
||||
<NamespaceRulesList {...{ namespaceRules }} />
|
||||
)}
|
||||
{renderReferences(rule)}
|
||||
</section>
|
||||
{renderToggleSourceButton()}
|
||||
</Animate.fromBottom>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
function NamespaceRulesList({ namespaceRules }) {
|
||||
const colors = useContext(ThemeColorsContext)
|
||||
const sitePaths = useContext(SitePathsContext)
|
||||
return (
|
||||
<section>
|
||||
<h2>
|
||||
<Trans>Pages associées</Trans>
|
||||
</h2>
|
||||
<ul>
|
||||
{Object.values(namespaceRules).map(r => (
|
||||
<li key={r.name}>
|
||||
<Link
|
||||
style={{
|
||||
color: colors.textColorOnWhite,
|
||||
textDecoration: 'underline'
|
||||
}}
|
||||
to={sitePaths.documentation.rule(r.dottedName)}
|
||||
>
|
||||
{r.title || r.name}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
import React from 'react'
|
||||
|
||||
export const ShowValuesContext = React.createContext(false)
|
||||
|
||||
const {
|
||||
Consumer: ShowValuesConsumer,
|
||||
Provider: ShowValuesProvider
|
||||
} = ShowValuesContext
|
||||
export { ShowValuesConsumer, ShowValuesProvider }
|
|
@ -1,5 +1,5 @@
|
|||
situation:
|
||||
dirigeant: artiste-auteur
|
||||
dirigeant: "'artiste-auteur'"
|
||||
unité par défaut: €/an
|
||||
objectifs:
|
||||
- artiste-auteur . cotisations
|
||||
|
|
|
@ -36,5 +36,5 @@ questions:
|
|||
|
||||
unité par défaut: €/an
|
||||
situation:
|
||||
dirigeant: 'assimilé salarié'
|
||||
dirigeant: "'assimilé salarié'"
|
||||
contrat salarié . ATMP . taux réduit: oui
|
||||
|
|
|
@ -18,4 +18,4 @@ questions:
|
|||
|
||||
unité par défaut: €/an
|
||||
situation:
|
||||
dirigeant: 'auto-entrepreneur'
|
||||
dirigeant: "'auto-entrepreneur'"
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
objectifs:
|
||||
- contrat salarié . rémunération . brut de base
|
||||
|
||||
objectifs secondaires:
|
||||
- contrat salarié . prix du travail
|
||||
objectifs cachés:
|
||||
- contrat salarié . rémunération . net
|
||||
- contrat salarié . prix du travail
|
||||
- chômage partiel . revenu net habituel
|
||||
- chômage partiel . coût employeur habituel
|
||||
- contrat salarié . activité partielle . indemnités
|
||||
|
||||
questions:
|
||||
uniquement:
|
||||
|
|
|
@ -31,4 +31,4 @@ questions:
|
|||
|
||||
unité par défaut: €/an
|
||||
situation:
|
||||
dirigeant: 'indépendant'
|
||||
dirigeant: "'indépendant'"
|
||||
|
|
|
@ -2,12 +2,12 @@ titre: |
|
|||
Calcul du revenu du travailleur indépendant ou dirigeant d'entreprise après paiement des cotisations et de l'impôt sur le revenu.
|
||||
|
||||
objectifs:
|
||||
- revenu net après impôt
|
||||
- revenus net de cotisations
|
||||
- contrat salarié . rémunération . net
|
||||
- dirigeant . indépendant . revenu net de cotisations
|
||||
- dirigeant . auto-entrepreneur . net de cotisations
|
||||
- protection sociale . retraite
|
||||
- protection sociale . retraite . trimestres validés par an
|
||||
- protection sociale . retraite . trimestres validés
|
||||
- protection sociale . santé . indemnités journalières
|
||||
- protection sociale . accidents du travail et maladies professionnelles
|
||||
|
||||
questions:
|
||||
uniquement:
|
||||
|
@ -19,14 +19,6 @@ questions:
|
|||
- entreprise . catégorie d'activité . libérale règlementée
|
||||
|
||||
unité par défaut: €/an
|
||||
branches:
|
||||
- nom: Assimilé salarié
|
||||
situation:
|
||||
dirigeant: 'assimilé salarié'
|
||||
contrat salarié . ATMP . taux réduit: oui
|
||||
- nom: Indépendant
|
||||
situation:
|
||||
dirigeant: 'indépendant'
|
||||
- nom: Auto-entrepreneur
|
||||
situation:
|
||||
dirigeant: 'auto-entrepreneur'
|
||||
situation:
|
||||
dirigeant: "'auto-entrepreneur'"
|
||||
contrat salarié . ATMP . taux réduit: oui
|
||||
|
|
|
@ -1,16 +1,10 @@
|
|||
objectifs:
|
||||
- contrat salarié . prix du travail
|
||||
- contrat salarié . aides employeur
|
||||
- contrat salarié . rémunération . brut de base . équivalent temps plein
|
||||
- contrat salarié . rémunération . brut de base
|
||||
- contrat salarié . rémunération . net
|
||||
- contrat salarié . rémunération . net après impôt
|
||||
|
||||
objectifs secondaires:
|
||||
- contrat salarié . temps de travail
|
||||
- contrat salarié . cotisations
|
||||
- contrat salarié . frais professionnels . titres-restaurant . montant
|
||||
|
||||
questions:
|
||||
à l'affiche:
|
||||
Chômage partiel: contrat salarié . activité partielle
|
||||
|
|
|
@ -25,7 +25,7 @@ export default function AnimatedTargetValue({
|
|||
|
||||
// We don't want to show the animated if the difference comes from a change in the unit
|
||||
const currentUnit = useSelector(
|
||||
(state: RootState) => state?.simulation?.defaultUnit
|
||||
(state: RootState) => state?.simulation?.targetUnit
|
||||
)
|
||||
const previousUnit = useRef(currentUnit)
|
||||
|
||||
|
|
|
@ -52,7 +52,7 @@
|
|||
border: 1px dotted gray;
|
||||
}
|
||||
.ui__.toggle input[type='radio']:checked ~ .radioText {
|
||||
font-weight: 500;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ui__.toggle input[type='radio']:checked ~ * {
|
||||
|
|
|
@ -123,7 +123,7 @@ a:not(:disabled):not(.button):not(.button-choice):hover {
|
|||
|
||||
strong,
|
||||
b {
|
||||
font-weight: 500;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
textarea {
|
||||
|
|
|
@ -126,6 +126,7 @@ span.ui__.enumeration:not(:last-of-type)::after {
|
|||
|
||||
.ui__.label {
|
||||
font-size: 85%;
|
||||
line-height: initial;
|
||||
padding: 0.4rem 0.6rem;
|
||||
font-weight: bold;
|
||||
color: white !important;
|
||||
|
@ -133,6 +134,10 @@ span.ui__.enumeration:not(:last-of-type)::after {
|
|||
border-radius: 0.3rem;
|
||||
text-align: center;
|
||||
}
|
||||
.ui__.small.label {
|
||||
font-size: 75%;
|
||||
padding: 0.2rem 0.4rem;
|
||||
}
|
||||
|
||||
.no-scroll {
|
||||
overflow: hidden;
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
import Engine, { EvaluationOptions } from 'Engine'
|
||||
import { EvaluatedRule } from 'Engine/types'
|
||||
import React, { createContext, useContext } from 'react'
|
||||
import rules, { DottedName } from 'Rules'
|
||||
|
||||
export const EngineContext = createContext<Engine<DottedName>>(
|
||||
new Engine(rules)
|
||||
)
|
||||
|
||||
export const EngineProvider = EngineContext.Provider
|
||||
|
||||
type SituationProviderProps = {
|
||||
children: React.ReactNode
|
||||
situation: Partial<Record<DottedName, string | number | Object>>
|
||||
}
|
||||
export function SituationProvider({
|
||||
children,
|
||||
situation
|
||||
}: SituationProviderProps) {
|
||||
const engine = useContext(EngineContext)
|
||||
engine.setSituation(situation)
|
||||
return (
|
||||
<EngineContext.Provider value={engine}>{children}</EngineContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useEvaluation(
|
||||
rule: DottedName,
|
||||
options?: EvaluationOptions
|
||||
): EvaluatedRule<DottedName>
|
||||
export function useEvaluation(
|
||||
rule: DottedName[],
|
||||
options?: EvaluationOptions
|
||||
): EvaluatedRule<DottedName>[]
|
||||
export function useEvaluation(
|
||||
rule: Array<DottedName> | DottedName,
|
||||
options?: EvaluationOptions
|
||||
): Array<EvaluatedRule<DottedName>> | EvaluatedRule<DottedName> {
|
||||
const engine = useContext(EngineContext)
|
||||
if (Array.isArray(rule)) {
|
||||
return rule.map(name => engine.evaluate(name, options))
|
||||
}
|
||||
return engine.evaluate(rule, options)
|
||||
}
|
||||
|
||||
export function useInversionFail() {
|
||||
return useContext(EngineContext).inversionFail()
|
||||
}
|
||||
|
||||
export function useControls() {
|
||||
return useContext(EngineContext).controls()
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
import { splitName } from 'Engine/ruleUtils'
|
||||
import {
|
||||
add,
|
||||
countBy,
|
||||
descend,
|
||||
difference,
|
||||
equals,
|
||||
flatten,
|
||||
head,
|
||||
identity,
|
||||
intersection,
|
||||
keys,
|
||||
last,
|
||||
length,
|
||||
map,
|
||||
mergeWith,
|
||||
negate,
|
||||
pair,
|
||||
pipe,
|
||||
reduce,
|
||||
sortBy,
|
||||
sortWith,
|
||||
takeWhile,
|
||||
toPairs,
|
||||
values,
|
||||
zipWith
|
||||
} from 'ramda'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { useEvaluation } from './EngineContext'
|
||||
import {
|
||||
objectifsSelector,
|
||||
configSelector,
|
||||
answeredQuestionsSelector,
|
||||
currentQuestionSelector
|
||||
} from 'Selectors/simulationSelectors'
|
||||
import { useMemo } from 'react'
|
||||
import { DottedName } from 'Rules'
|
||||
import { SimulationConfig } from 'Reducers/rootReducer'
|
||||
|
||||
type MissingVariables = Array<Partial<Record<DottedName, number>>>
|
||||
export function getNextSteps(
|
||||
missingVariables: MissingVariables
|
||||
): Array<DottedName> {
|
||||
let byCount = ([, [count]]) => count
|
||||
let byScore = ([, [, score]]) => score
|
||||
|
||||
let missingByTotalScore = reduce(mergeWith(add), {}, missingVariables)
|
||||
|
||||
let innerKeys = flatten(map(keys, missingVariables)),
|
||||
missingByTargetsAdvanced = countBy(identity, innerKeys)
|
||||
|
||||
let missingByCompound = mergeWith(
|
||||
pair,
|
||||
missingByTargetsAdvanced,
|
||||
missingByTotalScore
|
||||
),
|
||||
pairs = toPairs(missingByCompound),
|
||||
sortedPairs = sortWith([descend(byCount), descend(byScore) as any], pairs)
|
||||
return map(head, sortedPairs)
|
||||
}
|
||||
|
||||
const similarity = (rule1: string = '', rule2: string = '') =>
|
||||
pipe(
|
||||
zipWith(equals),
|
||||
takeWhile(Boolean),
|
||||
length,
|
||||
negate
|
||||
)(splitName(rule1), splitName(rule2))
|
||||
|
||||
export function getNextQuestions(
|
||||
missingVariables: MissingVariables,
|
||||
questionConfig: SimulationConfig['questions'] = {},
|
||||
answeredQuestions = []
|
||||
): Array<DottedName> {
|
||||
const {
|
||||
'non prioritaires': notPriority = [],
|
||||
uniquement: only = null,
|
||||
'liste noire': blacklist = []
|
||||
} = questionConfig
|
||||
// console.log(missingVariables)
|
||||
let nextSteps = difference(getNextSteps(missingVariables), answeredQuestions)
|
||||
|
||||
if (only) {
|
||||
nextSteps = intersection(nextSteps, [...only, ...notPriority])
|
||||
}
|
||||
if (blacklist) {
|
||||
nextSteps = difference(nextSteps, blacklist)
|
||||
}
|
||||
|
||||
const lastStep = last(answeredQuestions)
|
||||
// L'ajout de la réponse permet de traiter les questions dont la réponse est "une possibilité", exemple "contrat salarié . cdd"
|
||||
// lastStepWithAnswer =
|
||||
// lastStep && situation[lastStep]
|
||||
// ? ([lastStep, situation[lastStep]].join(' . ') as DottedName)
|
||||
// : lastStep
|
||||
|
||||
return sortBy(
|
||||
question =>
|
||||
notPriority.includes(question)
|
||||
? notPriority.indexOf(question)
|
||||
: similarity(question, lastStep),
|
||||
|
||||
nextSteps
|
||||
)
|
||||
}
|
||||
|
||||
export const useNextQuestions = function(): Array<DottedName> {
|
||||
const objectifs = useSelector(objectifsSelector)
|
||||
const answeredQuestions = useSelector(answeredQuestionsSelector)
|
||||
const currentQuestion = useSelector(currentQuestionSelector)
|
||||
const questionsConfig = useSelector(configSelector).questions ?? {}
|
||||
const missingVariables = useEvaluation(objectifs, {
|
||||
useDefaultValues: false
|
||||
}).map(node => node.missingVariables ?? {})
|
||||
const nextQuestions = useMemo(() => {
|
||||
return getNextQuestions(
|
||||
missingVariables,
|
||||
questionsConfig,
|
||||
answeredQuestions
|
||||
)
|
||||
}, [missingVariables, questionsConfig, answeredQuestions])
|
||||
if (currentQuestion && currentQuestion !== nextQuestions[0]) {
|
||||
return [currentQuestion, ...nextQuestions]
|
||||
}
|
||||
return nextQuestions
|
||||
}
|
||||
|
||||
export function useSimulationProgress(): number {
|
||||
const numberQuestionAnswered = useSelector(answeredQuestionsSelector).length
|
||||
const numberQuestionLeft = useNextQuestions().length
|
||||
return numberQuestionAnswered / (numberQuestionAnswered + numberQuestionLeft)
|
||||
}
|
|
@ -1,13 +1,14 @@
|
|||
import Input from 'Components/conversation/Input'
|
||||
import Question from 'Components/conversation/Question'
|
||||
import SelectGéo from 'Components/conversation/select/SelectGéo'
|
||||
import SelectGéo from 'Components/conversation/select/SelectGeo'
|
||||
import SelectAtmp from 'Components/conversation/select/SelectTauxRisque'
|
||||
import SendButton from 'Components/conversation/SendButton'
|
||||
import CurrencyInput from 'Components/CurrencyInput/CurrencyInput'
|
||||
import PercentageField from 'Components/PercentageField'
|
||||
import ToggleSwitch from 'Components/ui/ToggleSwitch'
|
||||
import { EngineContext } from 'Components/utils/EngineContext'
|
||||
import { ParsedRules } from 'Engine/types'
|
||||
import React from 'react'
|
||||
import React, { useContext } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { DottedName } from 'Rules'
|
||||
import DateInput from '../components/conversation/DateInput'
|
||||
|
@ -17,7 +18,7 @@ export const binaryOptionChoices = [
|
|||
{ value: 'oui', label: 'Oui' }
|
||||
]
|
||||
|
||||
type Value = string | number | object | boolean
|
||||
type Value = string | number | object | boolean | null
|
||||
export type RuleInputProps = {
|
||||
rules: ParsedRules
|
||||
dottedName: DottedName
|
||||
|
@ -46,8 +47,9 @@ export default function RuleInput({
|
|||
onSubmit
|
||||
}: RuleInputProps) {
|
||||
let rule = rules[dottedName]
|
||||
let unit = rule.unit || rule.defaultUnit
|
||||
let unit = rule.unit
|
||||
let language = useTranslation().i18n.language
|
||||
let engine = useContext(EngineContext)
|
||||
|
||||
let commonProps = {
|
||||
key: dottedName,
|
||||
|
@ -100,6 +102,11 @@ export default function RuleInput({
|
|||
<Question {...commonProps} choices={binaryOptionChoices} />
|
||||
)
|
||||
}
|
||||
|
||||
commonProps.value =
|
||||
typeof commonProps.value === 'string'
|
||||
? engine.evaluate(commonProps.value as DottedName).nodeValue
|
||||
: commonProps.value
|
||||
if (unit?.numerators.includes('€') && isTarget) {
|
||||
return (
|
||||
<>
|
||||
|
@ -123,20 +130,18 @@ export default function RuleInput({
|
|||
return <Input {...commonProps} unit={unit} />
|
||||
}
|
||||
|
||||
let getVariant = rule => rule?.formule?.explanation['une possibilité']
|
||||
let getVariant = rule => rule?.formule?.explanation['possibilités']
|
||||
|
||||
export let buildVariantTree = (allRules, path) => {
|
||||
let rec = path => {
|
||||
let node = allRules[path]
|
||||
if (!node) throw new Error(`La règle ${path} est introuvable`)
|
||||
let variant = getVariant(node),
|
||||
variants = variant && node.formule.explanation['possibilités'],
|
||||
shouldBeExpanded = variant && true, //variants.find( v => relevantPaths.find(rp => contains(path + ' . ' + v)(rp) )),
|
||||
canGiveUp = variant && !node.formule.explanation['choix obligatoire']
|
||||
|
||||
let variant = getVariant(node)
|
||||
const variants = variant && node.formule.explanation['possibilités']
|
||||
const canGiveUp = variant && !node.formule.explanation['choix obligatoire']
|
||||
return Object.assign(
|
||||
node,
|
||||
shouldBeExpanded
|
||||
!!variant
|
||||
? {
|
||||
canGiveUp,
|
||||
children: variants.map(v => rec(path + ' . ' + v))
|
||||
|
|
|
@ -60,13 +60,13 @@ export function typeWarning(
|
|||
export function warning(
|
||||
rules: string[] | string,
|
||||
message: string,
|
||||
solution: string
|
||||
solution?: string
|
||||
) {
|
||||
console.warn(
|
||||
`\n[ Avertissement ]
|
||||
➡️ Dans la règle \`${coerceArray(rules).slice(-1)[0]}\`
|
||||
⚠️ ${message}
|
||||
💡 ${solution}
|
||||
💡 ${solution ? solution : ''}
|
||||
`
|
||||
)
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ import { ParsedRule } from 'Engine/types'
|
|||
import { map, mergeAll, pick, pipe } from 'ramda'
|
||||
import { typeWarning } from './error'
|
||||
import { convertNodeToUnit } from './nodeUnits'
|
||||
import { areUnitConvertible } from './units'
|
||||
|
||||
export const evaluateApplicability = (
|
||||
cache,
|
||||
|
@ -48,8 +47,11 @@ export const evaluateApplicability = (
|
|||
])
|
||||
|
||||
return {
|
||||
...node,
|
||||
isApplicable,
|
||||
nodeValue: isApplicable,
|
||||
missingVariables,
|
||||
parentDependencies,
|
||||
...evaluatedAttributes
|
||||
}
|
||||
}
|
||||
|
@ -72,7 +74,7 @@ export default (cache, situationGate, parsedRules, node) => {
|
|||
? evaluateNode(cache, situationGate, parsedRules, node.formule)
|
||||
: {}
|
||||
// evaluate the formula lazily, only if the applicability is known and true
|
||||
const evaluatedFormula = isApplicable
|
||||
let evaluatedFormula = isApplicable
|
||||
? evaluateFormula()
|
||||
: isApplicable === false
|
||||
? {
|
||||
|
@ -85,27 +87,10 @@ export default (cache, situationGate, parsedRules, node) => {
|
|||
missingVariables: {},
|
||||
nodeValue: null
|
||||
}
|
||||
let {
|
||||
missingVariables: formulaMissingVariables,
|
||||
nodeValue
|
||||
} = evaluatedFormula
|
||||
const missingVariables = mergeMissing(
|
||||
bonus(condMissing, !!Object.keys(condMissing).length),
|
||||
formulaMissingVariables
|
||||
)
|
||||
const unit =
|
||||
node.unit ||
|
||||
(node.defaultUnit &&
|
||||
cache._meta.defaultUnits.find(unit =>
|
||||
areUnitConvertible(node.defaultUnit, unit)
|
||||
)) ||
|
||||
node.defaultUnit ||
|
||||
evaluatedFormula.unit
|
||||
|
||||
const temporalValue = evaluatedFormula.temporalValue
|
||||
if (unit) {
|
||||
if (node.unit) {
|
||||
try {
|
||||
nodeValue = convertNodeToUnit(unit, evaluatedFormula).nodeValue
|
||||
evaluatedFormula = convertNodeToUnit(node.unit, evaluatedFormula)
|
||||
} catch (e) {
|
||||
typeWarning(
|
||||
node.dottedName,
|
||||
|
@ -114,14 +99,20 @@ export default (cache, situationGate, parsedRules, node) => {
|
|||
)
|
||||
}
|
||||
}
|
||||
const missingVariables = mergeMissing(
|
||||
bonus(condMissing, !!Object.keys(condMissing).length),
|
||||
evaluatedFormula.missingVariables
|
||||
)
|
||||
// console.log(node.dottedName, evaluatedFormula.unit)
|
||||
|
||||
let temporalValue = evaluatedFormula.temporalValue
|
||||
cache._meta.contextRule.pop()
|
||||
return {
|
||||
...node,
|
||||
...applicabilityEvaluation,
|
||||
...(node.formule && { formule: evaluatedFormula }),
|
||||
nodeValue,
|
||||
unit,
|
||||
nodeValue: evaluatedFormula.nodeValue,
|
||||
unit: node.unit ?? evaluatedFormula.unit,
|
||||
temporalValue,
|
||||
isApplicable,
|
||||
missingVariables
|
||||
|
|
|
@ -11,9 +11,9 @@ import {
|
|||
import React from 'react'
|
||||
import { typeWarning } from './error'
|
||||
import { convertNodeToUnit, simplifyNodeUnit } from './nodeUnits'
|
||||
import { EvaluatedNode } from 'Engine/types'
|
||||
import {
|
||||
concatTemporals,
|
||||
EvaluatedNode,
|
||||
liftTemporalNode,
|
||||
mapTemporal,
|
||||
pureTemporal,
|
||||
|
@ -21,10 +21,11 @@ import {
|
|||
temporalAverage,
|
||||
zipTemporals
|
||||
} from './temporal'
|
||||
import { ParsedRule, ParsedRules } from './types'
|
||||
|
||||
export let makeJsx = node =>
|
||||
typeof node.jsx == 'function'
|
||||
? node.jsx(node.nodeValue, node.explanation, node.lazyEval, node.unit)
|
||||
? node.jsx(node.nodeValue, node.explanation, node.unit)
|
||||
: node.jsx
|
||||
|
||||
export let collectNodeMissing = node => node.missingVariables || {}
|
||||
|
@ -40,12 +41,6 @@ export let evaluateNode = (cache, situationGate, parsedRules, node) => {
|
|||
let evaluatedNode = node.evaluate
|
||||
? node.evaluate(cache, situationGate, parsedRules, node)
|
||||
: node
|
||||
if (typeof evaluatedNode.nodeValue !== 'number') {
|
||||
return evaluatedNode
|
||||
}
|
||||
evaluatedNode = node.unité
|
||||
? convertNodeToUnit(node.unit, evaluatedNode)
|
||||
: simplifyNodeUnit(evaluatedNode)
|
||||
return evaluatedNode
|
||||
}
|
||||
|
||||
|
@ -175,6 +170,7 @@ export let evaluateObject = (objectShape, effect) => (
|
|||
}, temporalExplanations)
|
||||
|
||||
const sameUnitTemporalExplanation: Temporal<EvaluatedNode<
|
||||
string,
|
||||
number
|
||||
>> = convertNodesToSameUnit(
|
||||
temporalExplanation.map(x => x.value),
|
||||
|
@ -210,3 +206,22 @@ export let evaluateObject = (objectShape, effect) => (
|
|||
temporalExplanation
|
||||
}
|
||||
}
|
||||
|
||||
type DefaultValues<Names extends string> = { [name in Names]: any } | {}
|
||||
export function collectDefaults<Names extends string>(
|
||||
parsedRules: ParsedRules<Names>
|
||||
): DefaultValues<Names> {
|
||||
const cache = { _meta: { contextRule: [] as string[] } }
|
||||
return (Object.values(parsedRules) as Array<ParsedRule<Names>>).reduce(
|
||||
(acc, parsedRule) => {
|
||||
if (parsedRule?.['par défaut'] == null) {
|
||||
return acc
|
||||
}
|
||||
return {
|
||||
...acc,
|
||||
[parsedRule.dottedName]: parsedRule['par défaut']
|
||||
}
|
||||
},
|
||||
{}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,48 +1,62 @@
|
|||
import { expect } from 'chai'
|
||||
import { parseUnit } from 'Engine/units'
|
||||
import { formatCurrency, formatPercentage, formatValue } from './format'
|
||||
import { formatValue } from './format'
|
||||
|
||||
describe('format engine values', () => {
|
||||
it('format currencies', () => {
|
||||
expect(formatCurrency(12, 'fr')).to.equal('12 €')
|
||||
expect(formatCurrency(1200, 'fr')).to.equal('1 200 €')
|
||||
expect(formatCurrency(12, 'en')).to.equal('€ 12')
|
||||
expect(formatCurrency(12.1)).to.equal('€ 12.10')
|
||||
expect(formatCurrency(12.123)).to.equal('€ 12.12')
|
||||
expect(formatValue({ nodeValue: 12, unit: '€', language: 'fr' })).to.equal(
|
||||
'12 €'
|
||||
)
|
||||
expect(
|
||||
formatValue({ nodeValue: 1200, unit: '€', language: 'fr' })
|
||||
).to.equal('1 200 €')
|
||||
expect(formatValue({ nodeValue: 12, unit: '€', language: 'en' })).to.equal(
|
||||
'€12'
|
||||
)
|
||||
expect(
|
||||
formatValue({ nodeValue: 12.1, unit: '€', language: 'en' })
|
||||
).to.equal('€12.10')
|
||||
expect(
|
||||
formatValue({ nodeValue: 12.123, unit: '€', language: 'en' })
|
||||
).to.equal('€12.12')
|
||||
})
|
||||
|
||||
it('format percentages', () => {
|
||||
expect(formatPercentage(10)).to.equal('10%')
|
||||
expect(formatPercentage(100)).to.equal('100%')
|
||||
expect(formatPercentage(10.2)).to.equal('10.2%')
|
||||
expect(formatValue({ nodeValue: 10, unit: '%' })).to.equal('10%')
|
||||
expect(formatValue({ nodeValue: 100, unit: '%' })).to.equal('100%')
|
||||
expect(formatValue({ nodeValue: 10.2, unit: '%' })).to.equal('10.2%')
|
||||
})
|
||||
|
||||
it('format values', () => {
|
||||
expect(formatValue({ unit: '€', value: 12 })).to.equal('€12')
|
||||
expect(formatValue({ unit: '€', value: 12.1 })).to.equal('€12.10')
|
||||
expect(formatValue({ unit: '€', value: 12, language: 'fr' })).to.equal(
|
||||
expect(formatValue({ unit: '€', nodeValue: 12 })).to.equal('€12')
|
||||
expect(formatValue({ unit: '€', nodeValue: 12.1 })).to.equal('€12.10')
|
||||
expect(formatValue({ unit: '€', nodeValue: 12, language: 'fr' })).to.equal(
|
||||
'12 €'
|
||||
)
|
||||
expect(formatValue({ value: 1200, language: 'fr' })).to.equal('1 200')
|
||||
expect(formatValue({ nodeValue: 1200, language: 'fr' })).to.equal('1 200')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Units handling', () => {
|
||||
it('translate unit', () => {
|
||||
expect(formatValue({ value: 1, unit: 'jour', language: 'fr' })).to.equal(
|
||||
'1 jour'
|
||||
)
|
||||
expect(formatValue({ value: 1, unit: 'jour', language: 'en' })).to.equal(
|
||||
'1 day'
|
||||
)
|
||||
expect(
|
||||
formatValue({ nodeValue: 1, unit: 'jour', language: 'fr' })
|
||||
).to.equal('1 jour')
|
||||
expect(
|
||||
formatValue({ nodeValue: 1, unit: 'jour', language: 'en' })
|
||||
).to.equal('1 day')
|
||||
})
|
||||
|
||||
it('pluralize unit', () => {
|
||||
expect(formatValue({ value: 2, unit: 'jour', language: 'fr' })).to.equal(
|
||||
'2 jours'
|
||||
)
|
||||
expect(
|
||||
formatValue({ value: 7, unit: parseUnit('jour/semaine'), language: 'fr' })
|
||||
formatValue({ nodeValue: 2, unit: 'jour', language: 'fr' })
|
||||
).to.equal('2 jours')
|
||||
expect(
|
||||
formatValue({
|
||||
nodeValue: 7,
|
||||
unit: parseUnit('jour/semaine'),
|
||||
language: 'fr'
|
||||
})
|
||||
).to.equal('7 jours / semaine')
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { serializeUnit } from 'Engine/units'
|
||||
import { memoizeWith } from 'ramda'
|
||||
import { Evaluation } from './types'
|
||||
import { Unit } from './units'
|
||||
|
||||
const NumberFormat = memoizeWith(
|
||||
|
@ -46,7 +47,7 @@ export const currencyFormat = (language: string | undefined) => ({
|
|||
export const formatCurrency = (value: number | undefined, language: string) => {
|
||||
return value == null
|
||||
? ''
|
||||
: (formatValue({ unit: '€', language, value }) ?? '').replace(
|
||||
: (formatNumber({ unit: '€', language, value }) ?? '').replace(
|
||||
/^(-)?€/,
|
||||
'$1€\u00A0'
|
||||
)
|
||||
|
@ -55,17 +56,17 @@ export const formatCurrency = (value: number | undefined, language: string) => {
|
|||
export const formatPercentage = (value: number | undefined) =>
|
||||
value == null
|
||||
? ''
|
||||
: formatValue({ unit: '%', value, maximumFractionDigits: 2 })
|
||||
: formatNumber({ unit: '%', value, maximumFractionDigits: 2 })
|
||||
|
||||
export type formatValueOptions = {
|
||||
maximumFractionDigits?: number
|
||||
minimumFractionDigits?: number
|
||||
language?: string
|
||||
unit?: Unit | string
|
||||
value?: number
|
||||
value: number
|
||||
}
|
||||
|
||||
export function formatValue({
|
||||
function formatNumber({
|
||||
maximumFractionDigits,
|
||||
minimumFractionDigits,
|
||||
language,
|
||||
|
@ -106,3 +107,44 @@ export function formatValue({
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
const booleanTranslations = {
|
||||
fr: { true: 'Oui', false: 'Non' },
|
||||
en: { true: 'Yes', false: 'No' }
|
||||
}
|
||||
|
||||
type ValueArg = {
|
||||
nodeValue: Evaluation
|
||||
language: string
|
||||
unit?: string | Unit
|
||||
precision?: number
|
||||
}
|
||||
|
||||
export function formatValue({
|
||||
nodeValue,
|
||||
language,
|
||||
unit,
|
||||
precision = 2
|
||||
}: ValueArg) {
|
||||
if (
|
||||
(typeof nodeValue === 'number' && Number.isNaN(nodeValue)) ||
|
||||
nodeValue === null
|
||||
) {
|
||||
return '-'
|
||||
}
|
||||
return typeof nodeValue === 'string'
|
||||
? nodeValue
|
||||
: typeof nodeValue === 'object'
|
||||
? (nodeValue as any).nom
|
||||
: typeof nodeValue === 'boolean'
|
||||
? booleanTranslations[language][nodeValue]
|
||||
: typeof nodeValue === 'number'
|
||||
? formatNumber({
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: precision,
|
||||
language,
|
||||
unit,
|
||||
value: nodeValue
|
||||
})
|
||||
: null
|
||||
}
|
||||
|
|
|
@ -1,60 +0,0 @@
|
|||
import {
|
||||
add,
|
||||
countBy,
|
||||
descend,
|
||||
flatten,
|
||||
head,
|
||||
identity,
|
||||
keys,
|
||||
map,
|
||||
mergeWith,
|
||||
pair,
|
||||
reduce,
|
||||
sortWith,
|
||||
toPairs,
|
||||
values
|
||||
} from 'ramda'
|
||||
import { DottedName } from 'Rules'
|
||||
|
||||
/*
|
||||
COLLECTE DES VARIABLES MANQUANTES
|
||||
*********************************
|
||||
on collecte les variables manquantes : celles qui sont nécessaires pour
|
||||
remplir les objectifs de la simulation (calculer des cotisations) mais qui n'ont pas
|
||||
encore été renseignées
|
||||
|
||||
TODO perf : peut-on le faire en même temps que l'on traverse l'AST ?
|
||||
Oui sûrement, cette liste se complète en remontant l'arbre. En fait, on le fait déjà pour nodeValue,
|
||||
et quand nodeValue vaut null, c'est qu'il y a des missingVariables ! Il suffit donc de remplacer les
|
||||
null par un tableau, et d'ailleurs utiliser des fonction d'aide pour mutualiser ces tests.
|
||||
|
||||
missingVariables: {variable: [objectives]}
|
||||
*/
|
||||
|
||||
type Explanation = {
|
||||
missingVariables: Array<DottedName>
|
||||
dottedName: DottedName
|
||||
}
|
||||
|
||||
export let getNextSteps = missingVariablesByTarget => {
|
||||
let byCount = ([, [count]]) => count
|
||||
let byScore = ([, [, score]]) => score
|
||||
|
||||
let missingByTotalScore = reduce(
|
||||
mergeWith(add),
|
||||
{},
|
||||
values(missingVariablesByTarget)
|
||||
)
|
||||
|
||||
let innerKeys = flatten(map(keys, values(missingVariablesByTarget))),
|
||||
missingByTargetsAdvanced = countBy(identity, innerKeys)
|
||||
|
||||
let missingByCompound = mergeWith(
|
||||
pair,
|
||||
missingByTargetsAdvanced,
|
||||
missingByTotalScore
|
||||
),
|
||||
pairs = toPairs(missingByCompound),
|
||||
sortedPairs = sortWith([descend(byCount), descend(byScore) as any], pairs)
|
||||
return map(head, sortedPairs)
|
||||
}
|
|
@ -21,21 +21,11 @@ let evaluateBottomUp = situationGate => startingFragments => {
|
|||
|
||||
return rec(startingFragments)
|
||||
}
|
||||
let formatBooleanValue = { oui: true, non: false }
|
||||
|
||||
export let getSituationValue = (situationGate, variableName, rule) => {
|
||||
// get the current situation value
|
||||
// it's the user input or test input, possibly with default values
|
||||
let value = situationGate(variableName)
|
||||
|
||||
if (rule.API) return typeof value == 'string' ? JSON.parse(value) : value
|
||||
|
||||
if (rule.unit != null) {
|
||||
return value == undefined ? value : +value
|
||||
}
|
||||
|
||||
// a leaf variable with an unit attribute is not boolean
|
||||
if (formatBooleanValue[value] !== undefined) return formatBooleanValue[value]
|
||||
if (rule.formule && rule.formule['une possibilité'])
|
||||
return evaluateBottomUp(situationGate)(splitName(variableName))
|
||||
|
||||
|
|
|
@ -1,24 +1,21 @@
|
|||
import { evaluateControls } from 'Engine/controls'
|
||||
import { ParsedRules, Rules } from 'Engine/types'
|
||||
import { convertNodeToUnit, simplifyNodeUnit } from 'Engine/nodeUnits'
|
||||
import { parse } from 'Engine/parse'
|
||||
import { EvaluatedNode, EvaluatedRule, ParsedRules, Rules } from 'Engine/types'
|
||||
import { parseUnit } from 'Engine/units'
|
||||
import { mapObjIndexed } from 'ramda'
|
||||
import { Simulation } from 'Reducers/rootReducer'
|
||||
import { evaluateNode } from './evaluation'
|
||||
import { evaluationError, warning } from './error'
|
||||
import { collectDefaults, evaluateNode } from './evaluation'
|
||||
import parseRules from './parseRules'
|
||||
import { collectDefaults } from './ruleUtils'
|
||||
import { parseUnit, Unit } from './units'
|
||||
|
||||
const emptyCache = {
|
||||
_meta: { contextRule: [], defaultUnits: [] }
|
||||
}
|
||||
|
||||
type EngineConfig<Names extends string> = {
|
||||
rules: string | Rules<Names> | ParsedRules<Names>
|
||||
useDefaultValues?: boolean
|
||||
}
|
||||
const emptyCache = () => ({
|
||||
_meta: { contextRule: [] }
|
||||
})
|
||||
|
||||
type Cache = {
|
||||
_meta: {
|
||||
contextRule: Array<string>
|
||||
defaultUnits: Array<Unit>
|
||||
inversionFail?: {
|
||||
given: string
|
||||
estimated: string
|
||||
|
@ -26,70 +23,135 @@ type Cache = {
|
|||
}
|
||||
}
|
||||
|
||||
export type EvaluationOptions = Partial<{
|
||||
unit: string
|
||||
useDefaultValues: boolean
|
||||
}>
|
||||
|
||||
export { default as translateRules } from './translateRules'
|
||||
export { parseRules }
|
||||
export default class Engine<Names extends string> {
|
||||
parsedRules: ParsedRules<Names>
|
||||
defaultValues: Simulation['situation']
|
||||
situation: Simulation['situation'] = {}
|
||||
cache: Cache = { ...emptyCache }
|
||||
cache: Cache
|
||||
cacheWithoutDefault: Cache
|
||||
|
||||
constructor({ rules, useDefaultValues = true }: EngineConfig<Names>) {
|
||||
constructor(rules: string | Rules<Names> | ParsedRules<Names>) {
|
||||
this.cache = emptyCache()
|
||||
this.cacheWithoutDefault = emptyCache()
|
||||
this.parsedRules =
|
||||
typeof rules === 'string' || !(Object.values(rules)[0] as any)?.dottedName
|
||||
? parseRules(rules)
|
||||
: (rules as ParsedRules<Names>)
|
||||
this.defaultValues = useDefaultValues
|
||||
? collectDefaults(this.parsedRules)
|
||||
: {}
|
||||
|
||||
this.defaultValues = mapObjIndexed(
|
||||
(value, name) =>
|
||||
typeof value === 'string'
|
||||
? this.evaluateExpression(value, `[valeur par défaut] ${name}`, false)
|
||||
: value,
|
||||
collectDefaults(this.parsedRules)
|
||||
)
|
||||
}
|
||||
|
||||
private resetCache() {
|
||||
this.cache = { ...emptyCache }
|
||||
this.cache = emptyCache()
|
||||
this.cacheWithoutDefault = emptyCache()
|
||||
}
|
||||
|
||||
setSituation(situation: Simulation['situation'] = {}) {
|
||||
this.situation = situation
|
||||
this.resetCache()
|
||||
return this
|
||||
private situationGate(useDefaultValues = true) {
|
||||
return dottedName =>
|
||||
this.situation[dottedName] ??
|
||||
(useDefaultValues ? this.defaultValues[dottedName] : null)
|
||||
}
|
||||
|
||||
setDefaultUnits(defaultUnits: string[] = []) {
|
||||
this.cache._meta.defaultUnits = defaultUnits.map(unit =>
|
||||
parseUnit(unit)
|
||||
) as any
|
||||
return this
|
||||
}
|
||||
|
||||
evaluate(expression: string | Array<string>) {
|
||||
const results = (Array.isArray(expression) ? expression : [expression]).map(
|
||||
expr =>
|
||||
this.cache[expr] ||
|
||||
(this.parsedRules[expr]
|
||||
? evaluateNode(
|
||||
this.cache,
|
||||
this.situationGate,
|
||||
this.parsedRules,
|
||||
this.parsedRules[expr]
|
||||
)
|
||||
: // TODO: To support expressions (with operations, unit conversion,
|
||||
// etc.) it should be enough to replace the above line with :
|
||||
// parse(this.parsedRules, { dottedName: '' }, this.parsedRules)(expr)
|
||||
// But currently there are small side effects (null values converted
|
||||
// to 0), so we need to modify a little bit the engine before enabling
|
||||
// publicode expressions in the UI.
|
||||
|
||||
null)
|
||||
private evaluateExpression(
|
||||
expression: string,
|
||||
context: string,
|
||||
useDefaultValues: boolean = true
|
||||
): EvaluatedRule<Names> {
|
||||
const result = simplifyNodeUnit(
|
||||
evaluateNode(
|
||||
useDefaultValues ? this.cache : this.cacheWithoutDefault,
|
||||
this.situationGate(useDefaultValues),
|
||||
this.parsedRules,
|
||||
parse(
|
||||
this.parsedRules,
|
||||
{ dottedName: context },
|
||||
this.parsedRules
|
||||
)(expression)
|
||||
)
|
||||
)
|
||||
return Array.isArray(expression) ? results : results[0]
|
||||
|
||||
if (Object.keys(result.defaultValue?.missingVariable ?? {}).length) {
|
||||
throw new evaluationError(
|
||||
context,
|
||||
"Impossible d'évaluer l'expression car celle ci fait appel à des variables manquantes"
|
||||
)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
setSituation(
|
||||
situation: Partial<Record<Names, string | number | object>> = {}
|
||||
) {
|
||||
this.resetCache()
|
||||
this.situation = mapObjIndexed(
|
||||
(value, name) =>
|
||||
typeof value === 'string'
|
||||
? this.evaluateExpression(value, `[situation] ${name}`, true)
|
||||
: value,
|
||||
situation
|
||||
)
|
||||
return this
|
||||
}
|
||||
|
||||
evaluate(expression: Names, options?: EvaluationOptions): EvaluatedRule<Names>
|
||||
evaluate(
|
||||
expression: string,
|
||||
options?: EvaluationOptions
|
||||
): EvaluatedNode<Names>
|
||||
evaluate(
|
||||
expression: string,
|
||||
options?: EvaluationOptions
|
||||
): EvaluatedNode<Names> {
|
||||
let result = this.evaluateExpression(
|
||||
expression,
|
||||
`[evaluation] ${expression}`,
|
||||
options?.useDefaultValues ?? true
|
||||
)
|
||||
if (result.category === 'reference' && result.explanation) {
|
||||
result = result.explanation
|
||||
}
|
||||
if (options?.unit) {
|
||||
try {
|
||||
return convertNodeToUnit(
|
||||
parseUnit(options.unit),
|
||||
result as EvaluatedNode<Names, number>
|
||||
)
|
||||
} catch (e) {
|
||||
warning(
|
||||
`[evaluation] ${expression}`,
|
||||
"L'unité demandée est incompatible avec l'expression évaluée"
|
||||
)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
controls() {
|
||||
return evaluateControls(this.cache, this.situationGate, this.parsedRules)
|
||||
return evaluateControls(this.cache, this.situationGate(), this.parsedRules)
|
||||
}
|
||||
|
||||
inversionFail(): boolean {
|
||||
return !!this.cache._meta.inversionFail
|
||||
}
|
||||
|
||||
getParsedRules(): ParsedRules<Names> {
|
||||
return this.parsedRules
|
||||
}
|
||||
|
||||
// TODO : this should be private
|
||||
getCache(): Cache {
|
||||
return this.cache
|
||||
}
|
||||
situationGate = (dottedName: string) =>
|
||||
this.situation[dottedName] ?? this.defaultValues[dottedName]
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@ export default function Allègement(nodeValue, rawExplanation) {
|
|||
</li>
|
||||
)}
|
||||
{explanation.plafond && (
|
||||
<li key="abattement">
|
||||
<li key="plafond">
|
||||
<span className="key">plafond: </span>
|
||||
<span className="value">{makeJsx(explanation.plafond)}</span>
|
||||
</li>
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
padding: 0.1em 0.4em;
|
||||
}
|
||||
.barème table th {
|
||||
font-weight: 500;
|
||||
font-weight: 600;
|
||||
}
|
||||
.barème table th:first-letter {
|
||||
text-transform: uppercase;
|
||||
|
|
|
@ -4,8 +4,9 @@ import { Trans } from 'react-i18next'
|
|||
import { makeJsx } from '../evaluation'
|
||||
import './Barème.css'
|
||||
import { Node, NodeValuePointer } from './common'
|
||||
import { parseUnit } from 'Engine/units'
|
||||
|
||||
export default function Barème(nodeValue, explanation, _, unit) {
|
||||
export default function Barème(nodeValue, explanation, unit) {
|
||||
return (
|
||||
<Node classes="mecanism barème" name="barème" value={nodeValue} unit={unit}>
|
||||
<ul className="properties">
|
||||
|
@ -22,7 +23,7 @@ export default function Barème(nodeValue, explanation, _, unit) {
|
|||
</b>
|
||||
<NodeValuePointer
|
||||
data={(100 * nodeValue) / explanation.assiette.nodeValue}
|
||||
unit="%"
|
||||
unit={parseUnit('%')}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
|
|
@ -72,6 +72,6 @@ let Comp = function Composantes({ nodeValue, explanation, unit }) {
|
|||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
export default (nodeValue, explanation, _, unit) => (
|
||||
export default (nodeValue, explanation, unit) => (
|
||||
<Comp {...{ nodeValue, explanation, unit }} />
|
||||
)
|
||||
|
|
|
@ -3,7 +3,7 @@ import { BarèmeAttributes, TrancheTable } from './Barème'
|
|||
import './Barème.css'
|
||||
import { Node } from './common'
|
||||
|
||||
export default function Grille(nodeValue, explanation, _, unit) {
|
||||
export default function Grille(nodeValue, explanation, unit) {
|
||||
return (
|
||||
<Node classes="mecanism barème" name="grille" value={nodeValue} unit={unit}>
|
||||
<ul className="properties">
|
||||
|
|
|
@ -1,49 +1,53 @@
|
|||
import { ShowValuesConsumer } from 'Components/rule/ShowValuesContext'
|
||||
import { makeJsx } from 'Engine/evaluation'
|
||||
import { Leaf } from 'Engine/mecanismViews/common'
|
||||
import React from 'react'
|
||||
import { Node } from './common'
|
||||
import './InversionNumérique.css'
|
||||
|
||||
let Comp = function InversionNumérique({ nodeValue, explanation }) {
|
||||
return (
|
||||
<ShowValuesConsumer>
|
||||
{showValues => (
|
||||
<Node
|
||||
classes="mecanism inversionNumérique"
|
||||
name="inversion numérique"
|
||||
value={nodeValue}
|
||||
>
|
||||
{!showValues || explanation.inversedWith?.value == null ? (
|
||||
<>
|
||||
<p>
|
||||
Cette formule de calcul n'existe pas ! Mais on peut faire une
|
||||
estimation à partir de :
|
||||
</p>
|
||||
<ul id="inversionsPossibles">
|
||||
{explanation.avec.map(el => (
|
||||
<li key={el.name}>{makeJsx(el)}</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{' '}
|
||||
<p>
|
||||
Cette valeur a été estimée à partir d'une autre variable qui
|
||||
possède une formule de calcul et dont la valeur a été fixée dans
|
||||
la simulation :
|
||||
</p>
|
||||
<Leaf
|
||||
classes="variable"
|
||||
dottedName={explanation.inversedWith.rule.dottedName}
|
||||
value={explanation.inversedWith.value}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Node>
|
||||
<Node
|
||||
classes="mecanism inversionNumérique"
|
||||
name="inversion numérique"
|
||||
value={nodeValue}
|
||||
>
|
||||
{explanation.inversionFailed ? (
|
||||
<>
|
||||
{' '}
|
||||
<p>
|
||||
Cette valeur devrait pouvoir être estimée à partir d'une autre
|
||||
variable qui possède une formule de calcul et dont la valeur a été
|
||||
fixée dans la simulation :
|
||||
</p>
|
||||
{makeJsx(explanation.inversedWith)}
|
||||
<p>
|
||||
Malheureusement, il a été impossible de retrouver une valeur pour
|
||||
cette formule qui permette d'atterir sur la valeur demandée.
|
||||
</p>
|
||||
</>
|
||||
) : explanation.inversedWith ? (
|
||||
<>
|
||||
{' '}
|
||||
<p>
|
||||
Cette valeur a été estimée à partir d'une autre variable qui possède
|
||||
une formule de calcul et dont la valeur a été fixée dans la
|
||||
simulation :
|
||||
</p>
|
||||
{makeJsx(explanation.inversedWith)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p>
|
||||
Cette formule de calcul n'existe pas, mais on peut la calculer par
|
||||
inversion en utilisant les formules des règles suivantes :
|
||||
</p>
|
||||
<ul id="inversionsPossibles">
|
||||
{explanation.inversionCandidates.map(el => (
|
||||
<li key={el.dottedName}>{makeJsx(el)}</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</ShowValuesConsumer>
|
||||
</Node>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import { Trans } from 'react-i18next'
|
|||
import { Node } from './common'
|
||||
import './InversionNumérique.css'
|
||||
|
||||
export default function ProductView(nodeValue, explanation, _, unit) {
|
||||
export default function ProductView(nodeValue, explanation, unit) {
|
||||
return (
|
||||
// The rate and factor and threshold are given defaut neutral values. If there is nothing to explain, don't display them at all
|
||||
<Node
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue