feat(lodeom): ajout du simulateur lodem (zone un - barème compétitivité)

feat/2909-2-init-simulateur
Alice Dahan 2024-12-20 11:22:12 +01:00
parent cd9ea8ac12
commit 8f6f33556e
26 changed files with 1812 additions and 21 deletions

View File

@ -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

View File

@ -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 = () => (
<Trans i18nKey="pages.simulateurs.réduction-générale.options.heures-sup.popover">
<Body>
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.
</Body>
</Trans>
)

View File

@ -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

View File

@ -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}
>
<Grid item md="auto" sm={9} xs={8}>
<StyledBody id={`${idPrefix}-label`}>{label}</StyledBody>
<StyledSmallBody id={`${idPrefix}-label`}>{label}</StyledSmallBody>
</Grid>
<LectureGuide />
<Grid item>
<StyledBody id={`${idPrefix}-value`}>
<StyledSmallBody id={`${idPrefix}-value`}>
{formatValue(value, {
displayedUnit: '€',
precision: 2,
language,
})}
</StyledBody>
</StyledSmallBody>
</Grid>
</Grid>
</StyledValue>
@ -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`};

View File

@ -1447,6 +1447,43 @@ pages:
gains (sale of securities, sale of patents). These regimes are not
included in the simulator.</5>
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</1> 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.</0>"
calculate the reduction: gross remuneration is compared with the
SMIC increased by this number of hours.</0>"
label:
heures-complémentaires: Number of overtime hours
heures-supplémentaires: Number of overtime hours

View File

@ -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.</5>
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 €</1> 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.</0>"
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.</0>"
label:
heures-complémentaires: Nombre d'heures complémentaires
heures-supplémentaires: Nombre d'heures supplémentaires

View File

@ -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),

View File

@ -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 (
<>
<SimulationGoal
dottedName={rémunérationBruteDottedName}
round={false}
label={t('Rémunération brute')}
onUpdateSituation={onUpdate}
/>
<Warnings />
<Condition expression="salarié . cotisations . exonérations . lodeom . montant = 0">
<Message type="info">
<Body>
<WarningSalaireTrans />
</Body>
</Message>
</Condition>
<Condition expression={`${lodeomDottedName} >= 0`}>
<SimulationValue
dottedName={lodeomDottedName}
isInfoMode={true}
round={false}
/>
<Spacing md />
<Répartition répartition={répartition} />
</Condition>
</>
)
}

View File

@ -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<MonthState[]>([])
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<Options> => {
const options = {} as Partial<Options>
const previousHeuresSupplémentaires =
previousSituation[heuresSupplémentairesDottedName]?.explanation.nodeValue
const newHeuresSupplémentaires =
newSituation[heuresSupplémentairesDottedName]?.explanation.nodeValue
if (newHeuresSupplémentaires !== previousHeuresSupplémentaires) {
options.heuresSupplémentaires = newHeuresSupplémentaires || 0
}
const previousHeuresComplémentaires =
previousSituation[heuresComplémentairesDottedName]?.valeur
const newHeuresComplémentaires =
newSituation[heuresComplémentairesDottedName]?.valeur
if (newHeuresComplémentaires !== previousHeuresComplémentaires) {
options.heuresComplémentaires = newHeuresComplémentaires || 0
}
return options
}
useEffect(() => {
setData((previousData) => {
if (!Object.keys(situation).length) {
return getInitialLodeomMoisParMois(year, engine)
}
const newOptions = getOptionsFromSituations(
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<DottedName, SimpleRuleEvaluation>)
)
}
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 (
<SimulationGoals toggles={toggles} legend={legend}>
{monthByMonth ? (
<LodeomMoisParMois
data={lodeomMoisParMoisData}
onRémunérationChange={onRémunérationChange}
onOptionsChange={onOptionsChange}
/>
) : (
<LodeomBasique onUpdate={initializeLodeomMoisParMoisData} />
)}
</SimulationGoals>
)
}

View File

@ -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<RégularisationMethod>('progressive')
return (
<>
<Simulation afterQuestionsSlot={<SelectSimulationYear />}>
<SimulateurWarning simulateur="lodeom" />
<LodeomSimulationGoals
monthByMonth={monthByMonth}
legend={t(
'pages.simulateurs.lodeom.legend',
'Rémunération brute du salarié et exonération Lodeom applicable'
)}
toggles={
<>
<RégularisationSwitch
régularisationMethod={régularisationMethod}
setRégularisationMethod={setRégularisationMethod}
/>
<PeriodSwitch periods={periods} onSwitch={onPeriodSwitch} />
</>
}
régularisationMethod={régularisationMethod}
/>
</Simulation>
</>
)
}

View File

@ -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 ? (
<>
<H3 as="h2">
{t(
'pages.simulateurs.lodeom.month-by-month.caption',
'Exonération Lodeom mois par mois :'
)}
</H3>
<StyledTable>
<caption className="sr-only">
{t(
'pages.simulateurs.lodeom.month-by-month.caption',
'Exonération Lodeom mois par mois :'
)}
</caption>
<thead>
<tr>
<th scope="col">{t('Mois')}</th>
<th scope="col">
{/* TODO: remplacer par rémunérationBruteDottedName lorsque ... */}
<RuleLink dottedName="salarié . rémunération . brut" />
</th>
<th scope="col">
<RuleLink dottedName={lodeomDottedName} />
</th>
<th scope="col">
<RuleLink dottedName="salarié . cotisations . exonérations . réduction générale . régularisation" />
</th>
</tr>
</thead>
<tbody>
{data.length > 0 &&
months.map((monthName, monthIndex) => (
<LodeomMois
key={`month-${monthIndex}`}
monthName={monthName}
data={data[monthIndex]}
index={monthIndex}
onRémunérationChange={(
monthIndex: number,
rémunérationBrute: number
) => {
onRémunérationChange(monthIndex, rémunérationBrute)
}}
onOptionsChange={(monthIndex: number, options: Options) => {
onOptionsChange(monthIndex, options)
}}
/>
))}
</tbody>
</StyledTable>
<Spacing md />
<H3 as="h2">
{t(
'pages.simulateurs.lodeom.recap.caption',
'Récapitulatif trimestriel :'
)}
</H3>
<StyledRecapTable>
<caption className="sr-only">
{t(
'pages.simulateurs.lodeom.recap.caption',
'Récapitulatif trimestriel :'
)}
</caption>
<thead>
<tr>
<th scope="col">{t('Trimestre')}</th>
<th scope="col">
{t(
'pages.simulateurs.lodeom.recap.header.réduction',
'Réduction calculée'
)}
{/* <br />
{t(
'pages.simulateurs.lodeom.recap.code671',
'code 671(€)'
)} */}
</th>
<th scope="col">
{t(
'pages.simulateurs.lodeom.recap.header.régularisation',
'Régularisation calculée'
)}
{/* <br />
{t(
'pages.simulateurs.lodeom.recap.code801',
'code 801(€)'
)} */}
</th>
</tr>
</thead>
<tbody>
{Object.keys(quarters).map((label, index) => (
<RécapitulatifTrimestre
key={index}
label={label}
data={quarters[label]}
/>
))}
</tbody>
</StyledRecapTable>
</>
) : (
<>
<H3 as="h2">
{t(
'pages.simulateurs.lodeom.month-by-month.caption',
'Exonération Lodeom mois par mois :'
)}
</H3>
{data.length > 0 &&
months.map((monthName, monthIndex) => (
<LodeomMois
key={`month-${monthIndex}`}
monthName={monthName}
data={data[monthIndex]}
index={monthIndex}
onRémunérationChange={(
monthIndex: number,
rémunérationBrute: number
) => {
onRémunérationChange(monthIndex, rémunérationBrute)
}}
onOptionsChange={(monthIndex: number, options: Options) => {
onOptionsChange(monthIndex, options)
}}
mobileVersion={true}
/>
))}
<Spacing xxl />
<H3 as="h2">
{t(
'pages.simulateurs.lodeom.recap.caption',
'Récapitulatif trimestriel :'
)}
</H3>
{Object.keys(quarters).map((label, index) => (
<RécapitulatifTrimestre
key={index}
label={label}
data={quarters[label]}
mobileVersion={true}
/>
))}
</>
)}
<span id="options-description" className="sr-only">
{t(
'pages.simulateurs.lodeom.options.description',
"Ajoute des champs pour moduler l'activité du salarié"
)}
</span>
<Warnings />
</>
)
}
const StyledTable = styled.table`
border-collapse: collapse;
text-align: left;
width: 100%;
color: ${({ theme }) => theme.colors.bases.primary[100]};
font-family: ${({ theme }) => theme.fonts.main};
caption {
text-align: left;
margin: ${({ theme }) => `${theme.spacings.sm} 0 `};
}
th,
td {
padding: ${({ theme }) => theme.spacings.xs};
}
tbody tr th {
text-transform: capitalize;
font-weight: normal;
}
`
const StyledRecapTable = styled(StyledTable)`
thead {
border-bottom: solid 1px;
}
thead th:not(:last-of-type),
tbody th,
td:not(:last-of-type) {
border-right: solid 1px;
}
thead th:not(:first-of-type) {
text-align: center;
}
`

View File

@ -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 (
<NumberInput
{...ruleInputProps}
id={`${rémunérationBruteDottedName.replace(
/\s|\./g,
'_'
)}-${monthName}`}
aria-label={`${engine.getRule(rémunérationBruteDottedName)
?.title} (${monthName})`}
onChange={(rémunérationBrute?: PublicodesExpression) =>
onRémunérationChange(
index,
(rémunérationBrute as RémunérationBruteInput).valeur
)
}
value={data.rémunérationBrute}
formatOptions={{
maximumFractionDigits: 2,
}}
displaySuggestions={false}
/>
)
}
const OptionsButton = () => {
return (
<Button
size="XXS"
light
onPress={() => setOptionVisible(!isOptionVisible)}
aria-describedby="options-description"
aria-expanded={isOptionVisible}
aria-controls={`options-${monthName}`}
aria-label={!isOptionVisible ? t('Déplier') : t('Replier')}
>
{t('Options')}&nbsp;
<RotatingChevronIcon aria-hidden $isOpen={isOptionVisible} />
</Button>
)
}
const MontantLodeom = () => {
return (
<MontantRéduction
id={`${lodeomDottedName.replace(/\s|\./g, '_')}-${monthName}`}
rémunérationBrute={data.rémunérationBrute}
lodeom={data.lodeom.value}
répartition={data.lodeom.répartition}
displayedUnit={displayedUnit}
language={language}
/>
)
}
const MontantRégularisation = () => {
return (
<MontantRéduction
id={`${lodeomDottedName.replace(
/\s|\./g,
'_'
)}__régularisation-${monthName}`}
rémunérationBrute={data.rémunérationBrute}
lodeom={data.régularisation.value}
répartition={data.régularisation.répartition}
displayedUnit={displayedUnit}
language={language}
displayNull={false}
/>
)
}
return mobileVersion ? (
<div>
<StyledMonth>{monthName}</StyledMonth>
<GridContainer container spacing={2}>
<Grid item xs={12} sm={4}>
<RuleLink dottedName="salarié . rémunération . brut" />
</Grid>
<Grid item xs={7} sm={5}>
<RémunérationInput />
</Grid>
<Grid item xs={5} sm={3}>
<OptionsButton />
</Grid>
</GridContainer>
{isOptionVisible && (
<OptionsContainer>
<MonthOptions
month={monthName}
index={index}
options={data.options}
onOptionsChange={onOptionsChange}
/>
</OptionsContainer>
)}
<Spacing xs />
<GridContainer container spacing={2}>
<Grid item>
<RuleLink dottedName={lodeomDottedName} />
</Grid>
<Grid item>
<StyledBody>
<MontantLodeom />
</StyledBody>
</Grid>
</GridContainer>
<GridContainer container spacing={2}>
<Grid item>
<RuleLink dottedName="salarié . cotisations . exonérations . réduction générale . régularisation" />
</Grid>
<Grid item>
<StyledBody>
<MontantRégularisation />
</StyledBody>
</Grid>
</GridContainer>
</div>
) : (
<>
<tr>
<th scope="row">{monthName}</th>
<td>
<InputContainer>
<RémunérationInput />
<OptionsButton />
</InputContainer>
</td>
<td>
<MontantLodeom />
</td>
<td>
<MontantRégularisation />
</td>
</tr>
{isOptionVisible && (
<StyledTableRow>
<td />
<td colSpan={3}>
<MonthOptions
month={monthName}
index={index}
options={data.options}
onOptionsChange={onOptionsChange}
/>
</td>
</StyledTableRow>
)}
</>
)
}
const StyledMonth = styled(Body)`
font-weight: bold;
text-transform: capitalize;
border-bottom: solid 1px ${({ theme }) => theme.colors.bases.primary[100]};
`
const GridContainer = styled(Grid)`
align-items: baseline;
justify-content: space-between;
`
const StyledBody = styled(Body)`
margin-top: 0;
`
const OptionsContainer = styled.div`
margin-top: ${({ theme }) => theme.spacings.xs};
background-color: ${({ theme }) => theme.colors.bases.primary[200]};
padding: ${({ theme }) => theme.spacings.sm};
`
const StyledTableRow = styled.tr`
background-color: ${({ theme }) => theme.colors.bases.primary[200]};
td {
padding-top: ${({ theme }) => theme.spacings.sm};
padding-bottom: ${({ theme }) => theme.spacings.sm};
}
`
const InputContainer = styled.div`
${FlexCenter}
gap: ${({ theme }) => theme.spacings.md};
`

View File

@ -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 = <Répartition répartition={répartition} />
return lodeom ? (
<StyledTooltip tooltip={tooltip}>
<FlexDiv id={id} $alignment={alignment}>
{formatValue(
{
nodeValue: lodeom,
},
{
displayedUnit,
language,
}
)}
<StyledSearchIcon />
</FlexDiv>
</StyledTooltip>
) : (
displayNull && (
<FlexDiv id={id} $alignment={alignment}>
{formatValue(0, { displayedUnit, language })}
<Condition
expression="salarié . cotisations . exonérations . lodeom . montant = 0"
contexte={{
[rémunérationBruteDottedName]: rémunérationBrute,
}}
>
<Tooltip tooltip={<WarningSalaireTrans />}>
<span className="sr-only">{t('Attention')}</span>
<StyledWarningIcon aria-label={t('Attention')} />
</Tooltip>
</Condition>
</FlexDiv>
)
)
}
const StyledTooltip = styled(Tooltip)`
width: 100%;
`
const FlexDiv = styled.div<{ $alignment: 'end' | 'center' }>`
${FlexCenter}
justify-content: ${({ $alignment }) => $alignment};
`
const StyledSearchIcon = styled(SearchIcon)`
margin-left: ${({ theme }) => theme.spacings.sm};
`
const StyledWarningIcon = styled(WarningIcon)`
margin-top: ${({ theme }) => theme.spacings.xxs};
margin-left: ${({ theme }) => theme.spacings.sm};
`

View File

@ -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 (
<MontantRéduction
id={`recap-${label.replace(/\s|\./g, '_')}-réduction`}
rémunérationBrute={rémunération}
lodeom={réduction}
répartition={répartition}
displayedUnit={displayedUnit}
language={language}
displayNull={false}
alignment="center"
/>
)
}
const MontantRégularisation = () => {
return (
<MontantRéduction
id={`recap-${label.replace(/\s|\./g, '_')}-régularisation`}
rémunérationBrute={rémunération}
lodeom={régularisation}
répartition={répartition}
displayedUnit={displayedUnit}
language={language}
displayNull={false}
alignment="center"
/>
)
}
return mobileVersion ? (
<div>
<StyledMonth>{label}</StyledMonth>
<GridContainer container spacing={2}>
<Grid item>
<StyledBody>
{t(
'pages.simulateurs.lodeom.recap.header.réduction',
'Réduction calculée'
)}
{/* <br />
{t(
'pages.simulateurs.lodeom.recap.code671',
'code 671(€)'
)} */}
</StyledBody>
</Grid>
<Grid item>
<StyledBody>
<MontantExonération />
</StyledBody>
</Grid>
</GridContainer>
<GridContainer container spacing={2}>
<Grid item>
<StyledBody>
{t(
'pages.simulateurs.lodeom.recap.header.régularisation',
'Régularisation calculée'
)}
{/* <br />
{t(
'pages.simulateurs.lodeom.recap.code801',
'code 801(€)'
)} */}
</StyledBody>
</Grid>
<Grid item>
<StyledBody>
<MontantRégularisation />
</StyledBody>
</Grid>
</GridContainer>
</div>
) : (
<tr>
<th scope="row">{label}</th>
<td>
<MontantExonération />
</td>
<td>
<MontantRégularisation />
</td>
</tr>
)
}
const StyledMonth = styled(Body)`
font-weight: bold;
text-transform: capitalize;
border-bottom: solid 1px ${({ theme }) => theme.colors.bases.primary[100]};
`
const GridContainer = styled(Grid)`
align-items: center;
justify-content: space-between;
`
const StyledBody = styled(Body)`
margin-top: 0;
`

View File

@ -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 (
<>
<Body>
<Strong>
<Trans>Détail du montant :</Trans>
</Strong>
</Body>
<StyledUl>
<StyledLi>
<RépartitionValue
value={répartition.IRC}
label={t('pages.simulateurs.lodeom.répartition.retraite', 'IRC')}
idPrefix={`${lodeomDottedName} . imputation retraite complémentaire`.replace(
/\s|\./g,
'_'
)}
/>
</StyledLi>
<StyledLi>
<RépartitionValue
value={répartition.Urssaf}
label={t('pages.simulateurs.lodeom.répartition.urssaf', 'URSSAF')}
idPrefix={`${lodeomDottedName} . imputation sécurité sociale`.replace(
/\s|\./g,
'_'
)}
/>
</StyledLi>
</StyledUl>
</>
)
}
const StyledUl = styled(Ul)`
margin-top: 0;
`
const StyledLi = styled(Li)`
&::before {
margin-top: ${({ theme }) => theme.spacings.sm};
}
`

View File

@ -0,0 +1,11 @@
import { Trans } from 'react-i18next'
export default function WarningSalaireTrans() {
return (
<Trans i18nKey="pages.simulateurs.lodeom.warnings.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 <strong>3&nbsp;964&nbsp;</strong> bruts par mois.
</Trans>
)
}

View File

@ -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 (
<>
<Condition expression="salarié . cotisations . exonérations . JEI = oui">
<Message type="info">
<Body>
<Trans i18nKey="pages.simulateurs.lodeom.warnings.JEI">
L'exonération Lodeom n'est pas cumulable avec l'exonération Jeune
Entreprise Innovante (JEI).
</Trans>
</Body>
</Message>
</Condition>
<Condition expression="salarié . contrat = 'stage'">
<Message type="info">
<Body>
<Trans i18nKey="pages.simulateurs.lodeom.warnings.stage">
L'exonération Lodeom ne s'applique pas sur les gratifications de
stage.
</Trans>
</Body>
</Message>
</Condition>
</>
)
}

View File

@ -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)
}

View File

@ -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'",
},
}

View File

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

View File

@ -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'

View File

@ -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

View File

@ -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

View File

@ -5,7 +5,7 @@ export default function WarningSalaireTrans() {
<Trans i18nKey="pages.simulateurs.réduction-générale.warnings.salaire">
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{' '}
<strong>2 827,07 </strong> bruts par mois.
<strong>2&nbsp;827,07&nbsp;</strong> bruts par mois.
</Trans>
)
}

View File

@ -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'

View File

@ -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',