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:
parent
2b3de4c2aa
commit
4f93c72324
28 changed files with 687 additions and 1896 deletions
|
@ -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:
|
||||
|
|
|
@ -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', '1 569,81 €')
|
||||
cy.get('#recap-4ème_trimestre-671').should('include.text', '1 568,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',
|
||||
'1 569,81 €'
|
||||
)
|
||||
cy.get('#recap-4ème_trimestre-réduction').should(
|
||||
'include.text',
|
||||
'1 568,39 €'
|
||||
)
|
||||
})
|
||||
|
||||
it('should be RGAA compliant', function () {
|
||||
|
|
|
@ -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>
|
|
@ -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
|
|
@ -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 />
|
|
@ -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>
|
||||
</>
|
||||
)
|
|
@ -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 />
|
|
@ -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}
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -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
|
|
@ -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,
|
||||
'_'
|
||||
)}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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} />
|
||||
</>
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
`
|
|
@ -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')}
|
||||
<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};
|
||||
`
|
|
@ -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};
|
||||
`
|
|
@ -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};
|
||||
}
|
||||
`
|
|
@ -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
|
||||
},
|
||||
[]
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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;
|
||||
`
|
|
@ -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)
|
Loading…
Add table
Reference in a new issue