From 4f93c72324ead9e6cc34905f614fe1e23d2652f1 Mon Sep 17 00:00:00 2001 From: Alice Dahan Date: Fri, 20 Dec 2024 13:00:54 +0100 Subject: [PATCH] refactor(lodeom): factorise le code commun avec RGCP --- .../règles/salarié/cotisations.publicodes | 6 + .../mon-entreprise/reduction-generale.ts | 25 +- .../components/EffectifSwitch.tsx | 0 .../MontantAvecRépartition.tsx} | 38 +- .../MonthOptions.tsx | 3 +- .../RécapitulatifTrimestre.tsx | 68 +-- .../RéductionBasique.tsx} | 53 +- .../RéductionDeCotisations/RéductionMois.tsx} | 63 +-- .../RéductionMoisParMois.tsx} | 99 ++-- .../RégularisationSwitch.tsx | 3 +- .../RéductionDeCotisations}/Répartition.tsx | 16 +- .../RépartitionValue.tsx | 0 site/source/locales/rules-en.yaml | 3 + site/source/locales/ui-en.yaml | 24 +- site/source/locales/ui-fr.yaml | 24 +- .../source/pages/simulateurs/lodeom/Goals.tsx | 167 +++---- .../pages/simulateurs/lodeom/Lodeom.tsx | 6 +- .../pages/simulateurs/lodeom/MoisParMois.tsx | 256 ---------- .../lodeom/components/LodeomMois.tsx | 254 ---------- .../lodeom/components/MontantRéduction.tsx | 91 ---- .../lodeom/components/Répartition.tsx | 58 --- site/source/pages/simulateurs/lodeom/utils.ts | 449 ----------------- .../reduction-generale/Basique.tsx | 77 --- .../simulateurs/reduction-generale/Goals.tsx | 168 +++---- .../reduction-generale/RéductionGénérale.tsx | 6 +- .../components/CongésPayésSwitch.tsx | 3 +- .../components/RécapitulatifTrimestre.tsx | 170 ------- .../réductionDeCotisations.ts} | 453 +++++++++++++----- 28 files changed, 687 insertions(+), 1896 deletions(-) rename site/source/{pages/simulateurs/reduction-generale => }/components/EffectifSwitch.tsx (100%) rename site/source/{pages/simulateurs/reduction-generale/components/MontantRéduction.tsx => components/RéductionDeCotisations/MontantAvecRépartition.tsx} (70%) rename site/source/components/{ => RéductionDeCotisations}/MonthOptions.tsx (99%) rename site/source/{pages/simulateurs/lodeom/components => components/RéductionDeCotisations}/RécapitulatifTrimestre.tsx (68%) rename site/source/{pages/simulateurs/lodeom/Basique.tsx => components/RéductionDeCotisations/RéductionBasique.tsx} (53%) rename site/source/{pages/simulateurs/reduction-generale/components/RéductionGénéraleMois.tsx => components/RéductionDeCotisations/RéductionMois.tsx} (81%) rename site/source/{pages/simulateurs/reduction-generale/MoisParMois.tsx => components/RéductionDeCotisations/RéductionMoisParMois.tsx} (72%) rename site/source/components/{ => RéductionDeCotisations}/RégularisationSwitch.tsx (92%) rename site/source/{pages/simulateurs/reduction-generale/components => components/RéductionDeCotisations}/Répartition.tsx (74%) rename site/source/components/{ => RéductionDeCotisations}/RépartitionValue.tsx (100%) delete mode 100644 site/source/pages/simulateurs/lodeom/MoisParMois.tsx delete mode 100644 site/source/pages/simulateurs/lodeom/components/LodeomMois.tsx delete mode 100644 site/source/pages/simulateurs/lodeom/components/MontantRéduction.tsx delete mode 100644 site/source/pages/simulateurs/lodeom/components/Répartition.tsx delete mode 100644 site/source/pages/simulateurs/lodeom/utils.ts delete mode 100644 site/source/pages/simulateurs/reduction-generale/Basique.tsx delete mode 100644 site/source/pages/simulateurs/reduction-generale/components/RécapitulatifTrimestre.tsx rename site/source/{pages/simulateurs/reduction-generale/utils.ts => utils/réductionDeCotisations.ts} (60%) diff --git a/modele-social/règles/salarié/cotisations.publicodes b/modele-social/règles/salarié/cotisations.publicodes index 8e5ab5348..63cacbe53 100644 --- a/modele-social/règles/salarié/cotisations.publicodes +++ b/modele-social/règles/salarié/cotisations.publicodes @@ -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: diff --git a/site/cypress/integration/mon-entreprise/reduction-generale.ts b/site/cypress/integration/mon-entreprise/reduction-generale.ts index 5e5505bd5..33aa2820b 100755 --- a/site/cypress/integration/mon-entreprise/reduction-generale.ts +++ b/site/cypress/integration/mon-entreprise/reduction-generale.ts @@ -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 () { diff --git a/site/source/pages/simulateurs/reduction-generale/components/EffectifSwitch.tsx b/site/source/components/EffectifSwitch.tsx similarity index 100% rename from site/source/pages/simulateurs/reduction-generale/components/EffectifSwitch.tsx rename to site/source/components/EffectifSwitch.tsx diff --git a/site/source/pages/simulateurs/reduction-generale/components/MontantRéduction.tsx b/site/source/components/RéductionDeCotisations/MontantAvecRépartition.tsx similarity index 70% rename from site/source/pages/simulateurs/reduction-generale/components/MontantRéduction.tsx rename to site/source/components/RéductionDeCotisations/MontantAvecRépartition.tsx index 5f39630a5..5e51cfc4a 100644 --- a/site/source/pages/simulateurs/reduction-generale/components/MontantRéduction.tsx +++ b/site/source/components/RéductionDeCotisations/MontantAvecRépartition.tsx @@ -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 = + const tooltip = ( + + ) - return réductionGénérale ? ( + return réduction ? ( {formatValue( { - nodeValue: réductionGénérale, + nodeValue: réduction, }, { displayedUnit, @@ -55,17 +61,17 @@ export default function MontantRéduction({ ) : ( - displayNull && ( + !!warningCondition && !!warningTooltip && ( {formatValue(0, { displayedUnit, language })} 1.6 * SMIC`} + expression={warningCondition} contexte={{ [rémunérationBruteDottedName]: rémunérationBrute, }} > - }> + {t('Attention')} diff --git a/site/source/components/MonthOptions.tsx b/site/source/components/RéductionDeCotisations/MonthOptions.tsx similarity index 99% rename from site/source/components/MonthOptions.tsx rename to site/source/components/RéductionDeCotisations/MonthOptions.tsx index 62394f7ea..14229755d 100644 --- a/site/source/components/MonthOptions.tsx +++ b/site/source/components/RéductionDeCotisations/MonthOptions.tsx @@ -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 diff --git a/site/source/pages/simulateurs/lodeom/components/RécapitulatifTrimestre.tsx b/site/source/components/RéductionDeCotisations/RécapitulatifTrimestre.tsx similarity index 68% rename from site/source/pages/simulateurs/lodeom/components/RécapitulatifTrimestre.tsx rename to site/source/components/RéductionDeCotisations/RécapitulatifTrimestre.tsx index 12706188d..beaaecc35 100644 --- a/site/source/pages/simulateurs/lodeom/components/RécapitulatifTrimestre.tsx +++ b/site/source/components/RéductionDeCotisations/RécapitulatifTrimestre.tsx @@ -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 ( - ) @@ -76,14 +88,14 @@ export default function RécapitulatifTrimestre({ const MontantRégularisation = () => { return ( - ) @@ -96,19 +108,20 @@ export default function RécapitulatifTrimestre({ {t( - 'pages.simulateurs.lodeom.recap.header.réduction', + 'pages.simulateurs.réduction-générale.recap.header-réduction', 'Réduction calculée' )} - {/*
- {t( - 'pages.simulateurs.lodeom.recap.code671', - 'code 671(€)' - )} */} + {codeRéduction && ( + <> +
+ {codeRéduction} + + )}
- + @@ -117,14 +130,15 @@ export default function RécapitulatifTrimestre({ {t( - 'pages.simulateurs.lodeom.recap.header.régularisation', + 'pages.simulateurs.réduction-générale.recap.header-régularisation', 'Régularisation calculée' )} - {/*
- {t( - 'pages.simulateurs.lodeom.recap.code801', - 'code 801(€)' - )} */} + {codeRégularisation && ( + <> +
+ {codeRégularisation} + + )}
@@ -138,7 +152,7 @@ export default function RécapitulatifTrimestre({ {label} - + diff --git a/site/source/pages/simulateurs/lodeom/Basique.tsx b/site/source/components/RéductionDeCotisations/RéductionBasique.tsx similarity index 53% rename from site/source/pages/simulateurs/lodeom/Basique.tsx rename to site/source/components/RéductionDeCotisations/RéductionBasique.tsx index 4f01522f5..98678417b 100644 --- a/site/source/pages/simulateurs/lodeom/Basique.tsx +++ b/site/source/components/RéductionDeCotisations/RéductionBasique.tsx @@ -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} + + - - - + {warningMessage} - = 0`}> + = 0`}> - + ) diff --git a/site/source/pages/simulateurs/reduction-generale/components/RéductionGénéraleMois.tsx b/site/source/components/RéductionDeCotisations/RéductionMois.tsx similarity index 81% rename from site/source/pages/simulateurs/reduction-generale/components/RéductionGénéraleMois.tsx rename to site/source/components/RéductionDeCotisations/RéductionMois.tsx index 233662ea7..5180841fa 100644 --- a/site/source/pages/simulateurs/reduction-generale/components/RéductionGénéraleMois.tsx +++ b/site/source/components/RéductionDeCotisations/RéductionMois.tsx @@ -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 ( - ) } const MontantRégularisation = () => { return ( - ) } @@ -172,18 +171,20 @@ export default function RéductionGénéraleMois({ - + - + - + @@ -203,7 +204,7 @@ export default function RéductionGénéraleMois({ - + diff --git a/site/source/pages/simulateurs/reduction-generale/MoisParMois.tsx b/site/source/components/RéductionDeCotisations/RéductionMoisParMois.tsx similarity index 72% rename from site/source/pages/simulateurs/reduction-generale/MoisParMois.tsx rename to site/source/components/RéductionDeCotisations/RéductionMoisParMois.tsx index 30e67471f..2e267c683 100644 --- a/site/source/pages/simulateurs/reduction-generale/MoisParMois.tsx +++ b/site/source/components/RéductionDeCotisations/RéductionMoisParMois.tsx @@ -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 ? ( <> -

- {t( - 'pages.simulateurs.réduction-générale.month-by-month.caption', - 'Réduction générale mois par mois :' - )} -

+

{caption}

- - {t( - 'pages.simulateurs.réduction-générale.month-by-month.caption', - 'Réduction générale mois par mois :' - )} - + {caption} {t('Mois')} @@ -79,18 +89,21 @@ export default function RéductionGénéraleMoisParMois({ - + - + {data.length > 0 && months.map((monthName, monthIndex) => ( - { onOptionsChange(monthIndex, options) }} + warningCondition={warningCondition} + warningTooltip={warningTooltip} /> ))} @@ -128,24 +143,26 @@ export default function RéductionGénéraleMoisParMois({ {t('Trimestre')} {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' )} -
- {t( - 'pages.simulateurs.réduction-générale.recap.code671', - 'code 671(€)' + {codeRéduction && ( + <> +
+ {codeRéduction} + )} {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' )} -
- {t( - 'pages.simulateurs.réduction-générale.recap.code801', - 'code 801(€)' + {codeRégularisation && ( + <> +
+ {codeRégularisation} + )} @@ -154,8 +171,11 @@ export default function RéductionGénéraleMoisParMois({ {Object.keys(quarters).map((label, index) => ( ))} @@ -163,16 +183,12 @@ export default function RéductionGénéraleMoisParMois({ ) : ( <> -

- {t( - 'pages.simulateurs.réduction-générale.month-by-month.caption', - 'Réduction générale mois par mois :' - )} -

+

{caption}

{data.length > 0 && months.map((monthName, monthIndex) => ( - { 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) => ( ))} @@ -215,7 +236,7 @@ export default function RéductionGénéraleMoisParMois({ )} - + {warnings} ) } diff --git a/site/source/components/RégularisationSwitch.tsx b/site/source/components/RéductionDeCotisations/RégularisationSwitch.tsx similarity index 92% rename from site/source/components/RégularisationSwitch.tsx rename to site/source/components/RéductionDeCotisations/RégularisationSwitch.tsx index 9b86a1f72..13f4520f1 100644 --- a/site/source/components/RégularisationSwitch.tsx +++ b/site/source/components/RéductionDeCotisations/RégularisationSwitch.tsx @@ -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 diff --git a/site/source/pages/simulateurs/reduction-generale/components/Répartition.tsx b/site/source/components/RéductionDeCotisations/Répartition.tsx similarity index 74% rename from site/source/pages/simulateurs/reduction-generale/components/Répartition.tsx rename to site/source/components/RéductionDeCotisations/Répartition.tsx index 8006a5f62..000b9997a 100644 --- a/site/source/pages/simulateurs/reduction-generale/components/Répartition.tsx +++ b/site/source/components/RéductionDeCotisations/Répartition.tsx @@ -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, '_' )} diff --git a/site/source/components/RépartitionValue.tsx b/site/source/components/RéductionDeCotisations/RépartitionValue.tsx similarity index 100% rename from site/source/components/RépartitionValue.tsx rename to site/source/components/RéductionDeCotisations/RépartitionValue.tsx diff --git a/site/source/locales/rules-en.yaml b/site/source/locales/rules-en.yaml index c5000e7dd..ed5276fc9 100644 --- a/site/source/locales/rules-en.yaml +++ b/site/source/locales/rules-en.yaml @@ -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 diff --git a/site/source/locales/ui-en.yaml b/site/source/locales/ui-en.yaml index d35530119..fa03fd2e6 100644 --- a/site/source/locales/ui-en.yaml +++ b/site/source/locales/ui-en.yaml @@ -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 diff --git a/site/source/locales/ui-fr.yaml b/site/source/locales/ui-fr.yaml index 33571a7d7..9f21fe3f4 100644 --- a/site/source/locales/ui-fr.yaml +++ b/site/source/locales/ui-fr.yaml @@ -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 diff --git a/site/source/pages/simulateurs/lodeom/Goals.tsx b/site/source/pages/simulateurs/lodeom/Goals.tsx index 049071c80..779f67cf9 100644 --- a/site/source/pages/simulateurs/lodeom/Goals.tsx +++ b/site/source/pages/simulateurs/lodeom/Goals.tsx @@ -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 => { - const options = {} as Partial - - 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) - ) - } - 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 ( {monthByMonth ? ( - } + warningCondition={`${lodeomDottedName} = 0`} + warningTooltip={} + codeRéduction={t( + 'pages.simulateurs.lodeom.recap.code.462', + 'code 462(€)' + )} + codeRégularisation={t( + 'pages.simulateurs.lodeom.recap.code.684', + 'code 684(€)' + )} /> ) : ( - + } + warningCondition={`${lodeomDottedName} = 0`} + warningMessage={} + /> )} ) diff --git a/site/source/pages/simulateurs/lodeom/Lodeom.tsx b/site/source/pages/simulateurs/lodeom/Lodeom.tsx index 8305d28f8..48cba3376 100644 --- a/site/source/pages/simulateurs/lodeom/Lodeom.tsx +++ b/site/source/pages/simulateurs/lodeom/Lodeom.tsx @@ -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} /> + } diff --git a/site/source/pages/simulateurs/lodeom/MoisParMois.tsx b/site/source/pages/simulateurs/lodeom/MoisParMois.tsx deleted file mode 100644 index 9f168e596..000000000 --- a/site/source/pages/simulateurs/lodeom/MoisParMois.tsx +++ /dev/null @@ -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 ? ( - <> -

- {t( - 'pages.simulateurs.lodeom.month-by-month.caption', - 'Exonération Lodeom mois par mois :' - )} -

- - - {t( - 'pages.simulateurs.lodeom.month-by-month.caption', - 'Exonération Lodeom mois par mois :' - )} - - - - {t('Mois')} - - {/* TODO: remplacer par rémunérationBruteDottedName lorsque ... */} - - - - - - - - - - - - {data.length > 0 && - months.map((monthName, monthIndex) => ( - { - onRémunérationChange(monthIndex, rémunérationBrute) - }} - onOptionsChange={(monthIndex: number, options: Options) => { - onOptionsChange(monthIndex, options) - }} - /> - ))} - - - - - -

- {t( - 'pages.simulateurs.lodeom.recap.caption', - 'Récapitulatif trimestriel :' - )} -

- - - {t( - 'pages.simulateurs.lodeom.recap.caption', - 'Récapitulatif trimestriel :' - )} - - - - {t('Trimestre')} - - {t( - 'pages.simulateurs.lodeom.recap.header.réduction', - 'Réduction calculée' - )} - {/*
- {t( - 'pages.simulateurs.lodeom.recap.code671', - 'code 671(€)' - )} */} - - - {t( - 'pages.simulateurs.lodeom.recap.header.régularisation', - 'Régularisation calculée' - )} - {/*
- {t( - 'pages.simulateurs.lodeom.recap.code801', - 'code 801(€)' - )} */} - - - - - {Object.keys(quarters).map((label, index) => ( - - ))} - -
- - ) : ( - <> -

- {t( - 'pages.simulateurs.lodeom.month-by-month.caption', - 'Exonération Lodeom mois par mois :' - )} -

- {data.length > 0 && - months.map((monthName, monthIndex) => ( - { - onRémunérationChange(monthIndex, rémunérationBrute) - }} - onOptionsChange={(monthIndex: number, options: Options) => { - onOptionsChange(monthIndex, options) - }} - mobileVersion={true} - /> - ))} - - - -

- {t( - 'pages.simulateurs.lodeom.recap.caption', - 'Récapitulatif trimestriel :' - )} -

- {Object.keys(quarters).map((label, index) => ( - - ))} - - )} - - - {t( - 'pages.simulateurs.lodeom.options.description', - "Ajoute des champs pour moduler l'activité du salarié" - )} - - - - - ) -} - -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; - } -` diff --git a/site/source/pages/simulateurs/lodeom/components/LodeomMois.tsx b/site/source/pages/simulateurs/lodeom/components/LodeomMois.tsx deleted file mode 100644 index 0a77d3867..000000000 --- a/site/source/pages/simulateurs/lodeom/components/LodeomMois.tsx +++ /dev/null @@ -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 ( - - 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 ( - - ) - } - - const MontantLodeom = () => { - return ( - - ) - } - - const MontantRégularisation = () => { - return ( - - ) - } - - return mobileVersion ? ( -
- {monthName} - - - - - - - - - - - - - {isOptionVisible && ( - - - - )} - - - - - - - - - - - - - - - - - - - - - - - - -
- ) : ( - <> - - {monthName} - - - - - - - - - - - - - - {isOptionVisible && ( - - - - - - - )} - - ) -} - -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}; -` diff --git a/site/source/pages/simulateurs/lodeom/components/MontantRéduction.tsx b/site/source/pages/simulateurs/lodeom/components/MontantRéduction.tsx deleted file mode 100644 index 35e31f8f5..000000000 --- a/site/source/pages/simulateurs/lodeom/components/MontantRéduction.tsx +++ /dev/null @@ -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 = - - return lodeom ? ( - - - {formatValue( - { - nodeValue: lodeom, - }, - { - displayedUnit, - language, - } - )} - - - - ) : ( - displayNull && ( - - {formatValue(0, { displayedUnit, language })} - - - }> - {t('Attention')} - - - - - ) - ) -} - -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}; -` diff --git a/site/source/pages/simulateurs/lodeom/components/Répartition.tsx b/site/source/pages/simulateurs/lodeom/components/Répartition.tsx deleted file mode 100644 index 8f9b7767b..000000000 --- a/site/source/pages/simulateurs/lodeom/components/Répartition.tsx +++ /dev/null @@ -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 ( - <> - - - Détail du montant : - - - - - - - - - - - - ) -} - -const StyledUl = styled(Ul)` - margin-top: 0; -` -const StyledLi = styled(Li)` - &::before { - margin-top: ${({ theme }) => theme.spacings.sm}; - } -` diff --git a/site/source/pages/simulateurs/lodeom/utils.ts b/site/source/pages/simulateurs/lodeom/utils.ts deleted file mode 100644 index e37b8f789..000000000 --- a/site/source/pages/simulateurs/lodeom/utils.ts +++ /dev/null @@ -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 -): 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 -): 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 -): 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 -): 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, - 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 -): 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 - }, - [] - ) -} diff --git a/site/source/pages/simulateurs/reduction-generale/Basique.tsx b/site/source/pages/simulateurs/reduction-generale/Basique.tsx deleted file mode 100644 index 76d3f439f..000000000 --- a/site/source/pages/simulateurs/reduction-generale/Basique.tsx +++ /dev/null @@ -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 ( - <> - - - - 1.6 * SMIC`}> - - - - - - - - = 0`}> - - - - - - ) -} diff --git a/site/source/pages/simulateurs/reduction-generale/Goals.tsx b/site/source/pages/simulateurs/reduction-generale/Goals.tsx index 9b6ebc255..f55754697 100644 --- a/site/source/pages/simulateurs/reduction-generale/Goals.tsx +++ b/site/source/pages/simulateurs/reduction-generale/Goals.tsx @@ -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 => { - const options = {} as Partial - - 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) - ) - } - 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 ( {monthByMonth ? ( - } + warningCondition={`${rémunérationBruteDottedName} > 1.6 * SMIC`} + warningTooltip={} + 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(€)' + )} /> ) : ( - } + warningCondition={`${rémunérationBruteDottedName} > 1.6 * SMIC`} + warningMessage={} /> )} diff --git a/site/source/pages/simulateurs/reduction-generale/RéductionGénérale.tsx b/site/source/pages/simulateurs/reduction-generale/RéductionGénérale.tsx index bc92ef371..2bb1e6111 100644 --- a/site/source/pages/simulateurs/reduction-generale/RéductionGénérale.tsx +++ b/site/source/pages/simulateurs/reduction-generale/RéductionGénérale.tsx @@ -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() diff --git a/site/source/pages/simulateurs/reduction-generale/components/CongésPayésSwitch.tsx b/site/source/pages/simulateurs/reduction-generale/components/CongésPayésSwitch.tsx index 87ca3e5cc..fac2f5855 100644 --- a/site/source/pages/simulateurs/reduction-generale/components/CongésPayésSwitch.tsx +++ b/site/source/pages/simulateurs/reduction-generale/components/CongésPayésSwitch.tsx @@ -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' diff --git a/site/source/pages/simulateurs/reduction-generale/components/RécapitulatifTrimestre.tsx b/site/source/pages/simulateurs/reduction-generale/components/RécapitulatifTrimestre.tsx deleted file mode 100644 index ed944f889..000000000 --- a/site/source/pages/simulateurs/reduction-generale/components/RécapitulatifTrimestre.tsx +++ /dev/null @@ -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 ( - - ) - } - - const Montant801 = () => { - return ( - - ) - } - - return mobileVersion ? ( -
- {label} - - - - {t( - 'pages.simulateurs.réduction-générale.recap.header', - 'Réduction calculée' - )} -
- {t( - 'pages.simulateurs.réduction-générale.recap.code671', - 'code 671(€)' - )} -
-
- - - - - -
- - - - - {t( - 'pages.simulateurs.réduction-générale.recap.header', - 'Réduction calculée' - )} -
- {t( - 'pages.simulateurs.réduction-générale.recap.code801', - 'code 801(€)' - )} -
-
- - - - - -
-
- ) : ( - - {label} - - - - - - - - ) -} - -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; -` diff --git a/site/source/pages/simulateurs/reduction-generale/utils.ts b/site/source/utils/réductionDeCotisations.ts similarity index 60% rename from site/source/pages/simulateurs/reduction-generale/utils.ts rename to site/source/utils/réductionDeCotisations.ts index 6db7da764..4c3f8240b 100644 --- a/site/source/pages/simulateurs/reduction-generale/utils.ts +++ b/site/source/utils/réductionDeCotisations.ts @@ -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 +): 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, + dispatch: Dispatch +): 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 -): 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 -): 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 -): 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 ): 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, year: number, - régularisationMethod: RégularisationMethod + régularisationMethod: RégularisationMethod, + engine: Engine ): 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 +): 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 => { + const options = {} as Partial + + 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 +): 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) + ) +} + +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 +): 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 +): 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 +): 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 ): 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)