feat: ajout de champs heures sup sur le simulateur RGCP

pull/3237/head
Alice Dahan 2024-11-28 12:16:56 +01:00 committed by liliced
parent 3faa153250
commit 9e307ebd36
7 changed files with 273 additions and 99 deletions

View File

@ -142,7 +142,7 @@ export default function NumberField(props: NumberFieldProps) {
</StyledUnit>
)}
{props.label && (
{props.label && !props.small && (
<StyledLabel {...labelProps}>{props.label}</StyledLabel>
)}
</StyledInputContainer>

View File

@ -22,6 +22,11 @@ export const FocusStyle = css`
box-shadow: 0 0 0 2px #ffffff;
`
export const FlexCenter = css`
display: flex;
align-items: center;
`
export const GlobalStyle = createGlobalStyle`
html {
transition: none !important;

View File

@ -89,6 +89,7 @@ Décrivez votre projet ou votre problème en donnant quelques éléments de cont
the right advisor for your request.
He or she will contact you by telephone within 5 days, and will provide you with assistance tailored to your situation.
Décès: Deaths
Déplier: Unfold
"Détail du montant :": "Amount in detail :"
Effacer mes réponses: Delete my answers
Effectif de l'entreprise: Number of employees
@ -208,6 +209,7 @@ Nouveau contenu disponible, cliquez sur recharger pour mettre à jour la page.:
Nouveautés: News
Option ACRE non activée: ACRE option not activated
Option la plus avantageuse.: Most advantageous option.
Options: Options
Oui: Yes
Outils pour les développeurs: Tools for developers
PFU (<1>"flat tax"</1>): PFU (<1>"flat tax"</1>)
@ -1487,6 +1489,16 @@ pages:
title: General reduction
month-by-month:
caption: "General discount month by month :"
options:
description: Adds fields to modulate employee activity
label:
heures-complémentaires: Overtime
heures-supplémentaires: Overtime
popover: "<0>The number of hours of overtime or complementary work is used to
calculate the general reduction: gross remuneration is compared with
the SMIC increased by this number of hours.</0><1>If you have answered
the question on overtime or complementary hours, the value will be
overwritten by the value you enter month by month.</1>"
régularisation:
annuelle: Annual adjustment
progressive: Progressive regularization

View File

@ -95,6 +95,7 @@ Décrivez votre projet ou votre problème en donnant quelques éléments de cont
le conseiller compétent pour votre demande.
Celui-ci vous contactera par téléphone sous 5 jours et vous accompagnera en fonction de votre situation.
Décès: Décès
Déplier: Déplier
"Détail du montant :": "Détail du montant :"
Effacer mes réponses: Effacer mes réponses
Effectif de l'entreprise: Effectif de l'entreprise
@ -220,6 +221,7 @@ Nouveau contenu disponible, cliquez sur recharger pour mettre à jour la page.:
Nouveautés: Nouveautés
Option ACRE non activée: Option ACRE non activée
Option la plus avantageuse.: Option la plus avantageuse.
Options: Options
Oui: Oui
Outils pour les développeurs: Outils pour les développeurs
PFU (<1>"flat tax"</1>): PFU (<1>"flat tax"</1>)
@ -1581,6 +1583,17 @@ pages:
title: Réduction générale
month-by-month:
caption: "Réduction générale mois par mois :"
options:
description: Ajoute des champs pour moduler l'activité du salarié
label:
heures-complémentaires: Heures complémentaires
heures-supplémentaires: Heures supplémentaires
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><1>Si
vous avez répondu à la question sur les heures supplémentaires ou
complémentaires, la valeur sera écrasée par celle que vous saisissez
mois par mois.</1>"
régularisation:
annuelle: Régularisation annuelle
progressive: Régularisation progressive

View File

@ -31,6 +31,7 @@ import RéductionGénéraleMoisParMois from './RéductionGénéraleMoisParMois'
import {
getInitialRéductionGénéraleMoisParMois,
MonthState,
Options,
réductionGénéraleDottedName,
reevaluateRéductionGénéraleMoisParMois,
RégularisationMethod,
@ -125,7 +126,7 @@ function RéductionGénéraleSimulationGoals({
}
}, [
initializeRéductionGénéraleMoisParMoisData,
réductionGénéraleMoisParMoisData,
réductionGénéraleMoisParMoisData.length,
])
const situation = useSelector(situationSelector)
@ -178,12 +179,30 @@ function RéductionGénéraleSimulationGoals({
})
}
const onOptionsChange = (monthIndex: number, options: Options) => {
setData((previousData) => {
const updatedData = [...previousData]
updatedData[monthIndex] = {
...updatedData[monthIndex],
options,
}
return reevaluateRéductionGénéraleMoisParMois(
updatedData,
engine,
year,
régularisationMethod
)
})
}
return (
<SimulationGoals toggles={toggles} legend={legend}>
{monthByMonth ? (
<RéductionGénéraleMoisParMois
data={réductionGénéraleMoisParMoisData}
onChange={onRémunérationChange}
onRémunérationChange={onRémunérationChange}
onOptionsChange={onOptionsChange}
/>
) : (
<>

View File

@ -5,16 +5,18 @@ import { ExplicableRule } from '@/components/conversation/Explicable'
import RéductionGénéraleMoisParMoisRow from './components/RéductionGénéraleMoisParMoisRow'
import Warnings from './components/Warnings'
import { MonthState, réductionGénéraleDottedName } from './utils'
import { MonthState, Options, réductionGénéraleDottedName } from './utils'
type Props = {
data: MonthState[]
onChange: (monthIndex: number, rémunérationBrute: number) => void
onRémunérationChange: (monthIndex: number, rémunérationBrute: number) => void
onOptionsChange: (monthIndex: number, options: Options) => void
}
export default function RéductionGénéraleMoisParMois({
data,
onChange,
onRémunérationChange,
onOptionsChange,
}: Props) {
const { t } = useTranslation()
@ -49,6 +51,7 @@ export default function RéductionGénéraleMoisParMois({
{t('Rémunération brute')}
<ExplicableRule dottedName="salarié . rémunération . brut" />
</th>
<th />
<th scope="col">
{t('Réduction générale')}
<ExplicableRule dottedName={réductionGénéraleDottedName} light />
@ -64,14 +67,27 @@ export default function RéductionGénéraleMoisParMois({
monthName={monthName}
data={data[monthIndex]}
index={monthIndex}
onChange={(monthIndex: number, rémunérationBrute: number) => {
onChange(monthIndex, rémunérationBrute)
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>
<span id="options-description" className="sr-only">
{t(
'pages.simulateurs.réduction-générale.options.description',
"Ajoute des champs pour moduler l'activité du salarié"
)}
</span>
<Warnings />
</>
)

View File

@ -1,15 +1,22 @@
import { formatValue, PublicodesExpression } from 'publicodes'
import { useTranslation } from 'react-i18next'
import { styled } from 'styled-components'
import { useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { css, styled } from 'styled-components'
import NumberInput from '@/components/conversation/NumberInput'
import { Condition } from '@/components/EngineValue/Condition'
import { Appear } from '@/components/ui/animate'
import { useEngine } from '@/components/utils/EngineContext'
import { SearchIcon, WarningIcon } from '@/design-system/icons'
import { Message, NumberField } from '@/design-system'
import { Button, HelpButtonWithPopover } from '@/design-system/buttons'
import { FlexCenter } from '@/design-system/global-style'
import { ChevronIcon, SearchIcon, WarningIcon } from '@/design-system/icons'
import { Tooltip } from '@/design-system/tooltip'
import { Body, SmallBody } from '@/design-system/typography/paragraphs'
import {
MonthState,
Options,
réductionGénéraleDottedName,
rémunérationBruteDottedName,
} from '../utils'
@ -20,7 +27,8 @@ type Props = {
monthName: string
data: MonthState
index: number
onChange: (monthIndex: number, rémunérationBrute: number) => void
onRémunérationChange: (monthIndex: number, rémunérationBrute: number) => void
onOptionsChange: (monthIndex: number, options: Options) => void
}
type RémunérationBruteInput = {
@ -32,19 +40,14 @@ export default function RéductionGénéraleMoisParMoisRow({
monthName,
data,
index,
onChange,
onRémunérationChange,
onOptionsChange,
}: Props) {
const { t, i18n } = useTranslation()
const language = i18n.language
const displayedUnit = '€'
const engine = useEngine()
const onRémunérationChange = (
monthIndex: number,
rémunérationBrute: RémunérationBruteInput
) => {
onChange(monthIndex, rémunérationBrute.valeur)
}
const [isOptionVisible, setOptionVisible] = useState(false)
// TODO: enlever les 4 premières props après résolution de #3123
const ruleInputProps = {
@ -64,6 +67,21 @@ export default function RéductionGénéraleMoisParMoisRow({
},
}
const isTempsPartiel = engine.evaluate(
'salarié . contrat . temps de travail . temps partiel'
).nodeValue as boolean
const additionalHours = isTempsPartiel ? 'complémentaires' : 'supplémentaires'
const additionalHoursLabels = {
supplémentaires: t(
'pages.simulateurs.réduction-générale.options.label.heures-supplémentaires',
'Heures supplémentaires'
),
complémentaires: t(
'pages.simulateurs.réduction-générale.options.label.heures-complémentaires',
'Heures complémentaires'
),
}
const tooltip = (
<Répartition
contexte={{
@ -75,94 +93,185 @@ export default function RéductionGénéraleMoisParMoisRow({
)
return (
<tr>
<th scope="row">{monthName}</th>
<td>
<NumberInput
{...ruleInputProps}
id={`${rémunérationBruteDottedName.replace(
<>
<tr>
<th scope="row">{monthName}</th>
<td>
<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,
}}
/>
</td>
<td>
<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;
<StyledChevron aria-hidden $isOpen={isOptionVisible} />
</Button>
</td>
<td
id={`${réductionGénéraleDottedName.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
)
}
value={data.rémunérationBrute}
formatOptions={{
maximumFractionDigits: 2,
}}
/>
</td>
<td
id={`${réductionGénéraleDottedName.replace(
/\s|\./g,
'_'
)}-${monthName}`}
>
{data.réductionGénérale ? (
<Tooltip tooltip={tooltip}>
<StyledDiv>
{formatValue(
{
nodeValue: data.réductionGénérale,
},
{
displayedUnit,
language,
}
)}
<SearchIcon />
</StyledDiv>
</Tooltip>
) : (
<StyledDiv>
{formatValue(0, { displayedUnit, language })}
>
{data.réductionGénérale ? (
<Tooltip tooltip={tooltip}>
<FlexDiv>
{formatValue(
{
nodeValue: data.réductionGénérale,
},
{
displayedUnit,
language,
}
)}
<StyledSearchIcon />
</FlexDiv>
</Tooltip>
) : (
<FlexDiv>
{formatValue(0, { displayedUnit, language })}
<Condition
expression={`${rémunérationBruteDottedName} > 1.6 * SMIC`}
contexte={{
[rémunérationBruteDottedName]: data.rémunérationBrute,
}}
>
<Tooltip tooltip={<WarningSalaireTrans />}>
<span className="sr-only">{t('Attention')}</span>
<StyledWarningIcon aria-label={t('Attention')} />
</Tooltip>
</Condition>
</StyledDiv>
)}
</td>
<td
id={`${réductionGénéraleDottedName.replace(
/\s|\./g,
'_'
)}__régularisation-${monthName}`}
>
{formatValue(
{
nodeValue: data.régularisation,
},
{
displayedUnit,
language,
}
)}
</td>
</tr>
<Condition
expression={`${rémunérationBruteDottedName} > 1.6 * SMIC`}
contexte={{
[rémunérationBruteDottedName]: data.rémunérationBrute,
}}
>
<Tooltip tooltip={<WarningSalaireTrans />}>
<span className="sr-only">{t('Attention')}</span>
<StyledWarningIcon aria-label={t('Attention')} />
</Tooltip>
</Condition>
</FlexDiv>
)}
</td>
<td
id={`${réductionGénéraleDottedName.replace(
/\s|\./g,
'_'
)}__régularisation-${monthName}`}
>
{formatValue(
{
nodeValue: data.régularisation,
},
{
displayedUnit,
language,
}
)}
</td>
</tr>
{isOptionVisible && (
<tr>
<td />
<td colSpan={4}>
<Appear id={`options-${monthName}`}>
<OptionContainer>
<FlexDiv>
<StyledSmallBody id={`heures-${additionalHours}-label`}>
{additionalHoursLabels[additionalHours]}
</StyledSmallBody>
<HelpButtonWithPopover
type="info"
title={additionalHoursLabels[additionalHours]}
>
<HeuresSupplémentairesPopoverContent />
</HelpButtonWithPopover>
</FlexDiv>
<NumberField
small={true}
value={data.options.heuresSupplémentaires}
onChange={(value?: number) =>
onOptionsChange(index, {
heuresSupplémentaires: value,
heuresComplémentaires: 0,
})
}
aria-labelledby={`heures-${additionalHours}-label`}
displayedUnit="heures"
/>
</OptionContainer>
</Appear>
</td>
</tr>
)}
</>
)
}
const StyledDiv = styled.div`
display: flex;
align-items: center;
gap: ${({ theme }) => theme.spacings.sm};
function HeuresSupplémentairesPopoverContent() {
return (
<Trans i18nKey="pages.simulateurs.réduction-générale.options.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.
</Body>
<Message type="info">
Si vous avez répondu à la question sur les heures supplémentaires ou
complémentaires, la valeur sera écrasée par celle que vous saisissez
mois par mois.
</Message>
</Trans>
)
}
const FlexDiv = styled.div`
${FlexCenter}
`
const StyledSearchIcon = styled(SearchIcon)`
margin-left: ${({ theme }) => theme.spacings.sm};
`
const StyledWarningIcon = styled(WarningIcon)`
margin-top: ${({ theme }) => theme.spacings.xxs};
`
const OptionContainer = styled.div`
${FlexCenter}
gap: ${({ theme }) => theme.spacings.lg};
margin-top: -${({ theme }) => theme.spacings.md};
`
const StyledSmallBody = styled(SmallBody)`
margin: 0;
`
const StyledChevron = styled(ChevronIcon)<{ $isOpen: boolean }>`
vertical-align: middle;
transform: rotate(-90deg);
transition: transform 0.3s;
${({ $isOpen }) =>
!$isOpen &&
css`
transform: rotate(90deg);
`}
`