From 8f6f33556e7cd32159094428345d33e6752a2658 Mon Sep 17 00:00:00 2001 From: Alice Dahan Date: Fri, 20 Dec 2024 11:22:12 +0100 Subject: [PATCH] =?UTF-8?q?feat(lodeom):=20ajout=20du=20simulateur=20lodem?= =?UTF-8?q?=20(zone=20un=20-=20bar=C3=A8me=20comp=C3=A9titivit=C3=A9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../règles/salarié/cotisations.publicodes | 3 - .../components/MonthOptions.tsx | 6 +- .../components/RégularisationSwitch.tsx | 2 +- .../components/RépartitionValue.tsx | 10 +- site/source/locales/ui-en.yaml | 41 +- site/source/locales/ui-fr.yaml | 42 +- .../simulateurs-et-assistants/metadata-src.ts | 2 + .../pages/simulateurs/lodeom/Basique.tsx | 69 +++ .../source/pages/simulateurs/lodeom/Goals.tsx | 190 ++++++++ .../pages/simulateurs/lodeom/Lodeom.tsx | 64 +++ .../pages/simulateurs/lodeom/MoisParMois.tsx | 256 ++++++++++ .../lodeom/components/LodeomMois.tsx | 254 ++++++++++ .../lodeom/components/MontantRéduction.tsx | 91 ++++ .../components/RécapitulatifTrimestre.tsx | 161 +++++++ .../lodeom/components/Répartition.tsx | 58 +++ .../lodeom/components/WarningSalaireTrans.tsx | 11 + .../lodeom/components/Warnings.tsx | 33 ++ .../source/pages/simulateurs/lodeom/config.ts | 31 ++ .../simulateurs/lodeom/simulationConfig.ts | 48 ++ site/source/pages/simulateurs/lodeom/utils.ts | 449 ++++++++++++++++++ .../reduction-generale/RéductionGénérale.tsx | 2 +- .../components/RéductionGénéraleMois.tsx | 2 +- .../components/Répartition.tsx | 2 +- .../components/WarningSalaireTrans.tsx | 2 +- .../_components/SimulateursChoice.tsx | 2 +- site/source/sitePaths.ts | 2 + 26 files changed, 1812 insertions(+), 21 deletions(-) rename site/source/{pages/simulateurs/reduction-generale => }/components/MonthOptions.tsx (98%) rename site/source/{pages/simulateurs/reduction-generale => }/components/RégularisationSwitch.tsx (92%) rename site/source/{pages/simulateurs/reduction-generale => }/components/RépartitionValue.tsx (81%) create mode 100644 site/source/pages/simulateurs/lodeom/Basique.tsx create mode 100644 site/source/pages/simulateurs/lodeom/Goals.tsx create mode 100644 site/source/pages/simulateurs/lodeom/Lodeom.tsx create mode 100644 site/source/pages/simulateurs/lodeom/MoisParMois.tsx create mode 100644 site/source/pages/simulateurs/lodeom/components/LodeomMois.tsx create mode 100644 site/source/pages/simulateurs/lodeom/components/MontantRéduction.tsx create mode 100644 site/source/pages/simulateurs/lodeom/components/RécapitulatifTrimestre.tsx create mode 100644 site/source/pages/simulateurs/lodeom/components/Répartition.tsx create mode 100644 site/source/pages/simulateurs/lodeom/components/WarningSalaireTrans.tsx create mode 100644 site/source/pages/simulateurs/lodeom/components/Warnings.tsx create mode 100644 site/source/pages/simulateurs/lodeom/config.ts create mode 100644 site/source/pages/simulateurs/lodeom/simulationConfig.ts create mode 100644 site/source/pages/simulateurs/lodeom/utils.ts diff --git a/modele-social/règles/salarié/cotisations.publicodes b/modele-social/règles/salarié/cotisations.publicodes index fce2a65cc..c748cb2a2 100644 --- a/modele-social/règles/salarié/cotisations.publicodes +++ b/modele-social/règles/salarié/cotisations.publicodes @@ -402,7 +402,6 @@ salarié . cotisations . exonérations . lodeom . montant: avec: coefficient: - privé: oui variations: - si: cotisations . assiette <= seuil inflexion * temps de travail . SMIC alors: T @@ -465,12 +464,10 @@ salarié . cotisations . exonérations . lodeom . montant: alors: 4.5 imputation retraite complémentaire: - privé: oui non applicable si: lodeom . zone deux valeur: lodeom . montant - imputation sécurité sociale imputation sécurité sociale: - privé: oui non applicable si: lodeom . zone deux produit: - lodeom . montant diff --git a/site/source/pages/simulateurs/reduction-generale/components/MonthOptions.tsx b/site/source/components/MonthOptions.tsx similarity index 98% rename from site/source/pages/simulateurs/reduction-generale/components/MonthOptions.tsx rename to site/source/components/MonthOptions.tsx index 556363c88..62394f7ea 100644 --- a/site/source/pages/simulateurs/reduction-generale/components/MonthOptions.tsx +++ b/site/source/components/MonthOptions.tsx @@ -20,7 +20,7 @@ import { Li, Ul } from '@/design-system/typography/list' import { Body, SmallBody } from '@/design-system/typography/paragraphs' import { useMediaQuery } from '@/hooks/useMediaQuery' -import { Options } from '../utils' +import { Options } from '../pages/simulateurs/reduction-generale/utils' type Props = { month: string @@ -260,8 +260,8 @@ const HeuresSupplémentairesPopoverContent = () => ( Le nombre d'heures supplémentaires et complémentaires est utilisé dans le - calcul de la réduction générale : la rémunération brute est comparée au - montant du SMIC majoré de ce nombre d'heures. + calcul de la réduction : la rémunération brute est comparée au montant du + SMIC majoré de ce nombre d'heures. ) diff --git a/site/source/pages/simulateurs/reduction-generale/components/RégularisationSwitch.tsx b/site/source/components/RégularisationSwitch.tsx similarity index 92% rename from site/source/pages/simulateurs/reduction-generale/components/RégularisationSwitch.tsx rename to site/source/components/RégularisationSwitch.tsx index 93c413860..9b86a1f72 100644 --- a/site/source/pages/simulateurs/reduction-generale/components/RégularisationSwitch.tsx +++ b/site/source/components/RégularisationSwitch.tsx @@ -2,7 +2,7 @@ import { useTranslation } from 'react-i18next' import { Radio, ToggleGroup } from '@/design-system' -import { RégularisationMethod } from '../utils' +import { RégularisationMethod } from '../pages/simulateurs/reduction-generale/utils' type Props = { régularisationMethod: RégularisationMethod diff --git a/site/source/pages/simulateurs/reduction-generale/components/RépartitionValue.tsx b/site/source/components/RépartitionValue.tsx similarity index 81% rename from site/source/pages/simulateurs/reduction-generale/components/RépartitionValue.tsx rename to site/source/components/RépartitionValue.tsx index 423fd2568..aa10cba0a 100644 --- a/site/source/pages/simulateurs/reduction-generale/components/RépartitionValue.tsx +++ b/site/source/components/RépartitionValue.tsx @@ -4,7 +4,7 @@ import { styled } from 'styled-components' import LectureGuide from '@/components/Simulation/LectureGuide' import { Grid } from '@/design-system/layout' -import { Body } from '@/design-system/typography/paragraphs' +import { SmallBody } from '@/design-system/typography/paragraphs' type Props = { value: number @@ -26,19 +26,19 @@ export default function RépartitionValue({ value, label, idPrefix }: Props) { spacing={2} > - {label} + {label} - + {formatValue(value, { displayedUnit: '€', precision: 2, language, })} - + @@ -55,7 +55,7 @@ const StyledValue = styled.div` } ` -const StyledBody = styled(Body)` +const StyledSmallBody = styled(SmallBody)` color: ${({ theme }) => theme.colors.extended.grey[100]}; margin: 0; padding: ${({ theme }) => `${theme.spacings.xs} ${theme.spacings.sm} 0 0`}; diff --git a/site/source/locales/ui-en.yaml b/site/source/locales/ui-en.yaml index 12da894f2..33b297b6a 100644 --- a/site/source/locales/ui-en.yaml +++ b/site/source/locales/ui-en.yaml @@ -1447,6 +1447,43 @@ pages: gains (sale of securities, sale of patents). These regimes are not included in the simulator. title: Corporate tax simulator + lodeom: + legend: Employee's gross salary and applicable Lodeom exemption + meta: + description: Estimated amount of Lodeom exemption. This exemption applies, under + certain conditions, to overseas employees. + 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 + shortname: Lodeom exemption + tab: + month: Monthly exemption + month-by-month: Month-by-month exemption + year: Annual exemption + title: Lodeom exemption simulator + warnings: + JEI: The Lodeom exemption cannot be combined with the Young Innovative Company + (JEI) exemption. + salaire: + zone-un: + barème-compétitivité: The competitiveness scale only applies to salaries below + 2.2 SMIC. This means, for 2024, a total remuneration not exceeding + <1>€3,964 gross per month. + stage: The Lodeom exemption does not apply to internship bonuses. médecin: meta: description: Calculation of net income after deduction of contributions, based @@ -1493,8 +1530,8 @@ pages: description: Adds fields to modulate employee activity heures-sup: popover: "<0>The number of hours of overtime and complementary work is used to - calculate the general reduction: gross remuneration is compared with - the SMIC increased by this number of hours." + calculate the reduction: gross remuneration is compared with the + SMIC increased by this number of hours." label: heures-complémentaires: Number of overtime hours heures-supplémentaires: Number of overtime hours diff --git a/site/source/locales/ui-fr.yaml b/site/source/locales/ui-fr.yaml index a51e1f9e9..93370f4f5 100644 --- a/site/source/locales/ui-fr.yaml +++ b/site/source/locales/ui-fr.yaml @@ -1540,6 +1540,44 @@ pages: plus-values (cession de titres, cession de brevets). Ces régimes ne sont pas intégrés dans le simulateur. title: Simulateur d'impôt sur les sociétés + lodeom: + legend: Rémunération brute du salarié et exonération Lodeom applicable + meta: + description: Estimation du montant de l'exonération Lodeom. Cette exonération + est applicable, sous conditions, aux salariés d'Outre-mer. + 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 + shortname: Éxonération Lodeom + tab: + month: Exonération mensuelle + month-by-month: Exonération mois par mois + year: Exonération annuelle + title: Simulateur d'éxonération Lodeom + warnings: + JEI: L'exonération Lodeom n'est pas cumulable avec l'exonération Jeune + Entreprise Innovante (JEI). + salaire: + zone-un: + barème-compétitivité: Le barème de compétitivité concerne uniquement les + salaires inférieurs à 2,2 SMIC. C'est-à-dire, pour 2024, une + rémunération totale qui ne dépasse pas <1>3 964 € bruts par + mois. + stage: L'exonération Lodeom ne s'applique pas sur les gratifications de stage. médecin: meta: description: Calcul du revenu net après déduction des cotisations à partir du @@ -1587,8 +1625,8 @@ pages: description: Ajoute des champs pour moduler l'activité du salarié heures-sup: popover: "<0>Le nombre d'heures supplémentaires et complémentaires est utilisé - dans le calcul de la réduction générale : la rémunération brute est - comparée au montant du SMIC majoré de ce nombre d'heures." + dans le calcul de la réduction : la rémunération brute est comparée + au montant du SMIC majoré de ce nombre d'heures." label: heures-complémentaires: Nombre d'heures complémentaires heures-supplémentaires: Nombre d'heures supplémentaires diff --git a/site/source/pages/simulateurs-et-assistants/metadata-src.ts b/site/source/pages/simulateurs-et-assistants/metadata-src.ts index 6be4040f7..1a550904e 100644 --- a/site/source/pages/simulateurs-et-assistants/metadata-src.ts +++ b/site/source/pages/simulateurs-et-assistants/metadata-src.ts @@ -22,6 +22,7 @@ import { eurlConfig } from '../simulateurs/eurl/config' import { expertComptableConfig } from '../simulateurs/expert-comptable/config' import { impôtSociétéConfig } from '../simulateurs/impot-societe/config' import { indépendantConfig } from '../simulateurs/indépendant/config' +import { lodeomConfig } from '../simulateurs/lodeom/config' import { médecinConfig } from '../simulateurs/médecin/config' import { pamcConfig } from '../simulateurs/pamc/config' import { pharmacienConfig } from '../simulateurs/pharmacien/config' @@ -63,6 +64,7 @@ const getMetadataSrc = (params: SimulatorsDataParams) => { ...impôtSociétéConfig(params), ...cipavConfig(params), ...réductionGénéraleConfig(params), + ...lodeomConfig(params), // assistants: ...choixStatutJuridiqueConfig(params), diff --git a/site/source/pages/simulateurs/lodeom/Basique.tsx b/site/source/pages/simulateurs/lodeom/Basique.tsx new file mode 100644 index 000000000..4f01522f5 --- /dev/null +++ b/site/source/pages/simulateurs/lodeom/Basique.tsx @@ -0,0 +1,69 @@ +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 { lodeomDottedName, rémunérationBruteDottedName } from './utils' + +type Props = { + onUpdate: () => void +} + +export default function LodeomBasique({ onUpdate }: 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, + } + + return ( + <> + + + + + + + + + + + + = 0`}> + + + + + + ) +} diff --git a/site/source/pages/simulateurs/lodeom/Goals.tsx b/site/source/pages/simulateurs/lodeom/Goals.tsx new file mode 100644 index 000000000..049071c80 --- /dev/null +++ b/site/source/pages/simulateurs/lodeom/Goals.tsx @@ -0,0 +1,190 @@ +import { DottedName } from 'modele-social' +import { PublicodesExpression } from 'publicodes' +import { useCallback, useEffect, useRef, useState } from 'react' +import { useDispatch, useSelector } from 'react-redux' + +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, + MonthState, + Options, + reevaluateLodeomMoisParMois, + RégularisationMethod, + rémunérationBruteDottedName, +} from './utils' + +type SituationType = Situation & { + [heuresSupplémentairesDottedName]?: { + explanation: { + nodeValue: number + } + } + [heuresComplémentairesDottedName]?: { + valeur: number + } +} + +export default function LodeomSimulationGoals({ + monthByMonth, + toggles, + legend, + régularisationMethod, +}: { + monthByMonth: boolean + toggles?: React.ReactNode + legend: string + régularisationMethod: RégularisationMethod +}) { + const engine = useEngine() + const dispatch = useDispatch() + const [lodeomMoisParMoisData, setData] = useState([]) + const year = useYear() + const situation = useSelector(situationSelector) as SituationType + const previousSituation = useRef(situation) + + const initializeLodeomMoisParMoisData = useCallback(() => { + const data = getInitialLodeomMoisParMois(year, engine) + setData(data) + }, [engine, year]) + + useEffect(() => { + if (lodeomMoisParMoisData.length === 0) { + initializeLodeomMoisParMoisData() + } + }, [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( + previousSituation.current, + situation + ) + + const updatedData = previousData.map((data) => { + return { + ...data, + options: { + ...data.options, + ...newOptions, + }, + } + }, []) + + return reevaluateLodeomMoisParMois( + updatedData, + engine, + year, + régularisationMethod + ) + }) + }, [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], + rémunérationBrute, + } + + updateRémunérationBruteAnnuelle(updatedData) + + return reevaluateLodeomMoisParMois( + updatedData, + engine, + year, + régularisationMethod + ) + }) + } + + const onOptionsChange = (monthIndex: number, options: Options) => { + setData((previousData) => { + const updatedData = [...previousData] + updatedData[monthIndex] = { + ...updatedData[monthIndex], + options, + } + + return reevaluateLodeomMoisParMois( + updatedData, + engine, + year, + régularisationMethod + ) + }) + } + + return ( + + {monthByMonth ? ( + + ) : ( + + )} + + ) +} diff --git a/site/source/pages/simulateurs/lodeom/Lodeom.tsx b/site/source/pages/simulateurs/lodeom/Lodeom.tsx new file mode 100644 index 000000000..8305d28f8 --- /dev/null +++ b/site/source/pages/simulateurs/lodeom/Lodeom.tsx @@ -0,0 +1,64 @@ +import { useCallback, useState } from 'react' +import { useTranslation } from 'react-i18next' + +import PeriodSwitch from '@/components/PeriodSwitch' +import RégularisationSwitch from '@/components/RégularisationSwitch' +import { SelectSimulationYear } from '@/components/SelectSimulationYear' +import SimulateurWarning from '@/components/SimulateurWarning' +import Simulation from '@/components/Simulation' + +import LodeomSimulationGoals from './Goals' +import { RégularisationMethod } from './utils' + +export default function LodeomSimulation() { + const { t } = useTranslation() + const [monthByMonth, setMonthByMonth] = useState(false) + const periods = [ + { + label: t('pages.simulateurs.lodeom.tab.month', 'Exonération mensuelle'), + unit: '€/mois', + }, + { + label: t('pages.simulateurs.lodeom.tab.year', 'Exonération annuelle'), + unit: '€/an', + }, + { + label: t( + 'pages.simulateurs.lodeom.tab.month-by-month', + 'Exonération mois par mois' + ), + unit: '€', + }, + ] + const onPeriodSwitch = useCallback((unit: string) => { + setMonthByMonth(unit === '€') + }, []) + + const [régularisationMethod, setRégularisationMethod] = + useState('progressive') + + return ( + <> + }> + + + + + + } + régularisationMethod={régularisationMethod} + /> + + + ) +} diff --git a/site/source/pages/simulateurs/lodeom/MoisParMois.tsx b/site/source/pages/simulateurs/lodeom/MoisParMois.tsx new file mode 100644 index 000000000..9f168e596 --- /dev/null +++ b/site/source/pages/simulateurs/lodeom/MoisParMois.tsx @@ -0,0 +1,256 @@ +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 new file mode 100644 index 000000000..0a77d3867 --- /dev/null +++ b/site/source/pages/simulateurs/lodeom/components/LodeomMois.tsx @@ -0,0 +1,254 @@ +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 new file mode 100644 index 000000000..35e31f8f5 --- /dev/null +++ b/site/source/pages/simulateurs/lodeom/components/MontantRéduction.tsx @@ -0,0 +1,91 @@ +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écapitulatifTrimestre.tsx b/site/source/pages/simulateurs/lodeom/components/RécapitulatifTrimestre.tsx new file mode 100644 index 000000000..12706188d --- /dev/null +++ b/site/source/pages/simulateurs/lodeom/components/RécapitulatifTrimestre.tsx @@ -0,0 +1,161 @@ +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.lodeom.répartition.IRC + + monthData.régularisation.répartition.IRC + ) + ), + Urssaf: sumAll( + data.map( + (monthData) => + monthData.lodeom.répartition.Urssaf + + monthData.régularisation.répartition.Urssaf + ) + ), + } + let réduction = sumAll(data.map((monthData) => monthData.lodeom.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 MontantExonération = () => { + return ( + + ) + } + + const MontantRégularisation = () => { + return ( + + ) + } + + return mobileVersion ? ( +
+ {label} + + + + {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(€)' + )} */} +
+
+ + + + + +
+
+ ) : ( + + {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/lodeom/components/Répartition.tsx b/site/source/pages/simulateurs/lodeom/components/Répartition.tsx new file mode 100644 index 000000000..8f9b7767b --- /dev/null +++ b/site/source/pages/simulateurs/lodeom/components/Répartition.tsx @@ -0,0 +1,58 @@ +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/components/WarningSalaireTrans.tsx b/site/source/pages/simulateurs/lodeom/components/WarningSalaireTrans.tsx new file mode 100644 index 000000000..76a6e2c64 --- /dev/null +++ b/site/source/pages/simulateurs/lodeom/components/WarningSalaireTrans.tsx @@ -0,0 +1,11 @@ +import { Trans } from 'react-i18next' + +export default function WarningSalaireTrans() { + return ( + + Le barème de compétitivité concerne uniquement les salaires inférieurs à + 2,2 SMIC. C'est-à-dire, pour 2024, une rémunération totale qui ne dépasse + pas 3 964 € bruts par mois. + + ) +} diff --git a/site/source/pages/simulateurs/lodeom/components/Warnings.tsx b/site/source/pages/simulateurs/lodeom/components/Warnings.tsx new file mode 100644 index 000000000..068980c42 --- /dev/null +++ b/site/source/pages/simulateurs/lodeom/components/Warnings.tsx @@ -0,0 +1,33 @@ +import { Trans } from 'react-i18next' + +import { Condition } from '@/components/EngineValue/Condition' +import { Message } from '@/design-system' +import { Body } from '@/design-system/typography/paragraphs' + +export default function Warnings() { + return ( + <> + + + + + L'exonération Lodeom n'est pas cumulable avec l'exonération Jeune + Entreprise Innovante (JEI). + + + + + + + + + + L'exonération Lodeom ne s'applique pas sur les gratifications de + stage. + + + + + + ) +} diff --git a/site/source/pages/simulateurs/lodeom/config.ts b/site/source/pages/simulateurs/lodeom/config.ts new file mode 100644 index 000000000..034c96789 --- /dev/null +++ b/site/source/pages/simulateurs/lodeom/config.ts @@ -0,0 +1,31 @@ +import { config } from '../_configs/config' +import { SimulatorsDataParams } from '../_configs/types' +import RéductionGénéraleSimulation from './Lodeom' +import { configRéductionGénérale } from './simulationConfig' + +export function lodeomConfig({ t, sitePaths }: SimulatorsDataParams) { + return config({ + id: 'lodeom', + beta: true, + tracking: 'lodeom', + icône: '🏷️', + iframePath: 'simulateur-lodeom', + pathId: 'simulateurs.lodeom', + shortName: t('pages.simulateurs.lodeom.shortname', 'Éxonération Lodeom'), + title: t( + 'pages.simulateurs.lodeom.title', + "Simulateur d'éxonération Lodeom" + ), + meta: { + title: t('pages.simulateurs.lodeom.meta.title', 'Éxonération Lodeom'), + description: t( + 'pages.simulateurs.lodeom.meta.description', + "Estimation du montant de l'exonération Lodeom. Cette exonération est applicable, sous conditions, aux salariés d'Outre-mer." + ), + }, + nextSteps: ['salarié'], + path: sitePaths.simulateurs.lodeom, + simulation: configRéductionGénérale, + component: RéductionGénéraleSimulation, + } as const) +} diff --git a/site/source/pages/simulateurs/lodeom/simulationConfig.ts b/site/source/pages/simulateurs/lodeom/simulationConfig.ts new file mode 100644 index 000000000..312a17771 --- /dev/null +++ b/site/source/pages/simulateurs/lodeom/simulationConfig.ts @@ -0,0 +1,48 @@ +import { SimulationConfig } from '@/domaine/SimulationConfig' + +export const configRéductionGénérale: SimulationConfig = { + // TODO: remplacer 'salarié . cotisations . assiette' par 'salarié . rémunération . brut' + // lorsque cette dernière n'incluera plus les frais professionnels. + 'objectifs exclusifs': ['salarié . cotisations . assiette'], + objectifs: ['salarié . cotisations . exonérations . lodeom . montant'], + questions: { + "à l'affiche": [ + { + label: 'Temps partiel', + dottedName: 'salarié . contrat . temps de travail . temps partiel', + }, + { + label: 'Heures supplémentaires', + dottedName: 'salarié . temps de travail . heures supplémentaires', + }, + { + label: 'Heures complémentaires', + dottedName: 'salarié . temps de travail . heures complémentaires', + }, + { + label: 'DFS', + dottedName: 'salarié . régimes spécifiques . DFS', + }, + { + label: 'JEI', + dottedName: 'salarié . cotisations . exonérations . JEI', + }, + ], + 'liste noire': [ + 'entreprise . salariés . effectif . seuil', + 'salarié . contrat . CDD . motif', + 'salarié . rémunération . primes . activité . base', + 'salarié . rémunération . avantages en nature', + ], + 'non prioritaires': ['salarié . convention collective'], + }, + 'unité par défaut': '€/an', + situation: { + dirigeant: 'non', + 'entreprise . catégorie juridique': "''", + 'entreprise . imposition': 'non', + 'salarié . cotisations . exonérations . lodeom . zone un': "'oui'", + 'salarié . cotisations . exonérations . lodeom . zone un . barème compétitivité': + "'oui'", + }, +} diff --git a/site/source/pages/simulateurs/lodeom/utils.ts b/site/source/pages/simulateurs/lodeom/utils.ts new file mode 100644 index 000000000..e37b8f789 --- /dev/null +++ b/site/source/pages/simulateurs/lodeom/utils.ts @@ -0,0 +1,449 @@ +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/RéductionGénérale.tsx b/site/source/pages/simulateurs/reduction-generale/RéductionGénérale.tsx index 8e8ee7636..bc92ef371 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 @@ -2,13 +2,13 @@ import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import PeriodSwitch from '@/components/PeriodSwitch' +import RégularisationSwitch from '@/components/RégularisationSwitch' import { SelectSimulationYear } from '@/components/SelectSimulationYear' import SimulateurWarning from '@/components/SimulateurWarning' import Simulation from '@/components/Simulation' import CongésPayésSwitch from './components/CongésPayésSwitch' import EffectifSwitch from './components/EffectifSwitch' -import RégularisationSwitch from './components/RégularisationSwitch' import RéductionGénéraleSimulationGoals from './Goals' import { RégularisationMethod } from './utils' diff --git a/site/source/pages/simulateurs/reduction-generale/components/RéductionGénéraleMois.tsx b/site/source/pages/simulateurs/reduction-generale/components/RéductionGénéraleMois.tsx index 562525e33..233662ea7 100644 --- a/site/source/pages/simulateurs/reduction-generale/components/RéductionGénéraleMois.tsx +++ b/site/source/pages/simulateurs/reduction-generale/components/RéductionGénéraleMois.tsx @@ -4,6 +4,7 @@ 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' @@ -19,7 +20,6 @@ import { rémunérationBruteDottedName, } from '../utils' import MontantRéduction from './MontantRéduction' -import MonthOptions from './MonthOptions' type Props = { monthName: string diff --git a/site/source/pages/simulateurs/reduction-generale/components/Répartition.tsx b/site/source/pages/simulateurs/reduction-generale/components/Répartition.tsx index d95811508..8006a5f62 100644 --- a/site/source/pages/simulateurs/reduction-generale/components/Répartition.tsx +++ b/site/source/pages/simulateurs/reduction-generale/components/Répartition.tsx @@ -1,6 +1,7 @@ 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' @@ -9,7 +10,6 @@ import { réductionGénéraleDottedName, Répartition as RépartitionType, } from '../utils' -import RépartitionValue from './RépartitionValue' type Props = { répartition: RépartitionType diff --git a/site/source/pages/simulateurs/reduction-generale/components/WarningSalaireTrans.tsx b/site/source/pages/simulateurs/reduction-generale/components/WarningSalaireTrans.tsx index e72b38ac8..7b0430f3a 100644 --- a/site/source/pages/simulateurs/reduction-generale/components/WarningSalaireTrans.tsx +++ b/site/source/pages/simulateurs/reduction-generale/components/WarningSalaireTrans.tsx @@ -5,7 +5,7 @@ export default function WarningSalaireTrans() { La RGCP concerne uniquement les salaires inférieurs à 1,6 SMIC. C'est-à-dire, pour 2024, une rémunération totale qui ne dépasse pas{' '} - 2 827,07 € bruts par mois. + 2 827,07 € bruts par mois. ) } diff --git a/site/source/pages/statistiques/_components/SimulateursChoice.tsx b/site/source/pages/statistiques/_components/SimulateursChoice.tsx index a3ceb52c0..eccb7dc53 100644 --- a/site/source/pages/statistiques/_components/SimulateursChoice.tsx +++ b/site/source/pages/statistiques/_components/SimulateursChoice.tsx @@ -1,9 +1,9 @@ +import { SimulateurCard } from '@/components/SimulateurCard' import { Item } from '@/design-system' import { Emoji } from '@/design-system/emoji' import { Select } from '@/design-system/field/Select' import useSimulatorsData from '@/hooks/useSimulatorsData' -import { SimulateurCard } from '../../../components/SimulateurCard' import { getFilter } from '../StatsPage' import { Filter } from '../types' diff --git a/site/source/sitePaths.ts b/site/source/sitePaths.ts index 1558067a1..e08f230d8 100644 --- a/site/source/sitePaths.ts +++ b/site/source/sitePaths.ts @@ -73,6 +73,7 @@ const rawSitePathsFr = { is: 'impot-societe', dividendes: 'dividendes', 'réduction-générale': 'réduction-générale', + lodeom: 'lodeom', }, nouveautés: { index: 'nouveautés', @@ -172,6 +173,7 @@ const rawSitePathsEn = { is: 'corporate-tax', dividendes: 'dividends', 'réduction-générale': 'réduction-générale', + lodeom: 'lodeom', }, nouveautés: { index: 'news',