1
0
Fork 0
mirror of https://github.com/betagouv/mon-entreprise synced 2025-02-11 00:25:02 +00:00

refactor(lodeom): factorise le code commun avec RGCP

This commit is contained in:
Alice Dahan 2024-12-20 13:00:54 +01:00 committed by liliced
parent 2b3de4c2aa
commit 4f93c72324
28 changed files with 687 additions and 1896 deletions

View file

@ -479,6 +479,12 @@ salarié . cotisations . exonérations . lodeom . montant:
- lodeom . montant
- T . sécurité sociale et chômage / T
imputation chômage:
non applicable si: lodeom . zone deux
produit:
- lodeom . montant
- chômage . employeur . taux / T
salarié . cotisations . exonérations . JEI:
question:
variations:

View file

@ -1,7 +1,7 @@
import { checkA11Y, fr } from '../../support/utils'
// TODO Échoue parfois … à creuser
describe.skip(
describe(
'Simulateur réduction générale',
{ testIsolation: false },
function () {
@ -206,6 +206,7 @@ describe.skip(
})
it('should handle incomplete months', function () {
cy.contains('Régularisation progressive').click()
cy.get(inputSelector).first().type('{selectall}1500')
cy.get('input[id="option-heures-sup-janvier"]').type('{selectall}5')
cy.get(
@ -251,11 +252,23 @@ describe.skip(
})
// Wait for values to update
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(500)
cy.get('#recap-1er_trimestre-671').should('include.text', '682,24 €')
cy.get('#recap-2ème_trimestre-801').should('include.text', '-186,36 €')
cy.get('#recap-3ème_trimestre-671').should('include.text', '1569,81 €')
cy.get('#recap-4ème_trimestre-671').should('include.text', '1568,39 €')
cy.wait(1000)
cy.get('#recap-1er_trimestre-réduction').should(
'include.text',
'682,24 €'
)
cy.get('#recap-2ème_trimestre-régularisation').should(
'include.text',
'-186,36 €'
)
cy.get('#recap-3ème_trimestre-réduction').should(
'include.text',
'1569,81 €'
)
cy.get('#recap-4ème_trimestre-réduction').should(
'include.text',
'1568,39 €'
)
})
it('should be RGAA compliant', function () {

View file

@ -1,50 +1,56 @@
import { formatValue } from 'publicodes'
import { formatValue, PublicodesExpression } from 'publicodes'
import { ReactNode } from 'react'
import { useTranslation } from 'react-i18next'
import { styled } from 'styled-components'
import { Condition } from '@/components/EngineValue/Condition'
import Répartition from '@/components/RéductionDeCotisations/Répartition'
import { FlexCenter } from '@/design-system/global-style'
import { SearchIcon, WarningIcon } from '@/design-system/icons'
import { Tooltip } from '@/design-system/tooltip'
import {
RéductionDottedName,
rémunérationBruteDottedName,
Répartition as RépartitionType,
} from '../utils'
import Répartition from './Répartition'
import WarningSalaireTrans from './WarningSalaireTrans'
} from '@/utils/réductionDeCotisations'
type Props = {
id?: string
dottedName: RéductionDottedName
rémunérationBrute: number
réductionGénérale: number
réduction: number
répartition: RépartitionType
displayedUnit: string
language: string
displayNull?: boolean
warningCondition?: PublicodesExpression
warningTooltip?: ReactNode
alignment?: 'center' | 'end'
}
export default function MontantRéduction({
export default function MontantAvecRépartition({
id,
dottedName,
rémunérationBrute,
réductionGénérale,
réduction,
répartition,
displayedUnit,
language,
displayNull = true,
warningCondition,
warningTooltip,
alignment = 'end',
}: Props) {
const { t } = useTranslation()
const tooltip = <Répartition répartition={répartition} />
const tooltip = (
<Répartition dottedName={dottedName} répartition={répartition} />
)
return réductionGénérale ? (
return réduction ? (
<StyledTooltip tooltip={tooltip}>
<FlexDiv id={id} $alignment={alignment}>
{formatValue(
{
nodeValue: réductionGénérale,
nodeValue: réduction,
},
{
displayedUnit,
@ -55,17 +61,17 @@ export default function MontantRéduction({
</FlexDiv>
</StyledTooltip>
) : (
displayNull && (
!!warningCondition && !!warningTooltip && (
<FlexDiv id={id} $alignment={alignment}>
{formatValue(0, { displayedUnit, language })}
<Condition
expression={`${rémunérationBruteDottedName} > 1.6 * SMIC`}
expression={warningCondition}
contexte={{
[rémunérationBruteDottedName]: rémunérationBrute,
}}
>
<Tooltip tooltip={<WarningSalaireTrans />}>
<Tooltip tooltip={warningTooltip}>
<span className="sr-only">{t('Attention')}</span>
<StyledWarningIcon aria-label={t('Attention')} />
</Tooltip>

View file

@ -19,8 +19,7 @@ import { Strong } from '@/design-system/typography'
import { Li, Ul } from '@/design-system/typography/list'
import { Body, SmallBody } from '@/design-system/typography/paragraphs'
import { useMediaQuery } from '@/hooks/useMediaQuery'
import { Options } from '../pages/simulateurs/reduction-generale/utils'
import { Options } from '@/utils/réductionDeCotisations'
type Props = {
month: string

View file

@ -2,15 +2,17 @@ import { sumAll } from 'effect/Number'
import { useTranslation } from 'react-i18next'
import { styled } from 'styled-components'
import MontantAvecRépartition from '@/components/RéductionDeCotisations/MontantAvecRépartition'
import { Grid } from '@/design-system/layout'
import { Body } from '@/design-system/typography/paragraphs'
import { MonthState } from '../utils'
import MontantRéduction from './MontantRéduction'
import { MonthState, RéductionDottedName } from '@/utils/réductionDeCotisations'
type Props = {
dottedName: RéductionDottedName
label: string
data: MonthState[]
codeRéduction?: string
codeRégularisation?: string
mobileVersion?: boolean
}
@ -20,8 +22,11 @@ export type RémunérationBruteInput = {
}
export default function RécapitulatifTrimestre({
dottedName,
label,
data,
codeRéduction,
codeRégularisation,
mobileVersion = false,
}: Props) {
const { t, i18n } = useTranslation()
@ -35,19 +40,26 @@ export default function RécapitulatifTrimestre({
IRC: sumAll(
data.map(
(monthData) =>
monthData.lodeom.répartition.IRC +
monthData.réduction.répartition.IRC +
monthData.régularisation.répartition.IRC
)
),
Urssaf: sumAll(
data.map(
(monthData) =>
monthData.lodeom.répartition.Urssaf +
monthData.réduction.répartition.Urssaf +
monthData.régularisation.répartition.Urssaf
)
),
chômage: sumAll(
data.map(
(monthData) =>
monthData.réduction.répartition.chômage +
monthData.régularisation.répartition.chômage
)
),
}
let réduction = sumAll(data.map((monthData) => monthData.lodeom.value))
let réduction = sumAll(data.map((monthData) => monthData.réduction.value))
let régularisation = sumAll(
data.map((monthData) => monthData.régularisation.value)
)
@ -59,16 +71,16 @@ export default function RécapitulatifTrimestre({
réduction = 0
}
const MontantExonération = () => {
const MontantRéduction = () => {
return (
<MontantRéduction
<MontantAvecRépartition
id={`recap-${label.replace(/\s|\./g, '_')}-réduction`}
dottedName={dottedName}
rémunérationBrute={rémunération}
lodeom={réduction}
réduction={réduction}
répartition={répartition}
displayedUnit={displayedUnit}
language={language}
displayNull={false}
alignment="center"
/>
)
@ -76,14 +88,14 @@ export default function RécapitulatifTrimestre({
const MontantRégularisation = () => {
return (
<MontantRéduction
<MontantAvecRépartition
id={`recap-${label.replace(/\s|\./g, '_')}-régularisation`}
dottedName={dottedName}
rémunérationBrute={rémunération}
lodeom={régularisation}
réduction={régularisation}
répartition={répartition}
displayedUnit={displayedUnit}
language={language}
displayNull={false}
alignment="center"
/>
)
@ -96,19 +108,20 @@ export default function RécapitulatifTrimestre({
<Grid item>
<StyledBody>
{t(
'pages.simulateurs.lodeom.recap.header.réduction',
'pages.simulateurs.réduction-générale.recap.header-réduction',
'Réduction calculée'
)}
{/* <br />
{t(
'pages.simulateurs.lodeom.recap.code671',
'code 671(€)'
)} */}
{codeRéduction && (
<>
<br />
{codeRéduction}
</>
)}
</StyledBody>
</Grid>
<Grid item>
<StyledBody>
<MontantExonération />
<MontantRéduction />
</StyledBody>
</Grid>
</GridContainer>
@ -117,14 +130,15 @@ export default function RécapitulatifTrimestre({
<Grid item>
<StyledBody>
{t(
'pages.simulateurs.lodeom.recap.header.régularisation',
'pages.simulateurs.réduction-générale.recap.header-régularisation',
'Régularisation calculée'
)}
{/* <br />
{t(
'pages.simulateurs.lodeom.recap.code801',
'code 801(€)'
)} */}
{codeRégularisation && (
<>
<br />
{codeRégularisation}
</>
)}
</StyledBody>
</Grid>
<Grid item>
@ -138,7 +152,7 @@ export default function RécapitulatifTrimestre({
<tr>
<th scope="row">{label}</th>
<td>
<MontantExonération />
<MontantRéduction />
</td>
<td>
<MontantRégularisation />

View file

@ -1,7 +1,10 @@
import { PublicodesExpression } from 'publicodes'
import { ReactNode } from 'react'
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { Condition } from '@/components/EngineValue/Condition'
import Répartition from '@/components/RéductionDeCotisations/Répartition'
import { SimulationGoal } from '@/components/Simulation'
import { SimulationValue } from '@/components/Simulation/SimulationValue'
import { useEngine } from '@/components/utils/EngineContext'
@ -9,33 +12,32 @@ import { Message } from '@/design-system'
import { Spacing } from '@/design-system/layout'
import { Body } from '@/design-system/typography/paragraphs'
import { targetUnitSelector } from '@/store/selectors/simulationSelectors'
import Répartition from './components/Répartition'
import Warnings from './components/Warnings'
import WarningSalaireTrans from './components/WarningSalaireTrans'
import { lodeomDottedName, rémunérationBruteDottedName } from './utils'
import {
getRépartitionBasique,
RéductionDottedName,
rémunérationBruteDottedName,
} from '@/utils/réductionDeCotisations'
type Props = {
dottedName: RéductionDottedName
onUpdate: () => void
warnings: ReactNode
warningCondition: PublicodesExpression
warningMessage: ReactNode
}
export default function LodeomBasique({ onUpdate }: Props) {
export default function RéductionBasique({
dottedName,
onUpdate,
warnings,
warningCondition,
warningMessage,
}: Props) {
const engine = useEngine()
const currentUnit = useSelector(targetUnitSelector)
const { t } = useTranslation()
const répartition = {
IRC:
(engine.evaluate({
valeur: `${lodeomDottedName} . imputation retraite complémentaire`,
unité: currentUnit,
})?.nodeValue as number) ?? 0,
Urssaf:
(engine.evaluate({
valeur: `${lodeomDottedName} . imputation sécurité sociale`,
unité: currentUnit,
})?.nodeValue as number) ?? 0,
}
const répartition = getRépartitionBasique(dottedName, currentUnit, engine)
return (
<>
@ -46,23 +48,22 @@ export default function LodeomBasique({ onUpdate }: Props) {
onUpdateSituation={onUpdate}
/>
<Warnings />
<Condition expression="salarié . cotisations . exonérations . lodeom . montant = 0">
{warnings}
<Condition expression={warningCondition}>
<Message type="info">
<Body>
<WarningSalaireTrans />
</Body>
<Body>{warningMessage}</Body>
</Message>
</Condition>
<Condition expression={`${lodeomDottedName} >= 0`}>
<Condition expression={`${dottedName} >= 0`}>
<SimulationValue
dottedName={lodeomDottedName}
dottedName={dottedName}
isInfoMode={true}
round={false}
/>
<Spacing md />
<Répartition répartition={répartition} />
<Répartition dottedName={dottedName} répartition={répartition} />
</Condition>
</>
)

View file

@ -1,10 +1,11 @@
import { PublicodesExpression } from 'publicodes'
import { useState } from 'react'
import { ReactNode, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { styled } from 'styled-components'
import NumberInput from '@/components/conversation/NumberInput'
import MonthOptions from '@/components/MonthOptions'
import MontantAvecRépartition from '@/components/RéductionDeCotisations/MontantAvecRépartition'
import MonthOptions from '@/components/RéductionDeCotisations/MonthOptions'
import RuleLink from '@/components/RuleLink'
import { useEngine } from '@/components/utils/EngineContext'
import { Button } from '@/design-system/buttons'
@ -12,35 +13,36 @@ import { FlexCenter } from '@/design-system/global-style'
import { RotatingChevronIcon } from '@/design-system/icons'
import { Grid, Spacing } from '@/design-system/layout'
import { Body } from '@/design-system/typography/paragraphs'
import {
MonthState,
Options,
RéductionDottedName,
réductionGénéraleDottedName,
rémunérationBruteDottedName,
} from '../utils'
import MontantRéduction from './MontantRéduction'
RémunérationBruteInput,
} from '@/utils/réductionDeCotisations'
type Props = {
dottedName: RéductionDottedName
monthName: string
data: MonthState
index: number
onRémunérationChange: (monthIndex: number, rémunérationBrute: number) => void
onOptionsChange: (monthIndex: number, options: Options) => void
warningCondition: PublicodesExpression
warningTooltip: ReactNode
mobileVersion?: boolean
}
export type RémunérationBruteInput = {
unité: string
valeur: number
}
export default function RéductionGénéraleMois({
export default function RéductionMois({
dottedName,
monthName,
data,
index,
onRémunérationChange,
onOptionsChange,
warningCondition,
warningTooltip,
mobileVersion = false,
}: Props) {
const { t, i18n } = useTranslation()
@ -52,7 +54,7 @@ export default function RéductionGénéraleMois({
const RémunérationInput = () => {
// TODO: enlever les 4 premières props après résolution de #3123
const ruleInputProps = {
dottedName: rémunérationBruteDottedName,
dottedName,
suggestions: {},
description: undefined,
question: undefined,
@ -109,35 +111,32 @@ export default function RéductionGénéraleMois({
)
}
const MontantRéductionGénérale = () => {
const MontantRéduction = () => {
return (
<MontantRéduction
id={`${réductionGénéraleDottedName.replace(
/\s|\./g,
'_'
)}-${monthName}`}
<MontantAvecRépartition
id={`${dottedName.replace(/\s|\./g, '_')}-${monthName}`}
dottedName={dottedName}
rémunérationBrute={data.rémunérationBrute}
réductionGénérale={data.réductionGénérale.value}
répartition={data.réductionGénérale.répartition}
réduction={data.réduction.value}
répartition={data.réduction.répartition}
displayedUnit={displayedUnit}
language={language}
warningCondition={warningCondition}
warningTooltip={warningTooltip}
/>
)
}
const MontantRégularisation = () => {
return (
<MontantRéduction
id={`${réductionGénéraleDottedName.replace(
/\s|\./g,
'_'
)}__régularisation-${monthName}`}
<MontantAvecRépartition
id={`${dottedName.replace(/\s|\./g, '_')}__régularisation-${monthName}`}
dottedName={dottedName}
rémunérationBrute={data.rémunérationBrute}
réductionGénérale={data.régularisation.value}
réduction={data.régularisation.value}
répartition={data.régularisation.répartition}
displayedUnit={displayedUnit}
language={language}
displayNull={false}
/>
)
}
@ -172,18 +171,20 @@ export default function RéductionGénéraleMois({
<GridContainer container spacing={2}>
<Grid item>
<RuleLink dottedName={réductionGénéraleDottedName} />
<RuleLink dottedName={dottedName} />
</Grid>
<Grid item>
<StyledBody>
<MontantRéductionGénérale />
<MontantRéduction />
</StyledBody>
</Grid>
</GridContainer>
<GridContainer container spacing={2}>
<Grid item>
<RuleLink dottedName="salarié . cotisations . exonérations . réduction générale . régularisation" />
<RuleLink
dottedName={`${réductionGénéraleDottedName} . régularisation`}
/>
</Grid>
<Grid item>
<StyledBody>
@ -203,7 +204,7 @@ export default function RéductionGénéraleMois({
</InputContainer>
</td>
<td>
<MontantRéductionGénérale />
<MontantRéduction />
</td>
<td>
<MontantRégularisation />

View file

@ -1,3 +1,5 @@
import { PublicodesExpression } from 'publicodes'
import { ReactNode } from 'react'
import { useTranslation } from 'react-i18next'
import { styled } from 'styled-components'
@ -6,22 +8,40 @@ import { Spacing } from '@/design-system/layout'
import { baseTheme } from '@/design-system/theme'
import { H3 } from '@/design-system/typography/heading'
import { useMediaQuery } from '@/hooks/useMediaQuery'
import {
MonthState,
Options,
RéductionDottedName,
réductionGénéraleDottedName,
} from '@/utils/réductionDeCotisations'
import RécapitulatifTrimestre from './components/RécapitulatifTrimestre'
import RéductionGénéraleMois from './components/RéductionGénéraleMois'
import Warnings from './components/Warnings'
import { MonthState, Options, réductionGénéraleDottedName } from './utils'
import RécapitulatifTrimestre from './RécapitulatifTrimestre'
import RéductionMois from './RéductionMois'
type Props = {
dottedName: RéductionDottedName
data: MonthState[]
onRémunérationChange: (monthIndex: number, rémunérationBrute: number) => void
onOptionsChange: (monthIndex: number, options: Options) => void
caption: string
warnings: ReactNode
warningCondition: PublicodesExpression
warningTooltip: ReactNode
codeRéduction?: string
codeRégularisation?: string
}
export default function RéductionGénéraleMoisParMois({
export default function RéductionMoisParMois({
dottedName,
data,
onRémunérationChange,
onOptionsChange,
caption,
warnings,
warningCondition,
warningTooltip,
codeRéduction,
codeRégularisation,
}: Props) {
const { t } = useTranslation()
const isDesktop = useMediaQuery(
@ -58,19 +78,9 @@ export default function RéductionGénéraleMoisParMois({
<>
{isDesktop ? (
<>
<H3 as="h2">
{t(
'pages.simulateurs.réduction-générale.month-by-month.caption',
'Réduction générale mois par mois :'
)}
</H3>
<H3 as="h2">{caption}</H3>
<StyledTable>
<caption className="sr-only">
{t(
'pages.simulateurs.réduction-générale.month-by-month.caption',
'Réduction générale mois par mois :'
)}
</caption>
<caption className="sr-only">{caption}</caption>
<thead>
<tr>
<th scope="col">{t('Mois')}</th>
@ -79,18 +89,21 @@ export default function RéductionGénéraleMoisParMois({
<RuleLink dottedName="salarié . rémunération . brut" />
</th>
<th scope="col">
<RuleLink dottedName={réductionGénéraleDottedName} />
<RuleLink dottedName={dottedName} />
</th>
<th scope="col">
<RuleLink dottedName="salarié . cotisations . exonérations . réduction générale . régularisation" />
<RuleLink
dottedName={`${réductionGénéraleDottedName} . régularisation`}
/>
</th>
</tr>
</thead>
<tbody>
{data.length > 0 &&
months.map((monthName, monthIndex) => (
<RéductionGénéraleMois
<RéductionMois
key={`month-${monthIndex}`}
dottedName={dottedName}
monthName={monthName}
data={data[monthIndex]}
index={monthIndex}
@ -103,6 +116,8 @@ export default function RéductionGénéraleMoisParMois({
onOptionsChange={(monthIndex: number, options: Options) => {
onOptionsChange(monthIndex, options)
}}
warningCondition={warningCondition}
warningTooltip={warningTooltip}
/>
))}
</tbody>
@ -128,24 +143,26 @@ export default function RéductionGénéraleMoisParMois({
<th scope="col">{t('Trimestre')}</th>
<th scope="col">
{t(
'pages.simulateurs.réduction-générale.recap.header',
'pages.simulateurs.réduction-générale.recap.header-réduction',
'Réduction calculée'
)}
<br />
{t(
'pages.simulateurs.réduction-générale.recap.code671',
'code 671(€)'
{codeRéduction && (
<>
<br />
{codeRéduction}
</>
)}
</th>
<th scope="col">
{t(
'pages.simulateurs.réduction-générale.recap.header',
'Réduction calculée'
'pages.simulateurs.réduction-générale.recap.header-régularisation',
'Régularisation calculée'
)}
<br />
{t(
'pages.simulateurs.réduction-générale.recap.code801',
'code 801(€)'
{codeRégularisation && (
<>
<br />
{codeRégularisation}
</>
)}
</th>
</tr>
@ -154,8 +171,11 @@ export default function RéductionGénéraleMoisParMois({
{Object.keys(quarters).map((label, index) => (
<RécapitulatifTrimestre
key={index}
dottedName={dottedName}
label={label}
data={quarters[label]}
codeRéduction={codeRéduction}
codeRégularisation={codeRégularisation}
/>
))}
</tbody>
@ -163,16 +183,12 @@ export default function RéductionGénéraleMoisParMois({
</>
) : (
<>
<H3 as="h2">
{t(
'pages.simulateurs.réduction-générale.month-by-month.caption',
'Réduction générale mois par mois :'
)}
</H3>
<H3 as="h2">{caption}</H3>
{data.length > 0 &&
months.map((monthName, monthIndex) => (
<RéductionGénéraleMois
<RéductionMois
key={`month-${monthIndex}`}
dottedName={dottedName}
monthName={monthName}
data={data[monthIndex]}
index={monthIndex}
@ -185,6 +201,8 @@ export default function RéductionGénéraleMoisParMois({
onOptionsChange={(monthIndex: number, options: Options) => {
onOptionsChange(monthIndex, options)
}}
warningCondition={warningCondition}
warningTooltip={warningTooltip}
mobileVersion={true}
/>
))}
@ -200,8 +218,11 @@ export default function RéductionGénéraleMoisParMois({
{Object.keys(quarters).map((label, index) => (
<RécapitulatifTrimestre
key={index}
dottedName={dottedName}
label={label}
data={quarters[label]}
codeRéduction={codeRéduction}
codeRégularisation={codeRégularisation}
mobileVersion={true}
/>
))}
@ -215,7 +236,7 @@ export default function RéductionGénéraleMoisParMois({
)}
</span>
<Warnings />
{warnings}
</>
)
}

View file

@ -1,8 +1,7 @@
import { useTranslation } from 'react-i18next'
import { Radio, ToggleGroup } from '@/design-system'
import { RégularisationMethod } from '../pages/simulateurs/reduction-generale/utils'
import { RégularisationMethod } from '@/utils/réductionDeCotisations'
type Props = {
régularisationMethod: RégularisationMethod

View file

@ -1,21 +1,21 @@
import { Trans, useTranslation } from 'react-i18next'
import { styled } from 'styled-components'
import RépartitionValue from '@/components/RépartitionValue'
import RépartitionValue from '@/components/RéductionDeCotisations/RépartitionValue'
import { Strong } from '@/design-system/typography'
import { Li, Ul } from '@/design-system/typography/list'
import { Body } from '@/design-system/typography/paragraphs'
import {
réductionGénéraleDottedName,
RéductionDottedName,
Répartition as RépartitionType,
} from '../utils'
} from '@/utils/réductionDeCotisations'
type Props = {
dottedName: RéductionDottedName
répartition: RépartitionType
}
export default function Répartition({ répartition }: Props) {
export default function Répartition({ dottedName, répartition }: Props) {
const { t } = useTranslation()
return (
@ -33,7 +33,7 @@ export default function Répartition({ répartition }: Props) {
'pages.simulateurs.réduction-générale.répartition.retraite',
'IRC'
)}
idPrefix={`${réductionGénéraleDottedName} . imputation retraite complémentaire`.replace(
idPrefix={`${dottedName} . imputation retraite complémentaire`.replace(
/\s|\./g,
'_'
)}
@ -46,7 +46,7 @@ export default function Répartition({ répartition }: Props) {
'pages.simulateurs.réduction-générale.répartition.urssaf',
'URSSAF'
)}
idPrefix={`${réductionGénéraleDottedName} . imputation sécurité sociale`.replace(
idPrefix={`${dottedName} . imputation sécurité sociale`.replace(
/\s|\./g,
'_'
)}
@ -57,7 +57,7 @@ export default function Répartition({ répartition }: Props) {
'pages.simulateurs.réduction-générale.répartition.chômage',
'dont chômage'
)}
idPrefix={`${réductionGénéraleDottedName} . imputation chômage`.replace(
idPrefix={`${dottedName} . imputation chômage`.replace(
/\s|\./g,
'_'
)}

View file

@ -9490,6 +9490,9 @@ salarié . cotisations . exonérations . lodeom . montant:
coefficient:
titre.en: '[automatic] coefficient'
titre.fr: coefficient
imputation chômage:
titre.en: '[automatic] unemployment allocation'
titre.fr: imputation chômage
imputation retraite complémentaire:
titre.en: '[automatic] imputation of supplementary pension'
titre.fr: imputation retraite complémentaire

View file

@ -1455,20 +1455,10 @@ pages:
title: Lodeom exemption
month-by-month:
caption: "Lodeom exemption month by month :"
options:
description: Adds fields to modulate employee activity
recap:
T1: 1st quarter
T2: 2nd quarter
T3: 3rd quarter
T4: 4th quarter
caption: "Quarterly summary :"
header:
réduction: Calculated reduction
régularisation: Calculated regularization
répartition:
retraite: IRC
urssaf: URSSAF
code:
"462": code 462(€)
"684": code 684(€)
shortname: Lodeom exemption
tab:
month: Monthly exemption
@ -1558,9 +1548,11 @@ pages:
T3: 3rd quarter
T4: 4th quarter
caption: "Quarterly summary :"
code671: code 671(€)
code801: code 801(€)
header: Calculated reduction
code:
"671": code 671(€)
"801": code 801(€)
header-réduction: Calculated reduction
header-régularisation: Calculated regularization
régularisation:
annuelle: Annual adjustment
progressive: Progressive regularization

View file

@ -1548,20 +1548,10 @@ pages:
title: Éxonération Lodeom
month-by-month:
caption: "Exonération Lodeom mois par mois :"
options:
description: Ajoute des champs pour moduler l'activité du salarié
recap:
T1: 1er trimestre
T2: 2ème trimestre
T3: 3ème trimestre
T4: 4ème trimestre
caption: "Récapitulatif trimestriel :"
header:
réduction: Réduction calculée
régularisation: Régularisation calculée
répartition:
retraite: IRC
urssaf: URSSAF
code:
"462": code 462(€)
"684": code 684(€)
shortname: Éxonération Lodeom
tab:
month: Exonération mensuelle
@ -1653,9 +1643,11 @@ pages:
T3: 3ème trimestre
T4: 4ème trimestre
caption: "Récapitulatif trimestriel :"
code671: code 671(€)
code801: code 801(€)
header: Réduction calculée
code:
"671": code 671(€)
"801": code 801(€)
header-réduction: Réduction calculée
header-régularisation: Régularisation calculée
régularisation:
annuelle: Régularisation annuelle
progressive: Régularisation progressive

View file

@ -1,39 +1,27 @@
import { DottedName } from 'modele-social'
import { PublicodesExpression } from 'publicodes'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useDispatch, useSelector } from 'react-redux'
import RéductionBasique from '@/components/RéductionDeCotisations/RéductionBasique'
import RéductionMoisParMois from '@/components/RéductionDeCotisations/RéductionMoisParMois'
import { SimulationGoals } from '@/components/Simulation'
import { useEngine } from '@/components/utils/EngineContext'
import useYear from '@/components/utils/useYear'
import { SimpleRuleEvaluation } from '@/domaine/engine/SimpleRuleEvaluation'
import { Situation } from '@/domaine/Situation'
import { ajusteLaSituation } from '@/store/actions/actions'
import { situationSelector } from '@/store/selectors/simulationSelectors'
import LodeomBasique from './Basique'
import LodeomMoisParMois from './MoisParMois'
import {
getInitialLodeomMoisParMois,
heuresComplémentairesDottedName,
heuresSupplémentairesDottedName,
getDataAfterOptionsChange,
getDataAfterRémunérationChange,
getDataAfterSituationChange,
getInitialRéductionMoisParMois,
lodeomDottedName,
MonthState,
Options,
reevaluateLodeomMoisParMois,
RégularisationMethod,
rémunérationBruteDottedName,
} from './utils'
SituationType,
} from '@/utils/réductionDeCotisations'
type SituationType = Situation & {
[heuresSupplémentairesDottedName]?: {
explanation: {
nodeValue: number
}
}
[heuresComplémentairesDottedName]?: {
valeur: number
}
}
import Warnings from './components/Warnings'
import WarningSalaireTrans from './components/WarningSalaireTrans'
export default function LodeomSimulationGoals({
monthByMonth,
@ -52,9 +40,10 @@ export default function LodeomSimulationGoals({
const year = useYear()
const situation = useSelector(situationSelector) as SituationType
const previousSituation = useRef(situation)
const { t } = useTranslation()
const initializeLodeomMoisParMoisData = useCallback(() => {
const data = getInitialLodeomMoisParMois(year, engine)
const data = getInitialRéductionMoisParMois(lodeomDottedName, year, engine)
setData(data)
}, [engine, year])
@ -64,112 +53,48 @@ export default function LodeomSimulationGoals({
}
}, [initializeLodeomMoisParMoisData, lodeomMoisParMoisData.length])
const getOptionsFromSituations = (
previousSituation: SituationType,
newSituation: SituationType
): Partial<Options> => {
const options = {} as Partial<Options>
const previousHeuresSupplémentaires =
previousSituation[heuresSupplémentairesDottedName]?.explanation.nodeValue
const newHeuresSupplémentaires =
newSituation[heuresSupplémentairesDottedName]?.explanation.nodeValue
if (newHeuresSupplémentaires !== previousHeuresSupplémentaires) {
options.heuresSupplémentaires = newHeuresSupplémentaires || 0
}
const previousHeuresComplémentaires =
previousSituation[heuresComplémentairesDottedName]?.valeur
const newHeuresComplémentaires =
newSituation[heuresComplémentairesDottedName]?.valeur
if (newHeuresComplémentaires !== previousHeuresComplémentaires) {
options.heuresComplémentaires = newHeuresComplémentaires || 0
}
return options
}
useEffect(() => {
setData((previousData) => {
if (!Object.keys(situation).length) {
return getInitialLodeomMoisParMois(year, engine)
}
const newOptions = getOptionsFromSituations(
return getDataAfterSituationChange(
lodeomDottedName,
situation,
previousSituation.current,
situation
)
const updatedData = previousData.map((data) => {
return {
...data,
options: {
...data.options,
...newOptions,
},
}
}, [])
return reevaluateLodeomMoisParMois(
updatedData,
engine,
previousData,
year,
régularisationMethod
régularisationMethod,
engine
)
})
}, [engine, situation, régularisationMethod, year])
const updateRémunérationBruteAnnuelle = (data: MonthState[]): void => {
const rémunérationBruteAnnuelle = data.reduce(
(total: number, monthState: MonthState) =>
total + monthState.rémunérationBrute,
0
)
dispatch(
ajusteLaSituation({
[rémunérationBruteDottedName]: {
valeur: rémunérationBruteAnnuelle,
unité: '€/an',
} as PublicodesExpression,
} as Record<DottedName, SimpleRuleEvaluation>)
)
}
const onRémunérationChange = (
monthIndex: number,
rémunérationBrute: number
) => {
setData((previousData) => {
const updatedData = [...previousData]
updatedData[monthIndex] = {
...updatedData[monthIndex],
return getDataAfterRémunérationChange(
lodeomDottedName,
monthIndex,
rémunérationBrute,
}
updateRémunérationBruteAnnuelle(updatedData)
return reevaluateLodeomMoisParMois(
updatedData,
engine,
previousData,
year,
régularisationMethod
régularisationMethod,
engine,
dispatch
)
})
}
const onOptionsChange = (monthIndex: number, options: Options) => {
setData((previousData) => {
const updatedData = [...previousData]
updatedData[monthIndex] = {
...updatedData[monthIndex],
return getDataAfterOptionsChange(
lodeomDottedName,
monthIndex,
options,
}
return reevaluateLodeomMoisParMois(
updatedData,
engine,
previousData,
year,
régularisationMethod
régularisationMethod,
engine
)
})
}
@ -177,13 +102,35 @@ export default function LodeomSimulationGoals({
return (
<SimulationGoals toggles={toggles} legend={legend}>
{monthByMonth ? (
<LodeomMoisParMois
<RéductionMoisParMois
dottedName={lodeomDottedName}
data={lodeomMoisParMoisData}
onRémunérationChange={onRémunérationChange}
onOptionsChange={onOptionsChange}
caption={t(
'pages.simulateurs.lodeom.month-by-month.caption',
'Exonération Lodeom mois par mois :'
)}
warnings={<Warnings />}
warningCondition={`${lodeomDottedName} = 0`}
warningTooltip={<WarningSalaireTrans />}
codeRéduction={t(
'pages.simulateurs.lodeom.recap.code.462',
'code 462(€)'
)}
codeRégularisation={t(
'pages.simulateurs.lodeom.recap.code.684',
'code 684(€)'
)}
/>
) : (
<LodeomBasique onUpdate={initializeLodeomMoisParMoisData} />
<RéductionBasique
dottedName={lodeomDottedName}
onUpdate={initializeLodeomMoisParMoisData}
warnings={<Warnings />}
warningCondition={`${lodeomDottedName} = 0`}
warningMessage={<WarningSalaireTrans />}
/>
)}
</SimulationGoals>
)

View file

@ -1,14 +1,15 @@
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import EffectifSwitch from '@/components/EffectifSwitch'
import PeriodSwitch from '@/components/PeriodSwitch'
import RégularisationSwitch from '@/components/RégularisationSwitch'
import RégularisationSwitch from '@/components/RéductionDeCotisations/RégularisationSwitch'
import { SelectSimulationYear } from '@/components/SelectSimulationYear'
import SimulateurWarning from '@/components/SimulateurWarning'
import Simulation from '@/components/Simulation'
import { RégularisationMethod } from '@/utils/réductionDeCotisations'
import LodeomSimulationGoals from './Goals'
import { RégularisationMethod } from './utils'
export default function LodeomSimulation() {
const { t } = useTranslation()
@ -53,6 +54,7 @@ export default function LodeomSimulation() {
régularisationMethod={régularisationMethod}
setRégularisationMethod={setRégularisationMethod}
/>
<EffectifSwitch />
<PeriodSwitch periods={periods} onSwitch={onPeriodSwitch} />
</>
}

View file

@ -1,256 +0,0 @@
import { useTranslation } from 'react-i18next'
import { styled } from 'styled-components'
import RuleLink from '@/components/RuleLink'
import { Spacing } from '@/design-system/layout'
import { baseTheme } from '@/design-system/theme'
import { H3 } from '@/design-system/typography/heading'
import { useMediaQuery } from '@/hooks/useMediaQuery'
import LodeomMois from './components/LodeomMois'
import RécapitulatifTrimestre from './components/RécapitulatifTrimestre'
import Warnings from './components/Warnings'
import { lodeomDottedName, MonthState, Options } from './utils'
type Props = {
data: MonthState[]
onRémunérationChange: (monthIndex: number, rémunérationBrute: number) => void
onOptionsChange: (monthIndex: number, options: Options) => void
}
export default function LodeomMoisParMois({
data,
onRémunérationChange,
onOptionsChange,
}: Props) {
const { t } = useTranslation()
const isDesktop = useMediaQuery(
`(min-width: ${baseTheme.breakpointsWidth.md})`
)
const months = [
t('janvier'),
t('février'),
t('mars'),
t('avril'),
t('mai'),
t('juin'),
t('juillet'),
t('août'),
t('septembre'),
t('octobre'),
t('novembre'),
t('décembre'),
]
const quarters = {
[t('pages.simulateurs.lodeom.recap.T1', '1er trimestre')]: data.slice(0, 3),
[t('pages.simulateurs.lodeom.recap.T2', '2ème trimestre')]: data.slice(
3,
6
),
[t('pages.simulateurs.lodeom.recap.T3', '3ème trimestre')]: data.slice(
6,
9
),
[t('pages.simulateurs.lodeom.recap.T4', '4ème trimestre')]: data.slice(9),
}
return (
<>
{isDesktop ? (
<>
<H3 as="h2">
{t(
'pages.simulateurs.lodeom.month-by-month.caption',
'Exonération Lodeom mois par mois :'
)}
</H3>
<StyledTable>
<caption className="sr-only">
{t(
'pages.simulateurs.lodeom.month-by-month.caption',
'Exonération Lodeom mois par mois :'
)}
</caption>
<thead>
<tr>
<th scope="col">{t('Mois')}</th>
<th scope="col">
{/* TODO: remplacer par rémunérationBruteDottedName lorsque ... */}
<RuleLink dottedName="salarié . rémunération . brut" />
</th>
<th scope="col">
<RuleLink dottedName={lodeomDottedName} />
</th>
<th scope="col">
<RuleLink dottedName="salarié . cotisations . exonérations . réduction générale . régularisation" />
</th>
</tr>
</thead>
<tbody>
{data.length > 0 &&
months.map((monthName, monthIndex) => (
<LodeomMois
key={`month-${monthIndex}`}
monthName={monthName}
data={data[monthIndex]}
index={monthIndex}
onRémunérationChange={(
monthIndex: number,
rémunérationBrute: number
) => {
onRémunérationChange(monthIndex, rémunérationBrute)
}}
onOptionsChange={(monthIndex: number, options: Options) => {
onOptionsChange(monthIndex, options)
}}
/>
))}
</tbody>
</StyledTable>
<Spacing md />
<H3 as="h2">
{t(
'pages.simulateurs.lodeom.recap.caption',
'Récapitulatif trimestriel :'
)}
</H3>
<StyledRecapTable>
<caption className="sr-only">
{t(
'pages.simulateurs.lodeom.recap.caption',
'Récapitulatif trimestriel :'
)}
</caption>
<thead>
<tr>
<th scope="col">{t('Trimestre')}</th>
<th scope="col">
{t(
'pages.simulateurs.lodeom.recap.header.réduction',
'Réduction calculée'
)}
{/* <br />
{t(
'pages.simulateurs.lodeom.recap.code671',
'code 671(€)'
)} */}
</th>
<th scope="col">
{t(
'pages.simulateurs.lodeom.recap.header.régularisation',
'Régularisation calculée'
)}
{/* <br />
{t(
'pages.simulateurs.lodeom.recap.code801',
'code 801(€)'
)} */}
</th>
</tr>
</thead>
<tbody>
{Object.keys(quarters).map((label, index) => (
<RécapitulatifTrimestre
key={index}
label={label}
data={quarters[label]}
/>
))}
</tbody>
</StyledRecapTable>
</>
) : (
<>
<H3 as="h2">
{t(
'pages.simulateurs.lodeom.month-by-month.caption',
'Exonération Lodeom mois par mois :'
)}
</H3>
{data.length > 0 &&
months.map((monthName, monthIndex) => (
<LodeomMois
key={`month-${monthIndex}`}
monthName={monthName}
data={data[monthIndex]}
index={monthIndex}
onRémunérationChange={(
monthIndex: number,
rémunérationBrute: number
) => {
onRémunérationChange(monthIndex, rémunérationBrute)
}}
onOptionsChange={(monthIndex: number, options: Options) => {
onOptionsChange(monthIndex, options)
}}
mobileVersion={true}
/>
))}
<Spacing xxl />
<H3 as="h2">
{t(
'pages.simulateurs.lodeom.recap.caption',
'Récapitulatif trimestriel :'
)}
</H3>
{Object.keys(quarters).map((label, index) => (
<RécapitulatifTrimestre
key={index}
label={label}
data={quarters[label]}
mobileVersion={true}
/>
))}
</>
)}
<span id="options-description" className="sr-only">
{t(
'pages.simulateurs.lodeom.options.description',
"Ajoute des champs pour moduler l'activité du salarié"
)}
</span>
<Warnings />
</>
)
}
const StyledTable = styled.table`
border-collapse: collapse;
text-align: left;
width: 100%;
color: ${({ theme }) => theme.colors.bases.primary[100]};
font-family: ${({ theme }) => theme.fonts.main};
caption {
text-align: left;
margin: ${({ theme }) => `${theme.spacings.sm} 0 `};
}
th,
td {
padding: ${({ theme }) => theme.spacings.xs};
}
tbody tr th {
text-transform: capitalize;
font-weight: normal;
}
`
const StyledRecapTable = styled(StyledTable)`
thead {
border-bottom: solid 1px;
}
thead th:not(:last-of-type),
tbody th,
td:not(:last-of-type) {
border-right: solid 1px;
}
thead th:not(:first-of-type) {
text-align: center;
}
`

View file

@ -1,254 +0,0 @@
import { PublicodesExpression } from 'publicodes'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { styled } from 'styled-components'
import NumberInput from '@/components/conversation/NumberInput'
import MonthOptions from '@/components/MonthOptions'
import RuleLink from '@/components/RuleLink'
import { useEngine } from '@/components/utils/EngineContext'
import { Button } from '@/design-system/buttons'
import { FlexCenter } from '@/design-system/global-style'
import { RotatingChevronIcon } from '@/design-system/icons'
import { Grid, Spacing } from '@/design-system/layout'
import { Body } from '@/design-system/typography/paragraphs'
import {
lodeomDottedName,
MonthState,
Options,
rémunérationBruteDottedName,
} from '../utils'
import MontantRéduction from './MontantRéduction'
type Props = {
monthName: string
data: MonthState
index: number
onRémunérationChange: (monthIndex: number, rémunérationBrute: number) => void
onOptionsChange: (monthIndex: number, options: Options) => void
mobileVersion?: boolean
}
export type RémunérationBruteInput = {
unité: string
valeur: number
}
export default function LodeomMois({
monthName,
data,
index,
onRémunérationChange,
onOptionsChange,
mobileVersion = false,
}: Props) {
const { t, i18n } = useTranslation()
const language = i18n.language
const displayedUnit = '€'
const engine = useEngine()
const [isOptionVisible, setOptionVisible] = useState(false)
const RémunérationInput = () => {
// TODO: enlever les 4 premières props après résolution de #3123
const ruleInputProps = {
dottedName: rémunérationBruteDottedName,
suggestions: {},
description: undefined,
question: undefined,
engine,
'aria-labelledby': 'simu-update-explaining',
formatOptions: {
maximumFractionDigits: 0,
},
displayedUnit,
unit: {
numerators: ['€'],
denominators: [],
},
}
return (
<NumberInput
{...ruleInputProps}
id={`${rémunérationBruteDottedName.replace(
/\s|\./g,
'_'
)}-${monthName}`}
aria-label={`${engine.getRule(rémunérationBruteDottedName)
?.title} (${monthName})`}
onChange={(rémunérationBrute?: PublicodesExpression) =>
onRémunérationChange(
index,
(rémunérationBrute as RémunérationBruteInput).valeur
)
}
value={data.rémunérationBrute}
formatOptions={{
maximumFractionDigits: 2,
}}
displaySuggestions={false}
/>
)
}
const OptionsButton = () => {
return (
<Button
size="XXS"
light
onPress={() => setOptionVisible(!isOptionVisible)}
aria-describedby="options-description"
aria-expanded={isOptionVisible}
aria-controls={`options-${monthName}`}
aria-label={!isOptionVisible ? t('Déplier') : t('Replier')}
>
{t('Options')}&nbsp;
<RotatingChevronIcon aria-hidden $isOpen={isOptionVisible} />
</Button>
)
}
const MontantLodeom = () => {
return (
<MontantRéduction
id={`${lodeomDottedName.replace(/\s|\./g, '_')}-${monthName}`}
rémunérationBrute={data.rémunérationBrute}
lodeom={data.lodeom.value}
répartition={data.lodeom.répartition}
displayedUnit={displayedUnit}
language={language}
/>
)
}
const MontantRégularisation = () => {
return (
<MontantRéduction
id={`${lodeomDottedName.replace(
/\s|\./g,
'_'
)}__régularisation-${monthName}`}
rémunérationBrute={data.rémunérationBrute}
lodeom={data.régularisation.value}
répartition={data.régularisation.répartition}
displayedUnit={displayedUnit}
language={language}
displayNull={false}
/>
)
}
return mobileVersion ? (
<div>
<StyledMonth>{monthName}</StyledMonth>
<GridContainer container spacing={2}>
<Grid item xs={12} sm={4}>
<RuleLink dottedName="salarié . rémunération . brut" />
</Grid>
<Grid item xs={7} sm={5}>
<RémunérationInput />
</Grid>
<Grid item xs={5} sm={3}>
<OptionsButton />
</Grid>
</GridContainer>
{isOptionVisible && (
<OptionsContainer>
<MonthOptions
month={monthName}
index={index}
options={data.options}
onOptionsChange={onOptionsChange}
/>
</OptionsContainer>
)}
<Spacing xs />
<GridContainer container spacing={2}>
<Grid item>
<RuleLink dottedName={lodeomDottedName} />
</Grid>
<Grid item>
<StyledBody>
<MontantLodeom />
</StyledBody>
</Grid>
</GridContainer>
<GridContainer container spacing={2}>
<Grid item>
<RuleLink dottedName="salarié . cotisations . exonérations . réduction générale . régularisation" />
</Grid>
<Grid item>
<StyledBody>
<MontantRégularisation />
</StyledBody>
</Grid>
</GridContainer>
</div>
) : (
<>
<tr>
<th scope="row">{monthName}</th>
<td>
<InputContainer>
<RémunérationInput />
<OptionsButton />
</InputContainer>
</td>
<td>
<MontantLodeom />
</td>
<td>
<MontantRégularisation />
</td>
</tr>
{isOptionVisible && (
<StyledTableRow>
<td />
<td colSpan={3}>
<MonthOptions
month={monthName}
index={index}
options={data.options}
onOptionsChange={onOptionsChange}
/>
</td>
</StyledTableRow>
)}
</>
)
}
const StyledMonth = styled(Body)`
font-weight: bold;
text-transform: capitalize;
border-bottom: solid 1px ${({ theme }) => theme.colors.bases.primary[100]};
`
const GridContainer = styled(Grid)`
align-items: baseline;
justify-content: space-between;
`
const StyledBody = styled(Body)`
margin-top: 0;
`
const OptionsContainer = styled.div`
margin-top: ${({ theme }) => theme.spacings.xs};
background-color: ${({ theme }) => theme.colors.bases.primary[200]};
padding: ${({ theme }) => theme.spacings.sm};
`
const StyledTableRow = styled.tr`
background-color: ${({ theme }) => theme.colors.bases.primary[200]};
td {
padding-top: ${({ theme }) => theme.spacings.sm};
padding-bottom: ${({ theme }) => theme.spacings.sm};
}
`
const InputContainer = styled.div`
${FlexCenter}
gap: ${({ theme }) => theme.spacings.md};
`

View file

@ -1,91 +0,0 @@
import { formatValue } from 'publicodes'
import { useTranslation } from 'react-i18next'
import { styled } from 'styled-components'
import { Condition } from '@/components/EngineValue/Condition'
import { FlexCenter } from '@/design-system/global-style'
import { SearchIcon, WarningIcon } from '@/design-system/icons'
import { Tooltip } from '@/design-system/tooltip'
import {
rémunérationBruteDottedName,
Répartition as RépartitionType,
} from '../utils'
import Répartition from './Répartition'
import WarningSalaireTrans from './WarningSalaireTrans'
type Props = {
id?: string
rémunérationBrute: number
lodeom: number
répartition: RépartitionType
displayedUnit: string
language: string
displayNull?: boolean
alignment?: 'center' | 'end'
}
export default function MontantRéduction({
id,
rémunérationBrute,
lodeom,
répartition,
displayedUnit,
language,
displayNull = true,
alignment = 'end',
}: Props) {
const { t } = useTranslation()
const tooltip = <Répartition répartition={répartition} />
return lodeom ? (
<StyledTooltip tooltip={tooltip}>
<FlexDiv id={id} $alignment={alignment}>
{formatValue(
{
nodeValue: lodeom,
},
{
displayedUnit,
language,
}
)}
<StyledSearchIcon />
</FlexDiv>
</StyledTooltip>
) : (
displayNull && (
<FlexDiv id={id} $alignment={alignment}>
{formatValue(0, { displayedUnit, language })}
<Condition
expression="salarié . cotisations . exonérations . lodeom . montant = 0"
contexte={{
[rémunérationBruteDottedName]: rémunérationBrute,
}}
>
<Tooltip tooltip={<WarningSalaireTrans />}>
<span className="sr-only">{t('Attention')}</span>
<StyledWarningIcon aria-label={t('Attention')} />
</Tooltip>
</Condition>
</FlexDiv>
)
)
}
const StyledTooltip = styled(Tooltip)`
width: 100%;
`
const FlexDiv = styled.div<{ $alignment: 'end' | 'center' }>`
${FlexCenter}
justify-content: ${({ $alignment }) => $alignment};
`
const StyledSearchIcon = styled(SearchIcon)`
margin-left: ${({ theme }) => theme.spacings.sm};
`
const StyledWarningIcon = styled(WarningIcon)`
margin-top: ${({ theme }) => theme.spacings.xxs};
margin-left: ${({ theme }) => theme.spacings.sm};
`

View file

@ -1,58 +0,0 @@
import { Trans, useTranslation } from 'react-i18next'
import { styled } from 'styled-components'
import RépartitionValue from '@/components/RépartitionValue'
import { Strong } from '@/design-system/typography'
import { Li, Ul } from '@/design-system/typography/list'
import { Body } from '@/design-system/typography/paragraphs'
import { lodeomDottedName, Répartition as RépartitionType } from '../utils'
type Props = {
répartition: RépartitionType
}
export default function Répartition({ répartition }: Props) {
const { t } = useTranslation()
return (
<>
<Body>
<Strong>
<Trans>Détail du montant :</Trans>
</Strong>
</Body>
<StyledUl>
<StyledLi>
<RépartitionValue
value={répartition.IRC}
label={t('pages.simulateurs.lodeom.répartition.retraite', 'IRC')}
idPrefix={`${lodeomDottedName} . imputation retraite complémentaire`.replace(
/\s|\./g,
'_'
)}
/>
</StyledLi>
<StyledLi>
<RépartitionValue
value={répartition.Urssaf}
label={t('pages.simulateurs.lodeom.répartition.urssaf', 'URSSAF')}
idPrefix={`${lodeomDottedName} . imputation sécurité sociale`.replace(
/\s|\./g,
'_'
)}
/>
</StyledLi>
</StyledUl>
</>
)
}
const StyledUl = styled(Ul)`
margin-top: 0;
`
const StyledLi = styled(Li)`
&::before {
margin-top: ${({ theme }) => theme.spacings.sm};
}
`

View file

@ -1,449 +0,0 @@
import { sumAll } from 'effect/Number'
import { DottedName } from 'modele-social'
import Engine from 'publicodes'
import { Situation } from '@/domaine/Situation'
// TODO: remplacer "salarié . cotisations . assiette" par "salarié . rémunération . brut"
// lorsqu'elle n'incluera plus les frais professionnels.
export const rémunérationBruteDottedName = 'salarié . cotisations . assiette'
export const lodeomDottedName =
'salarié . cotisations . exonérations . lodeom . montant'
export const heuresSupplémentairesDottedName =
'salarié . temps de travail . heures supplémentaires'
export const heuresComplémentairesDottedName =
'salarié . temps de travail . heures complémentaires'
export type Répartition = {
IRC: number
Urssaf: number
}
export type MonthState = {
rémunérationBrute: number
options: Options
lodeom: {
value: number
répartition: Répartition
}
régularisation: {
value: number
répartition: Répartition
}
}
export type Options = {
heuresSupplémentaires: number
heuresComplémentaires: number
rémunérationETP: number
rémunérationPrimes: number
rémunérationHeuresSup: number
}
export type RégularisationMethod = 'annuelle' | 'progressive'
const getDateForContexte = (monthIndex: number, year: number): string => {
const date = new Date(year, monthIndex)
return date.toLocaleDateString('fr')
}
const getMonthlyLodeom = (
date: string,
rémunérationBrute: number,
options: Options,
engine: Engine<DottedName>
): number => {
const lodeom = engine.evaluate({
valeur: lodeomDottedName,
unité: '€/mois',
contexte: {
date,
[rémunérationBruteDottedName]: rémunérationBrute,
[heuresSupplémentairesDottedName]: options.heuresSupplémentaires,
[heuresComplémentairesDottedName]: options.heuresComplémentaires,
},
})
return lodeom.nodeValue as number
}
const getTotalLodeom = (
rémunérationBrute: number,
SMIC: number,
coefT: number,
engine: Engine<DottedName>
): number => {
const lodeom = engine.evaluate({
valeur: lodeomDottedName,
arrondi: 'non',
contexte: {
[rémunérationBruteDottedName]: rémunérationBrute,
'salarié . temps de travail . SMIC': SMIC,
'salarié . cotisations . exonérations . T': coefT,
},
})
return lodeom.nodeValue as number
}
const emptyRépartition = {
IRC: 0,
Urssaf: 0,
}
const getRépartition = (
rémunération: number,
réduction: number,
engine: Engine<DottedName>
): Répartition => {
const contexte = {
[rémunérationBruteDottedName]: rémunération,
[lodeomDottedName]: réduction,
}
const IRC =
(engine.evaluate({
valeur: `${lodeomDottedName} . imputation retraite complémentaire`,
unité: '€/mois',
contexte,
})?.nodeValue as number) ?? 0
const Urssaf =
(engine.evaluate({
valeur: `${lodeomDottedName} . imputation sécurité sociale`,
unité: '€/mois',
contexte,
})?.nodeValue as number) ?? 0
return {
IRC,
Urssaf,
}
}
export const getInitialLodeomMoisParMois = (
year: number,
engine: Engine<DottedName>
): MonthState[] => {
const rémunérationBrute =
(engine.evaluate({
valeur: rémunérationBruteDottedName,
arrondi: 'oui',
unité: '€/mois',
})?.nodeValue as number) ?? 0
const heuresSupplémentaires =
(engine.evaluate({
valeur: heuresSupplémentairesDottedName,
unité: 'heures/mois',
})?.nodeValue as number) ?? 0
const heuresComplémentaires =
(engine.evaluate({
valeur: heuresComplémentairesDottedName,
unité: 'heures/mois',
})?.nodeValue as number) ?? 0
const rémunérationETP = 0
const rémunérationPrimes = 0
const rémunérationHeuresSup = 0
if (!rémunérationBrute) {
return Array(12).fill({
rémunérationBrute,
options: {
heuresSupplémentaires,
heuresComplémentaires,
rémunérationETP,
rémunérationPrimes,
rémunérationHeuresSup,
},
lodeom: {
value: 0,
répartition: emptyRépartition,
},
régularisation: {
value: 0,
répartition: emptyRépartition,
},
}) as MonthState[]
}
return Array.from({ length: 12 }, (_item, monthIndex) => {
const date = getDateForContexte(monthIndex, year)
const lodeom = getMonthlyLodeom(
date,
rémunérationBrute,
{
heuresSupplémentaires,
heuresComplémentaires,
rémunérationETP,
rémunérationPrimes,
rémunérationHeuresSup,
},
engine
)
const répartition = getRépartition(rémunérationBrute, lodeom, engine)
return {
rémunérationBrute,
options: {
heuresSupplémentaires,
heuresComplémentaires,
rémunérationETP,
rémunérationPrimes,
rémunérationHeuresSup,
},
lodeom: {
value: lodeom,
répartition,
},
régularisation: {
value: 0,
répartition: emptyRépartition,
},
}
})
}
export const reevaluateLodeomMoisParMois = (
data: MonthState[],
engine: Engine<DottedName>,
year: number,
régularisationMethod: RégularisationMethod
): MonthState[] => {
const totalRémunérationBrute = sumAll(
data.map((monthData) => monthData.rémunérationBrute)
)
if (!totalRémunérationBrute) {
return data.map((monthData) => {
return {
...monthData,
lodeom: {
value: 0,
répartition: emptyRépartition,
},
régularisation: {
value: 0,
répartition: emptyRépartition,
},
}
})
}
const rémunérationBruteCumulées = getRémunérationBruteCumulées(data)
const SMICCumulés = getSMICCumulés(data, year, engine)
// Si on laisse l'engine calculer T dans le calcul de l'exonération Lodeom,
// le résultat ne sera pas bon à cause de l'assiette de cotisations du contexte
const coefT = engine.evaluate({
valeur: 'salarié . cotisations . exonérations . T',
}).nodeValue as number
const reevaluatedData = data.reduce(
(reevaluatedData: MonthState[], monthState, monthIndex) => {
const rémunérationBrute = monthState.rémunérationBrute
const options = monthState.options
const lodeom = {
value: 0,
répartition: emptyRépartition,
}
const régularisation = {
value: 0,
répartition: emptyRépartition,
}
if (!rémunérationBrute) {
return [
...reevaluatedData,
{
rémunérationBrute,
options,
lodeom,
régularisation,
},
]
}
if (régularisationMethod === 'progressive') {
// La régularisation progressive du mois N est la différence entre l'exonération Lodeom
// calculée pour la rémunération totale jusqu'à N (comparée au SMIC équivalent pour ces N mois)
// et la somme des N-1 exonérations déjà accordées (en incluant les régularisations).
const lodeomTotale = getTotalLodeom(
rémunérationBruteCumulées[monthIndex],
SMICCumulés[monthIndex],
coefT,
engine
)
const lodeomCumulée = sumAll(
reevaluatedData.map(
(monthData) =>
monthData.lodeom.value + monthData.régularisation.value
)
)
régularisation.value = lodeomTotale - lodeomCumulée
if (régularisation.value > 0) {
lodeom.value = régularisation.value
lodeom.répartition = getRépartition(
rémunérationBrute,
lodeom.value,
engine
)
régularisation.value = 0
} else if (régularisation.value < 0) {
régularisation.répartition = getRépartition(
rémunérationBrute,
régularisation.value,
engine
)
}
} else if (régularisationMethod === 'annuelle') {
const date = getDateForContexte(monthIndex, year)
lodeom.value = getMonthlyLodeom(
date,
rémunérationBrute,
options,
engine
)
if (monthIndex === data.length - 1) {
// La régularisation annuelle est la différence entre l'exonération Lodeom calculée
// pour la rémunération annuelle (comparée au SMIC annuel) et la somme des exonérations
// Lodeom déjà accordées.
const lodeomTotale = getTotalLodeom(
rémunérationBruteCumulées[monthIndex],
SMICCumulés[monthIndex],
coefT,
engine
)
const currentLodeomCumulée =
lodeom.value +
sumAll(reevaluatedData.map((monthData) => monthData.lodeom.value))
régularisation.value = lodeomTotale - currentLodeomCumulée
if (lodeom.value + régularisation.value > 0) {
lodeom.value += régularisation.value
lodeom.répartition = getRépartition(
rémunérationBrute,
lodeom.value,
engine
)
régularisation.value = 0
} else if (régularisation.value < 0) {
régularisation.répartition = getRépartition(
rémunérationBrute,
régularisation.value,
engine
)
}
}
}
return [
...reevaluatedData,
{
rémunérationBrute,
options,
lodeom,
régularisation,
},
]
},
[]
)
return reevaluatedData
}
const getSMICCumulés = (
data: MonthState[],
year: number,
engine: Engine<DottedName>
): number[] => {
return data.reduce((SMICCumulés: number[], monthData, monthIndex) => {
// S'il n'y a pas de rémunération ce mois-ci, il n'y a pas d'exonération Lodeom
// et il ne faut pas compter le SMIC de ce mois-ci dans le SMIC cumulé.
if (!monthData.rémunérationBrute) {
SMICCumulés.push(0)
return SMICCumulés
}
const contexte = {
date: getDateForContexte(monthIndex, year),
} as Situation
if (!monthData.options.rémunérationETP) {
contexte[heuresSupplémentairesDottedName] =
monthData.options.heuresSupplémentaires
contexte[heuresComplémentairesDottedName] =
monthData.options.heuresComplémentaires
}
let SMIC = engine.evaluate({
valeur: 'salarié . temps de travail . SMIC',
unité: '€/mois',
contexte,
}).nodeValue as number
if (monthData.options.rémunérationETP) {
const SMICHoraire = engine.evaluate({
valeur: 'SMIC . horaire',
contexte,
}).nodeValue as number
// On retranche les primes et le paiements des heures supplémentaires à la rémunération versée
// et on la compare à la rémunération équivalente "mois complet" sans les primes
const prorata =
(monthData.rémunérationBrute -
monthData.options.rémunérationPrimes -
monthData.options.rémunérationHeuresSup) /
monthData.options.rémunérationETP
// On applique ce prorata au SMIC mensuel et on y ajoute les heures supplémentaires et complémentaires
SMIC =
SMIC * prorata +
SMICHoraire *
(monthData.options.heuresSupplémentaires +
monthData.options.heuresComplémentaires)
}
let SMICCumulé = SMIC
if (monthIndex > 0) {
// Il faut aller chercher la dernière valeur positive du SMIC cumulé.
const previousSMICCumulé =
SMICCumulés.findLast((SMICCumulé) => SMICCumulé > 0) ?? 0
SMICCumulé = previousSMICCumulé + SMIC
}
SMICCumulés.push(SMICCumulé)
return SMICCumulés
}, [])
}
const getRémunérationBruteCumulées = (data: MonthState[]) => {
return data.reduce(
(rémunérationBruteCumulées: number[], monthData, monthIndex) => {
// S'il n'y a pas de rémunération ce mois-ci, il n'y a pas d'exonération Lodeom
// et elle ne compte pas non plus pour la régularisation des mois à venir.
if (!monthData.rémunérationBrute) {
rémunérationBruteCumulées.push(0)
return rémunérationBruteCumulées
}
let rémunérationBruteCumulée = monthData.rémunérationBrute
if (monthIndex > 0) {
// Il faut aller chercher la dernière valeur positive de la rémunération cumulée.
const previousRémunérationBruteCumulée =
rémunérationBruteCumulées.findLast(
(rémunérationBruteCumulée) => rémunérationBruteCumulée > 0
) ?? 0
rémunérationBruteCumulée =
previousRémunérationBruteCumulée + monthData.rémunérationBrute
}
rémunérationBruteCumulées.push(rémunérationBruteCumulée)
return rémunérationBruteCumulées
},
[]
)
}

View file

@ -1,77 +0,0 @@
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { Condition } from '@/components/EngineValue/Condition'
import { SimulationGoal } from '@/components/Simulation'
import { SimulationValue } from '@/components/Simulation/SimulationValue'
import { useEngine } from '@/components/utils/EngineContext'
import { Message } from '@/design-system'
import { Spacing } from '@/design-system/layout'
import { Body } from '@/design-system/typography/paragraphs'
import { targetUnitSelector } from '@/store/selectors/simulationSelectors'
import Répartition from './components/Répartition'
import Warnings from './components/Warnings'
import WarningSalaireTrans from './components/WarningSalaireTrans'
import {
réductionGénéraleDottedName,
rémunérationBruteDottedName,
} from './utils'
type Props = {
onUpdate: () => void
}
export default function RéductionGénéraleBasique({ onUpdate }: Props) {
const engine = useEngine()
const currentUnit = useSelector(targetUnitSelector)
const { t } = useTranslation()
const répartition = {
IRC:
(engine.evaluate({
valeur: `${réductionGénéraleDottedName} . imputation retraite complémentaire`,
unité: currentUnit,
})?.nodeValue as number) ?? 0,
Urssaf:
(engine.evaluate({
valeur: `${réductionGénéraleDottedName} . imputation sécurité sociale`,
unité: currentUnit,
})?.nodeValue as number) ?? 0,
chômage:
(engine.evaluate({
valeur: `${réductionGénéraleDottedName} . imputation chômage`,
unité: currentUnit,
})?.nodeValue as number) ?? 0,
}
return (
<>
<SimulationGoal
dottedName={rémunérationBruteDottedName}
round={false}
label={t('Rémunération brute')}
onUpdateSituation={onUpdate}
/>
<Warnings />
<Condition expression={`${rémunérationBruteDottedName} > 1.6 * SMIC`}>
<Message type="info">
<Body>
<WarningSalaireTrans />
</Body>
</Message>
</Condition>
<Condition expression={`${réductionGénéraleDottedName} >= 0`}>
<SimulationValue
dottedName={réductionGénéraleDottedName}
isInfoMode={true}
round={false}
/>
<Spacing md />
<Répartition répartition={répartition} />
</Condition>
</>
)
}

View file

@ -1,39 +1,28 @@
import { DottedName } from 'modele-social'
import { PublicodesExpression } from 'publicodes'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useDispatch, useSelector } from 'react-redux'
import RéductionBasique from '@/components/RéductionDeCotisations/RéductionBasique'
import RéductionMoisParMois from '@/components/RéductionDeCotisations/RéductionMoisParMois'
import { SimulationGoals } from '@/components/Simulation'
import { useEngine } from '@/components/utils/EngineContext'
import useYear from '@/components/utils/useYear'
import { SimpleRuleEvaluation } from '@/domaine/engine/SimpleRuleEvaluation'
import { Situation } from '@/domaine/Situation'
import { ajusteLaSituation } from '@/store/actions/actions'
import { situationSelector } from '@/store/selectors/simulationSelectors'
import RéductionGénéraleBasique from './Basique'
import RéductionGénéraleMoisParMois from './MoisParMois'
import {
getInitialRéductionGénéraleMoisParMois,
heuresComplémentairesDottedName,
heuresSupplémentairesDottedName,
getDataAfterOptionsChange,
getDataAfterRémunérationChange,
getDataAfterSituationChange,
getInitialRéductionMoisParMois,
MonthState,
Options,
reevaluateRéductionGénéraleMoisParMois,
réductionGénéraleDottedName,
RégularisationMethod,
rémunérationBruteDottedName,
} from './utils'
SituationType,
} from '@/utils/réductionDeCotisations'
type SituationType = Situation & {
[heuresSupplémentairesDottedName]?: {
explanation: {
nodeValue: number
}
}
[heuresComplémentairesDottedName]?: {
valeur: number
}
}
import Warnings from './components/Warnings'
import WarningSalaireTrans from './components/WarningSalaireTrans'
export default function RéductionGénéraleSimulationGoals({
monthByMonth,
@ -52,9 +41,14 @@ export default function RéductionGénéraleSimulationGoals({
const year = useYear()
const situation = useSelector(situationSelector) as SituationType
const previousSituation = useRef(situation)
const { t } = useTranslation()
const initializeRéductionGénéraleMoisParMoisData = useCallback(() => {
const data = getInitialRéductionGénéraleMoisParMois(year, engine)
const data = getInitialRéductionMoisParMois(
réductionGénéraleDottedName,
year,
engine
)
setData(data)
}, [engine, year])
@ -67,112 +61,48 @@ export default function RéductionGénéraleSimulationGoals({
réductionGénéraleMoisParMoisData.length,
])
const getOptionsFromSituations = (
previousSituation: SituationType,
newSituation: SituationType
): Partial<Options> => {
const options = {} as Partial<Options>
const previousHeuresSupplémentaires =
previousSituation[heuresSupplémentairesDottedName]?.explanation.nodeValue
const newHeuresSupplémentaires =
newSituation[heuresSupplémentairesDottedName]?.explanation.nodeValue
if (newHeuresSupplémentaires !== previousHeuresSupplémentaires) {
options.heuresSupplémentaires = newHeuresSupplémentaires || 0
}
const previousHeuresComplémentaires =
previousSituation[heuresComplémentairesDottedName]?.valeur
const newHeuresComplémentaires =
newSituation[heuresComplémentairesDottedName]?.valeur
if (newHeuresComplémentaires !== previousHeuresComplémentaires) {
options.heuresComplémentaires = newHeuresComplémentaires || 0
}
return options
}
useEffect(() => {
setData((previousData) => {
if (!Object.keys(situation).length) {
return getInitialRéductionGénéraleMoisParMois(year, engine)
}
const newOptions = getOptionsFromSituations(
return getDataAfterSituationChange(
réductionGénéraleDottedName,
situation,
previousSituation.current,
situation
)
const updatedData = previousData.map((data) => {
return {
...data,
options: {
...data.options,
...newOptions,
},
}
}, [])
return reevaluateRéductionGénéraleMoisParMois(
updatedData,
engine,
previousData,
year,
régularisationMethod
régularisationMethod,
engine
)
})
}, [engine, situation, régularisationMethod, year])
const updateRémunérationBruteAnnuelle = (data: MonthState[]): void => {
const rémunérationBruteAnnuelle = data.reduce(
(total: number, monthState: MonthState) =>
total + monthState.rémunérationBrute,
0
)
dispatch(
ajusteLaSituation({
[rémunérationBruteDottedName]: {
valeur: rémunérationBruteAnnuelle,
unité: '€/an',
} as PublicodesExpression,
} as Record<DottedName, SimpleRuleEvaluation>)
)
}
const onRémunérationChange = (
monthIndex: number,
rémunérationBrute: number
) => {
setData((previousData) => {
const updatedData = [...previousData]
updatedData[monthIndex] = {
...updatedData[monthIndex],
return getDataAfterRémunérationChange(
réductionGénéraleDottedName,
monthIndex,
rémunérationBrute,
}
updateRémunérationBruteAnnuelle(updatedData)
return reevaluateRéductionGénéraleMoisParMois(
updatedData,
engine,
previousData,
year,
régularisationMethod
régularisationMethod,
engine,
dispatch
)
})
}
const onOptionsChange = (monthIndex: number, options: Options) => {
setData((previousData) => {
const updatedData = [...previousData]
updatedData[monthIndex] = {
...updatedData[monthIndex],
return getDataAfterOptionsChange(
réductionGénéraleDottedName,
monthIndex,
options,
}
return reevaluateRéductionGénéraleMoisParMois(
updatedData,
engine,
previousData,
year,
régularisationMethod
régularisationMethod,
engine
)
})
}
@ -180,14 +110,34 @@ export default function RéductionGénéraleSimulationGoals({
return (
<SimulationGoals toggles={toggles} legend={legend}>
{monthByMonth ? (
<RéductionGénéraleMoisParMois
<RéductionMoisParMois
dottedName={réductionGénéraleDottedName}
data={réductionGénéraleMoisParMoisData}
onRémunérationChange={onRémunérationChange}
onOptionsChange={onOptionsChange}
caption={t(
'pages.simulateurs.réduction-générale.month-by-month.caption',
'Réduction générale mois par mois :'
)}
warnings={<Warnings />}
warningCondition={`${rémunérationBruteDottedName} > 1.6 * SMIC`}
warningTooltip={<WarningSalaireTrans />}
codeRéduction={t(
'pages.simulateurs.réduction-générale.recap.code.671',
'code 671(€)'
)}
codeRégularisation={t(
'pages.simulateurs.réduction-générale.recap.code.801',
'code 801(€)'
)}
/>
) : (
<RéductionGénéraleBasique
<RéductionBasique
dottedName={réductionGénéraleDottedName}
onUpdate={initializeRéductionGénéraleMoisParMoisData}
warnings={<Warnings />}
warningCondition={`${rémunérationBruteDottedName} > 1.6 * SMIC`}
warningMessage={<WarningSalaireTrans />}
/>
)}
</SimulationGoals>

View file

@ -1,16 +1,16 @@
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import EffectifSwitch from '@/components/EffectifSwitch'
import PeriodSwitch from '@/components/PeriodSwitch'
import RégularisationSwitch from '@/components/RégularisationSwitch'
import RégularisationSwitch from '@/components/RéductionDeCotisations/RégularisationSwitch'
import { SelectSimulationYear } from '@/components/SelectSimulationYear'
import SimulateurWarning from '@/components/SimulateurWarning'
import Simulation from '@/components/Simulation'
import { RégularisationMethod } from '@/utils/réductionDeCotisations'
import CongésPayésSwitch from './components/CongésPayésSwitch'
import EffectifSwitch from './components/EffectifSwitch'
import RéductionGénéraleSimulationGoals from './Goals'
import { RégularisationMethod } from './utils'
export default function RéductionGénéraleSimulation() {
const { t } = useTranslation()

View file

@ -9,12 +9,13 @@ import { Radio, ToggleGroup } from '@/design-system'
import { FlexCenter } from '@/design-system/global-style'
import { Body } from '@/design-system/typography/paragraphs'
import { enregistreLaRéponse } from '@/store/actions/actions'
import { réductionGénéraleDottedName } from '@/utils/réductionDeCotisations'
export default function CongésPayésSwitch() {
const dispatch = useDispatch()
const engine = useEngine()
const dottedName =
'salarié . cotisations . exonérations . réduction générale . caisse de congés payés' as DottedName
`${réductionGénéraleDottedName} . caisse de congés payés` as DottedName
const engineCongésPayés = engine.evaluate(dottedName).nodeValue as boolean
const [currentCongésPayés, setCurrentCongésPayés] = useState(
engineCongésPayés ? 'oui' : 'non'

View file

@ -1,170 +0,0 @@
import { sumAll } from 'effect/Number'
import { useTranslation } from 'react-i18next'
import { styled } from 'styled-components'
import { Grid } from '@/design-system/layout'
import { Body } from '@/design-system/typography/paragraphs'
import { MonthState } from '../utils'
import MontantRéduction from './MontantRéduction'
type Props = {
label: string
data: MonthState[]
mobileVersion?: boolean
}
export type RémunérationBruteInput = {
unité: string
valeur: number
}
export default function RécapitulatifTrimestre({
label,
data,
mobileVersion = false,
}: Props) {
const { t, i18n } = useTranslation()
const language = i18n.language
const displayedUnit = '€'
const rémunération = sumAll(
data.map((monthData) => monthData.rémunérationBrute)
)
const répartition = {
IRC: sumAll(
data.map(
(monthData) =>
monthData.réductionGénérale.répartition.IRC +
monthData.régularisation.répartition.IRC
)
),
Urssaf: sumAll(
data.map(
(monthData) =>
monthData.réductionGénérale.répartition.Urssaf +
monthData.régularisation.répartition.Urssaf
)
),
chômage: sumAll(
data.map(
(monthData) =>
monthData.réductionGénérale.répartition.chômage +
monthData.régularisation.répartition.chômage
)
),
}
let réduction = sumAll(
data.map((monthData) => monthData.réductionGénérale.value)
)
let régularisation = sumAll(
data.map((monthData) => monthData.régularisation.value)
)
if (réduction + régularisation > 0) {
réduction += régularisation
régularisation = 0
} else {
régularisation += réduction
réduction = 0
}
const Montant671 = () => {
return (
<MontantRéduction
id={`recap-${label.replace(/\s|\./g, '_')}-671`}
rémunérationBrute={rémunération}
réductionGénérale={réduction}
répartition={répartition}
displayedUnit={displayedUnit}
language={language}
displayNull={false}
alignment="center"
/>
)
}
const Montant801 = () => {
return (
<MontantRéduction
id={`recap-${label.replace(/\s|\./g, '_')}-801`}
rémunérationBrute={rémunération}
réductionGénérale={régularisation}
répartition={répartition}
displayedUnit={displayedUnit}
language={language}
displayNull={false}
alignment="center"
/>
)
}
return mobileVersion ? (
<div>
<StyledMonth>{label}</StyledMonth>
<GridContainer container spacing={2}>
<Grid item>
<StyledBody>
{t(
'pages.simulateurs.réduction-générale.recap.header',
'Réduction calculée'
)}
<br />
{t(
'pages.simulateurs.réduction-générale.recap.code671',
'code 671(€)'
)}
</StyledBody>
</Grid>
<Grid item>
<StyledBody>
<Montant671 />
</StyledBody>
</Grid>
</GridContainer>
<GridContainer container spacing={2}>
<Grid item>
<StyledBody>
{t(
'pages.simulateurs.réduction-générale.recap.header',
'Réduction calculée'
)}
<br />
{t(
'pages.simulateurs.réduction-générale.recap.code801',
'code 801(€)'
)}
</StyledBody>
</Grid>
<Grid item>
<StyledBody>
<Montant801 />
</StyledBody>
</Grid>
</GridContainer>
</div>
) : (
<tr>
<th scope="row">{label}</th>
<td>
<Montant671 />
</td>
<td>
<Montant801 />
</td>
</tr>
)
}
const StyledMonth = styled(Body)`
font-weight: bold;
text-transform: capitalize;
border-bottom: solid 1px ${({ theme }) => theme.colors.bases.primary[100]};
`
const GridContainer = styled(Grid)`
align-items: center;
justify-content: space-between;
`
const StyledBody = styled(Body)`
margin-top: 0;
`

View file

@ -1,29 +1,38 @@
import { sumAll } from 'effect/Number'
import { DottedName } from 'modele-social'
import Engine from 'publicodes'
import Engine, { PublicodesExpression } from 'publicodes'
import { AnyAction, Dispatch } from 'redux'
import { SimpleRuleEvaluation } from '@/domaine/engine/SimpleRuleEvaluation'
import { Situation } from '@/domaine/Situation'
import { ajusteLaSituation } from '@/store/actions/actions'
/********************************************************************/
/* Types et méthodes communes à la Réduction générale et au Lodeom */
/********************************************************************/
export const réductionGénéraleDottedName =
'salarié . cotisations . exonérations . réduction générale'
export const lodeomDottedName =
'salarié . cotisations . exonérations . lodeom . montant'
// TODO: remplacer "salarié . cotisations . assiette" par "salarié . rémunération . brut"
// lorsqu'elle n'incluera plus les frais professionnels.
export const rémunérationBruteDottedName = 'salarié . cotisations . assiette'
export const réductionGénéraleDottedName =
'salarié . cotisations . exonérations . réduction générale'
export const heuresSupplémentairesDottedName =
const heuresSupplémentairesDottedName =
'salarié . temps de travail . heures supplémentaires'
export const heuresComplémentairesDottedName =
const heuresComplémentairesDottedName =
'salarié . temps de travail . heures complémentaires'
export type Répartition = {
IRC: number
Urssaf: number
chômage: number
}
export type RéductionDottedName =
| typeof réductionGénéraleDottedName
| typeof lodeomDottedName
export type MonthState = {
rémunérationBrute: number
options: Options
réductionGénérale: {
réduction: {
value: number
répartition: Répartition
}
@ -43,93 +52,115 @@ export type Options = {
export type RégularisationMethod = 'annuelle' | 'progressive'
const getDateForContexte = (monthIndex: number, year: number): string => {
const date = new Date(year, monthIndex)
return date.toLocaleDateString('fr')
export type SituationType = Situation & {
[heuresSupplémentairesDottedName]?: {
explanation: {
nodeValue: number
}
}
[heuresComplémentairesDottedName]?: {
valeur: number
}
}
const getMonthlyRéductionGénérale = (
date: string,
export type RémunérationBruteInput = {
unité: string
valeur: number
}
export type Répartition = {
IRC: number
Urssaf: number
chômage: number
}
export const getDataAfterSituationChange = (
dottedName: RéductionDottedName,
situation: SituationType,
previousSituation: SituationType,
previousData: MonthState[],
year: number,
régularisationMethod: RégularisationMethod,
engine: Engine<DottedName>
): MonthState[] => {
if (!Object.keys(situation).length) {
return getInitialRéductionMoisParMois(dottedName, year, engine)
}
const newOptions = getOptionsFromSituations(previousSituation, situation)
const updatedData = previousData.map((data) => {
return {
...data,
options: {
...data.options,
...newOptions,
},
}
}, [])
return reevaluateRéductionMoisParMois(
dottedName,
updatedData,
year,
régularisationMethod,
engine
)
}
export const getDataAfterRémunérationChange = (
dottedName: RéductionDottedName,
monthIndex: number,
rémunérationBrute: number,
previousData: MonthState[],
year: number,
régularisationMethod: RégularisationMethod,
engine: Engine<DottedName>,
dispatch: Dispatch<AnyAction>
): MonthState[] => {
const updatedData = [...previousData]
updatedData[monthIndex] = {
...updatedData[monthIndex],
rémunérationBrute,
}
updateRémunérationBruteAnnuelle(updatedData, dispatch)
return reevaluateRéductionMoisParMois(
dottedName,
updatedData,
year,
régularisationMethod,
engine
)
}
export const getDataAfterOptionsChange = (
dottedName: RéductionDottedName,
monthIndex: number,
options: Options,
previousData: MonthState[],
year: number,
régularisationMethod: RégularisationMethod,
engine: Engine<DottedName>
): number => {
const réductionGénérale = engine.evaluate({
valeur: réductionGénéraleDottedName,
unité: '€/mois',
contexte: {
date,
[rémunérationBruteDottedName]: rémunérationBrute,
[heuresSupplémentairesDottedName]: options.heuresSupplémentaires,
[heuresComplémentairesDottedName]: options.heuresComplémentaires,
},
})
return réductionGénérale.nodeValue as number
}
const getTotalRéductionGénérale = (
rémunérationBrute: number,
SMIC: number,
coefT: number,
engine: Engine<DottedName>
): number => {
const réductionGénérale = engine.evaluate({
valeur: réductionGénéraleDottedName,
arrondi: 'non',
contexte: {
[rémunérationBruteDottedName]: rémunérationBrute,
'salarié . temps de travail . SMIC': SMIC,
'salarié . cotisations . exonérations . T': coefT,
},
})
return réductionGénérale.nodeValue as number
}
const emptyRépartition = {
IRC: 0,
Urssaf: 0,
chômage: 0,
}
const getRépartition = (
rémunération: number,
réduction: number,
engine: Engine<DottedName>
): Répartition => {
const contexte = {
[rémunérationBruteDottedName]: rémunération,
[réductionGénéraleDottedName]: réduction,
): MonthState[] => {
const updatedData = [...previousData]
updatedData[monthIndex] = {
...updatedData[monthIndex],
options,
}
const IRC =
(engine.evaluate({
valeur: `${réductionGénéraleDottedName} . imputation retraite complémentaire`,
unité: '€/mois',
contexte,
})?.nodeValue as number) ?? 0
const Urssaf =
(engine.evaluate({
valeur: `${réductionGénéraleDottedName} . imputation sécurité sociale`,
unité: '€/mois',
contexte,
})?.nodeValue as number) ?? 0
const chômage =
(engine.evaluate({
valeur: `${réductionGénéraleDottedName} . imputation chômage`,
unité: '€/mois',
contexte,
})?.nodeValue as number) ?? 0
return {
IRC,
Urssaf,
chômage,
}
return reevaluateRéductionMoisParMois(
dottedName,
updatedData,
year,
régularisationMethod,
engine
)
}
export const getInitialRéductionGénéraleMoisParMois = (
export const getInitialRéductionMoisParMois = (
dottedName: RéductionDottedName,
year: number,
engine: Engine<DottedName>
): MonthState[] => {
@ -163,7 +194,7 @@ export const getInitialRéductionGénéraleMoisParMois = (
rémunérationPrimes,
rémunérationHeuresSup,
},
réductionGénérale: {
réduction: {
value: 0,
répartition: emptyRépartition,
},
@ -177,7 +208,8 @@ export const getInitialRéductionGénéraleMoisParMois = (
return Array.from({ length: 12 }, (_item, monthIndex) => {
const date = getDateForContexte(monthIndex, year)
const réductionGénérale = getMonthlyRéductionGénérale(
const réduction = getMonthlyRéduction(
dottedName,
date,
rémunérationBrute,
{
@ -190,8 +222,9 @@ export const getInitialRéductionGénéraleMoisParMois = (
engine
)
const répartition = getRépartition(
dottedName,
rémunérationBrute,
réductionGénérale,
réduction,
engine
)
@ -204,8 +237,8 @@ export const getInitialRéductionGénéraleMoisParMois = (
rémunérationPrimes,
rémunérationHeuresSup,
},
réductionGénérale: {
value: réductionGénérale,
réduction: {
value: réduction,
répartition,
},
régularisation: {
@ -216,11 +249,12 @@ export const getInitialRéductionGénéraleMoisParMois = (
})
}
export const reevaluateRéductionGénéraleMoisParMois = (
export const reevaluateRéductionMoisParMois = (
dottedName: RéductionDottedName,
data: MonthState[],
engine: Engine<DottedName>,
year: number,
régularisationMethod: RégularisationMethod
régularisationMethod: RégularisationMethod,
engine: Engine<DottedName>
): MonthState[] => {
const totalRémunérationBrute = sumAll(
data.map((monthData) => monthData.rémunérationBrute)
@ -244,7 +278,7 @@ export const reevaluateRéductionGénéraleMoisParMois = (
const rémunérationBruteCumulées = getRémunérationBruteCumulées(data)
const SMICCumulés = getSMICCumulés(data, year, engine)
// Si on laisse l'engine calculer T dans le calcul de la réduction générale,
// Si on laisse l'engine calculer T dans le calcul de la réduction,
// le résultat ne sera pas bon à cause de l'assiette de cotisations du contexte
const coefT = engine.evaluate({
valeur: 'salarié . cotisations . exonérations . T',
@ -254,7 +288,7 @@ export const reevaluateRéductionGénéraleMoisParMois = (
(reevaluatedData: MonthState[], monthState, monthIndex) => {
const rémunérationBrute = monthState.rémunérationBrute
const options = monthState.options
const réductionGénérale = {
const réduction = {
value: 0,
répartition: emptyRépartition,
}
@ -269,41 +303,43 @@ export const reevaluateRéductionGénéraleMoisParMois = (
{
rémunérationBrute,
options,
réductionGénérale,
réduction,
régularisation,
},
]
}
if (régularisationMethod === 'progressive') {
// La régularisation progressive du mois N est la différence entre la réduction générale
// La régularisation progressive du mois N est la différence entre la réduction
// calculée pour la rémunération totale jusqu'à N (comparée au SMIC équivalent pour ces N mois)
// et la somme des N-1 réductions générales déjà accordées (en incluant les régularisations).
const réductionGénéraleTotale = getTotalRéductionGénérale(
// et la somme des N-1 réductions déjà accordées (en incluant les régularisations).
const réductionTotale = getTotalRéduction(
dottedName,
rémunérationBruteCumulées[monthIndex],
SMICCumulés[monthIndex],
coefT,
engine
)
const réductionGénéraleCumulée = sumAll(
const réductionCumulée = sumAll(
reevaluatedData.map(
(monthData) =>
monthData.réductionGénérale.value + monthData.régularisation.value
monthData.réduction.value + monthData.régularisation.value
)
)
régularisation.value =
réductionGénéraleTotale - réductionGénéraleCumulée
régularisation.value = réductionTotale - réductionCumulée
if (régularisation.value > 0) {
réductionGénérale.value = régularisation.value
réductionGénérale.répartition = getRépartition(
réduction.value = régularisation.value
réduction.répartition = getRépartition(
dottedName,
rémunérationBrute,
réductionGénérale.value,
réduction.value,
engine
)
régularisation.value = 0
} else if (régularisation.value < 0) {
régularisation.répartition = getRépartition(
dottedName,
rémunérationBrute,
régularisation.value,
engine
@ -311,7 +347,8 @@ export const reevaluateRéductionGénéraleMoisParMois = (
}
} else if (régularisationMethod === 'annuelle') {
const date = getDateForContexte(monthIndex, year)
réductionGénérale.value = getMonthlyRéductionGénérale(
réduction.value = getMonthlyRéduction(
dottedName,
date,
rémunérationBrute,
options,
@ -319,35 +356,36 @@ export const reevaluateRéductionGénéraleMoisParMois = (
)
if (monthIndex === data.length - 1) {
// La régularisation annuelle est la différence entre la réduction générale calculée
// La régularisation annuelle est la différence entre la réduction calculée
// pour la rémunération annuelle (comparée au SMIC annuel) et la somme des réductions
// générales déjà accordées.
const réductionGénéraleTotale = getTotalRéductionGénérale(
// déjà accordées.
const réductionTotale = getTotalRéduction(
dottedName,
rémunérationBruteCumulées[monthIndex],
SMICCumulés[monthIndex],
coefT,
engine
)
const currentRéductionGénéraleCumulée =
réductionGénérale.value +
réduction.value +
sumAll(
reevaluatedData.map(
(monthData) => monthData.réductionGénérale.value
)
reevaluatedData.map((monthData) => monthData.réduction.value)
)
régularisation.value =
réductionGénéraleTotale - currentRéductionGénéraleCumulée
réductionTotale - currentRéductionGénéraleCumulée
if (réductionGénérale.value + régularisation.value > 0) {
réductionGénérale.value += régularisation.value
réductionGénérale.répartition = getRépartition(
if (réduction.value + régularisation.value > 0) {
réduction.value += régularisation.value
réduction.répartition = getRépartition(
dottedName,
rémunérationBrute,
réductionGénérale.value,
réduction.value,
engine
)
régularisation.value = 0
} else if (régularisation.value < 0) {
régularisation.répartition = getRépartition(
dottedName,
rémunérationBrute,
régularisation.value,
engine
@ -361,7 +399,7 @@ export const reevaluateRéductionGénéraleMoisParMois = (
{
rémunérationBrute,
options,
réductionGénérale,
réduction,
régularisation,
},
]
@ -372,13 +410,174 @@ export const reevaluateRéductionGénéraleMoisParMois = (
return reevaluatedData
}
export const getRépartitionBasique = (
dottedName: RéductionDottedName,
currentUnit: string,
engine: Engine<DottedName>
): Répartition => {
const IRC =
(engine.evaluate({
valeur: `${dottedName} . imputation retraite complémentaire`,
unité: currentUnit,
})?.nodeValue as number) ?? 0
const Urssaf =
(engine.evaluate({
valeur: `${dottedName} . imputation sécurité sociale`,
unité: currentUnit,
})?.nodeValue as number) ?? 0
const chômage =
(engine.evaluate({
valeur: `${dottedName} . imputation chômage`,
unité: currentUnit,
})?.nodeValue as number) ?? 0
return {
IRC,
Urssaf,
chômage,
}
}
const emptyRépartition = {
IRC: 0,
Urssaf: 0,
chômage: 0,
}
const getOptionsFromSituations = (
previousSituation: SituationType,
newSituation: SituationType
): Partial<Options> => {
const options = {} as Partial<Options>
const previousHeuresSupplémentaires =
previousSituation[heuresSupplémentairesDottedName]?.explanation.nodeValue
const newHeuresSupplémentaires =
newSituation[heuresSupplémentairesDottedName]?.explanation.nodeValue
if (newHeuresSupplémentaires !== previousHeuresSupplémentaires) {
options.heuresSupplémentaires = newHeuresSupplémentaires || 0
}
const previousHeuresComplémentaires =
previousSituation[heuresComplémentairesDottedName]?.valeur
const newHeuresComplémentaires =
newSituation[heuresComplémentairesDottedName]?.valeur
if (newHeuresComplémentaires !== previousHeuresComplémentaires) {
options.heuresComplémentaires = newHeuresComplémentaires || 0
}
return options
}
const updateRémunérationBruteAnnuelle = (
data: MonthState[],
dispatch: Dispatch<AnyAction>
): void => {
const rémunérationBruteAnnuelle = data.reduce(
(total: number, monthState: MonthState) =>
total + monthState.rémunérationBrute,
0
)
dispatch(
ajusteLaSituation({
[rémunérationBruteDottedName]: {
valeur: rémunérationBruteAnnuelle,
unité: '€/an',
} as PublicodesExpression,
} as Record<DottedName, SimpleRuleEvaluation>)
)
}
const getDateForContexte = (monthIndex: number, year: number): string => {
const date = new Date(year, monthIndex)
return date.toLocaleDateString('fr')
}
const getMonthlyRéduction = (
dottedName: RéductionDottedName,
date: string,
rémunérationBrute: number,
options: Options,
engine: Engine<DottedName>
): number => {
const réduction = engine.evaluate({
valeur: dottedName,
unité: '€/mois',
contexte: {
date,
[rémunérationBruteDottedName]: rémunérationBrute,
[heuresSupplémentairesDottedName]: options.heuresSupplémentaires,
[heuresComplémentairesDottedName]: options.heuresComplémentaires,
},
})
return réduction.nodeValue as number
}
const getTotalRéduction = (
dottedName: RéductionDottedName,
rémunérationBrute: number,
SMIC: number,
coefT: number,
engine: Engine<DottedName>
): number => {
const réductionGénérale = engine.evaluate({
valeur: dottedName,
arrondi: 'non',
contexte: {
[rémunérationBruteDottedName]: rémunérationBrute,
'salarié . temps de travail . SMIC': SMIC,
'salarié . cotisations . exonérations . T': coefT,
},
})
return réductionGénérale.nodeValue as number
}
const getRépartition = (
dottedName: RéductionDottedName,
rémunération: number,
réduction: number,
engine: Engine<DottedName>
): Répartition => {
const contexte = {
[rémunérationBruteDottedName]: rémunération,
[dottedName]: réduction,
}
const IRC =
(engine.evaluate({
valeur: `${dottedName} . imputation retraite complémentaire`,
unité: '€/mois',
contexte,
})?.nodeValue as number) ?? 0
const Urssaf =
(engine.evaluate({
valeur: `${dottedName} . imputation sécurité sociale`,
unité: '€/mois',
contexte,
})?.nodeValue as number) ?? 0
const chômage =
(engine.evaluate({
valeur: `${dottedName} . imputation chômage`,
unité: '€/mois',
contexte,
})?.nodeValue as number) ?? 0
return {
IRC,
Urssaf,
chômage,
}
}
const getSMICCumulés = (
data: MonthState[],
year: number,
engine: Engine<DottedName>
): number[] => {
return data.reduce((SMICCumulés: number[], monthData, monthIndex) => {
// S'il n'y a pas de rémunération ce mois-ci, il n'y a pas de réduction générale
// S'il n'y a pas de rémunération ce mois-ci, il n'y a pas de réduction
// et il ne faut pas compter le SMIC de ce mois-ci dans le SMIC cumulé.
if (!monthData.rémunérationBrute) {
SMICCumulés.push(0)
@ -437,10 +636,10 @@ const getSMICCumulés = (
}, [])
}
const getRémunérationBruteCumulées = (data: MonthState[]) => {
const getRémunérationBruteCumulées = (data: MonthState[]): number[] => {
return data.reduce(
(rémunérationBruteCumulées: number[], monthData, monthIndex) => {
// S'il n'y a pas de rémunération ce mois-ci, il n'y a pas de réduction générale
// S'il n'y a pas de rémunération ce mois-ci, il n'y a pas de réduction
// et elle ne compte pas non plus pour la régularisation des mois à venir.
if (!monthData.rémunérationBrute) {
rémunérationBruteCumulées.push(0)