style: version mobile du simulateur rgcp

pull/3239/head
Alice Dahan 2024-12-02 11:03:06 +01:00 committed by liliced
parent 424e26a5a5
commit c42da3ac18
7 changed files with 562 additions and 288 deletions

View File

@ -20,8 +20,6 @@ describe(
})
it('should allow to select a company size', function () {
cy.get(inputSelector).first().type('{selectall}2000')
cy.contains('Plus de 50 salariés').click()
cy.contains('Modifier mes réponses').click()
cy.get('div[data-cy="modal"]')
@ -43,15 +41,14 @@ describe(
it('should allow to change time period', function () {
cy.contains('Réduction mensuelle').click()
cy.get(inputSelector).first().type('{selectall}2000')
cy.get(inputSelector).first().type('{selectall}1900')
cy.contains('Réduction annuelle').click()
cy.get(inputSelector).first().should('have.value', '24000 €')
cy.get(inputSelector).first().should('have.value', '22800 €')
})
it('should display values for the réduction générale', function () {
cy.contains('Réduction mensuelle').click()
cy.get(inputSelector).first().type('{selectall}1900')
cy.get(
'p[id="salarié___cotisations___exonérations___réduction_générale-value"]'
@ -93,18 +90,21 @@ describe(
cy.contains('Réduction mois par mois').click()
cy.contains('Réduction générale mois par mois :')
// Wait for 1 ms in order for values to update
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(1)
cy.get(inputSelector)
.should('have.length', 12)
.each(($input) => {
cy.wrap($input).should('have.value', '1900 €')
})
cy.get(
'td[id^="salarié___cotisations___exonérations___réduction_générale-"]'
'[id^="salarié___cotisations___exonérations___réduction_générale-"]'
)
.should('have.length', 12)
.each(($td, $index) => {
.each(($input, $index) => {
if ($index < 10) {
cy.wrap($td).should('include.text', '493,43 €')
cy.wrap($input).should('include.text', '493,43 €')
}
})
})
@ -116,10 +116,10 @@ describe(
cy.get(inputSelector).first().type('{selectall}1900')
cy.get(inputSelector).eq(1).type('{selectall}2000')
cy.get(
'td[id="salarié___cotisations___exonérations___réduction_générale-janvier"]'
'#salarié___cotisations___exonérations___réduction_générale-janvier'
).should('include.text', '493,43 €')
cy.get(
'td[id="salarié___cotisations___exonérations___réduction_générale-février"]'
'#salarié___cotisations___exonérations___réduction_générale-février'
).should('include.text', '440,23 €')
})
@ -154,16 +154,16 @@ describe(
cy.get(inputSelector).eq(1).type('{selectall}3000')
cy.get(
'td[id="salarié___cotisations___exonérations___réduction_générale-février"]'
'#salarié___cotisations___exonérations___réduction_générale-février'
).should('include.text', '0 €')
cy.get(
'td[id="salarié___cotisations___exonérations___réduction_générale__régularisation-février"]'
'#salarié___cotisations___exonérations___réduction_générale__régularisation-février'
).should('include.text', '0 €')
cy.get(
'td[id="salarié___cotisations___exonérations___réduction_générale-mars"]'
'#salarié___cotisations___exonérations___réduction_générale-mars'
).should('include.text', '493,43 €')
cy.get(
'td[id="salarié___cotisations___exonérations___réduction_générale-décembre"]'
'#salarié___cotisations___exonérations___réduction_générale-décembre'
).should('include.text', '432,49 €')
})
@ -176,16 +176,16 @@ describe(
cy.get(inputSelector).eq(1).type('{selectall}3000')
cy.get(
'td[id="salarié___cotisations___exonérations___réduction_générale-février"]'
'#salarié___cotisations___exonérations___réduction_générale-février'
).should('include.text', '0 €')
cy.get(
'td[id="salarié___cotisations___exonérations___réduction_générale__régularisation-février"]'
'#salarié___cotisations___exonérations___réduction_générale__régularisation-février'
).should('include.text', '-92,12 €')
cy.get(
'td[id="salarié___cotisations___exonérations___réduction_générale-mars"]'
'#salarié___cotisations___exonérations___réduction_générale-mars'
).should('include.text', '493,57 €')
cy.get(
'td[id="salarié___cotisations___exonérations___réduction_générale-décembre"]'
'#salarié___cotisations___exonérations___réduction_générale-décembre'
).should('include.text', '523,62 €')
})

View File

@ -0,0 +1,94 @@
import { useEffect, useLayoutEffect, useState } from 'react'
// https://github.com/juliencrn/usehooks-ts/blob/master/packages/usehooks-ts/src/useIsomorphicLayoutEffect/useIsomorphicLayoutEffect.ts
/**
* Custom hook that uses either `useLayoutEffect` or `useEffect` based on the environment (client-side or server-side).
* @param {Function} effect - The effect function to be executed.
* @param {Array<any>} [dependencies] - An array of dependencies for the effect (optional).
* @public
* @see [Documentation](https://usehooks-ts.com/react-hook/use-isomorphic-layout-effect)
* @example
* ```tsx
* useIsomorphicLayoutEffect(() => {
* // Code to be executed during the layout phase on the client side
* }, [dependency1, dependency2]);
* ```
*/
export const useIsomorphicLayoutEffect =
typeof window !== 'undefined' ? useLayoutEffect : useEffect
// https://github.com/juliencrn/usehooks-ts/blob/master/packages/usehooks-ts/src/useMediaQuery/useMediaQuery.ts
/** Hook options. */
type UseMediaQueryOptions = {
/**
* The default value to return if the hook is being run on the server.
* @default false
*/
defaultValue?: boolean
/**
* If `true` (default), the hook will initialize reading the media query. In SSR, you should set it to `false`, returning `options.defaultValue` or `false` initially.
* @default true
*/
initializeWithValue?: boolean
}
const IS_SERVER = typeof window === 'undefined'
/**
* Custom hook that tracks the state of a media query using the [`Match Media API`](https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia).
* @param {string} query - The media query to track.
* @param {?UseMediaQueryOptions} [options] - The options for customizing the behavior of the hook (optional).
* @returns {boolean} The current state of the media query (true if the query matches, false otherwise).
* @public
* @see [Documentation](https://usehooks-ts.com/react-hook/use-media-query)
* @example
* ```tsx
* const isSmallScreen = useMediaQuery('(max-width: 600px)');
* // Use `isSmallScreen` to conditionally apply styles or logic based on the screen size.
* ```
*/
export function useMediaQuery(
query: string,
{
defaultValue = false,
initializeWithValue = true,
}: UseMediaQueryOptions = {}
): boolean {
const getMatches = (query: string): boolean => {
if (IS_SERVER) {
return defaultValue
}
return window.matchMedia(query).matches
}
const [matches, setMatches] = useState<boolean>(() => {
if (initializeWithValue) {
return getMatches(query)
}
return defaultValue
})
// Handles the change event of the media query.
function handleChange() {
setMatches(getMatches(query))
}
useIsomorphicLayoutEffect(() => {
const matchMedia = window.matchMedia(query)
// Triggered at the first client-side load and if query changes
handleChange()
matchMedia.addEventListener('change', handleChange)
return () => {
matchMedia.removeEventListener('change', handleChange)
}
}, [query])
return matches
}

View File

@ -2,8 +2,11 @@ import { useTranslation } from 'react-i18next'
import { styled } from 'styled-components'
import RuleLink from '@/components/RuleLink'
import { baseTheme } from '@/design-system/theme'
import { Body } from '@/design-system/typography/paragraphs'
import { useMediaQuery } from '@/hooks/useMediaQuery'
import RéductionGénéraleMoisParMoisRow from './components/RéductionGénéraleMoisParMoisRow'
import RéductionGénéraleMois from './components/MoisParMois'
import Warnings from './components/Warnings'
import { MonthState, Options, réductionGénéraleDottedName } from './utils'
@ -19,6 +22,9 @@ export default function RéductionGénéraleMoisParMois({
onOptionsChange,
}: Props) {
const { t } = useTranslation()
const isDesktop = useMediaQuery(
`(min-width: ${baseTheme.breakpointsWidth.md})`
)
const months = [
t('janvier'),
@ -37,31 +43,61 @@ export default function RéductionGénéraleMoisParMois({
return (
<>
<StyledTable style={{ width: '100%' }}>
<caption>
{t(
'pages.simulateurs.réduction-générale.month-by-month.caption',
'Réduction générale mois par mois :'
)}
</caption>
<thead>
<tr>
<th scope="col">{t('Mois')}</th>
<th scope="col">
<RuleLink dottedName="salarié . rémunération . brut" />
</th>
<th scope="col">
<RuleLink dottedName={réductionGénéraleDottedName} />
</th>
<th scope="col">
<RuleLink dottedName="salarié . cotisations . exonérations . réduction générale . régularisation" />
</th>
</tr>
</thead>
<tbody>
{isDesktop ? (
<StyledTable>
<caption>
{t(
'pages.simulateurs.réduction-générale.month-by-month.caption',
'Réduction générale 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={réductionGénéraleDottedName} />
</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) => (
<RéductionGénéraleMois
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>
) : (
<>
<Body>
{t(
'pages.simulateurs.réduction-générale.month-by-month.caption',
'Réduction générale mois par mois :'
)}
</Body>
{data.length > 0 &&
months.map((monthName, monthIndex) => (
<RéductionGénéraleMoisParMoisRow
<RéductionGénéraleMois
key={`month-${monthIndex}`}
monthName={monthName}
data={data[monthIndex]}
@ -75,10 +111,11 @@ export default function RéductionGénéraleMoisParMois({
onOptionsChange={(monthIndex: number, options: Options) => {
onOptionsChange(monthIndex, options)
}}
mobileVersion={true}
/>
))}
</tbody>
</StyledTable>
</>
)}
<span id="options-description" className="sr-only">
{t(

View File

@ -0,0 +1,247 @@
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 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 } from '@/design-system/layout'
import { Body } from '@/design-system/typography/paragraphs'
import {
MonthState,
Options,
réductionGénéraleDottedName,
rémunérationBruteDottedName,
} from '../utils'
import MontantRéduction from './MontantRéduction'
import MonthOptions from './MonthOptions'
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 RéductionGénéraleMois({
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 MontantRéductionGénérale = () => {
return (
<MontantRéduction
id={`${réductionGénéraleDottedName.replace(
/\s|\./g,
'_'
)}-${monthName}`}
rémunérationBrute={data.rémunérationBrute}
réductionGénérale={data.réductionGénérale}
displayedUnit={displayedUnit}
language={language}
warning={true}
/>
)
}
const MontantRégularisation = () => {
return (
<MontantRéduction
id={`${réductionGénéraleDottedName.replace(
/\s|\./g,
'_'
)}__régularisation-${monthName}`}
rémunérationBrute={data.rémunérationBrute}
réductionGénérale={data.régularisation}
displayedUnit={displayedUnit}
language={language}
warning={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 && (
<MonthOptions
month={monthName}
index={index}
options={data.options}
onOptionsChange={onOptionsChange}
/>
)}
<GridContainer container spacing={2}>
<Grid item>
<RuleLink dottedName={réductionGénéraleDottedName} />
</Grid>
<Grid item>
<StyledBody>
<MontantRéductionGénérale />
</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>
<MontantRéductionGénérale />
</td>
<td>
<MontantRégularisation />
</td>
</tr>
{isOptionVisible && (
<StyledTableRow>
<td />
<td colSpan={4}>
<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 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

@ -15,6 +15,7 @@ import Répartition from './Répartition'
import WarningSalaireTrans from './WarningSalaireTrans'
type Props = {
id?: string
rémunérationBrute: number
réductionGénérale: number
displayedUnit: string
@ -23,6 +24,7 @@ type Props = {
}
export default function MontantRéduction({
id,
rémunérationBrute,
réductionGénérale,
displayedUnit,
@ -42,7 +44,7 @@ export default function MontantRéduction({
return réductionGénérale ? (
<StyledTooltip tooltip={tooltip}>
<FlexDiv>
<FlexDiv id={id}>
{formatValue(
{
nodeValue: réductionGénérale,
@ -56,7 +58,7 @@ export default function MontantRéduction({
</FlexDiv>
</StyledTooltip>
) : (
<FlexDiv>
<FlexDiv id={id}>
{formatValue(0, { displayedUnit, language })}
{warning && (

View File

@ -0,0 +1,136 @@
import { Trans, useTranslation } from 'react-i18next'
import { styled } from 'styled-components'
import { Appear } from '@/components/ui/animate'
import { useEngine } from '@/components/utils/EngineContext'
import { Message, NumberField } from '@/design-system'
import { HelpButtonWithPopover } from '@/design-system/buttons'
import {
StyledInput,
StyledInputContainer,
StyledSuffix,
} from '@/design-system/field/TextField'
import { FlexCenter } from '@/design-system/global-style'
import { Body, SmallBody } from '@/design-system/typography/paragraphs'
import { Options } from '../utils'
type Props = {
month: string
index: number
options: Options
onOptionsChange: (monthIndex: number, options: Options) => void
}
export default function MonthOptions({
month,
index,
options,
onOptionsChange,
}: Props) {
const { t } = useTranslation()
const engine = useEngine()
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 onChange = (value?: number) => {
let options = {}
if (isTempsPartiel) {
options = {
heuresSupplémentaires: 0,
heuresComplémentaires: value,
}
} else {
options = {
heuresSupplémentaires: value,
heuresComplémentaires: 0,
}
}
onOptionsChange(index, options)
}
return (
<Appear id={`options-${month}`}>
<InputContainer>
<FlexDiv>
<StyledSmallBody id={`heures-${additionalHours}-label`}>
{additionalHoursLabels[additionalHours]}
</StyledSmallBody>
<HelpButtonWithPopover
type="info"
title={additionalHoursLabels[additionalHours]}
>
<HeuresSupplémentairesPopoverContent />
</HelpButtonWithPopover>
</FlexDiv>
<NumberFieldContainer>
<NumberField
small={true}
value={
isTempsPartiel
? options.heuresComplémentaires
: options.heuresSupplémentaires
}
onChange={onChange}
aria-labelledby={`heures-${additionalHours}-label`}
displayedUnit="heures"
/>
</NumberFieldContainer>
</InputContainer>
</Appear>
)
}
const HeuresSupplémentairesPopoverContent = () => (
<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 InputContainer = styled.div`
${FlexCenter}
gap: ${({ theme }) => theme.spacings.md};
`
const FlexDiv = styled.div`
${FlexCenter}
justify-content: end;
`
const StyledSmallBody = styled(SmallBody)`
margin: 0;
color: ${({ theme }) => theme.colors.bases.primary[800]};
`
const NumberFieldContainer = styled.div`
max-width: 120px;
${StyledInputContainer} {
border-color: ${({ theme }) => theme.colors.bases.primary[800]};
background-color: 'rgba(255, 255, 255, 10%)';
&:focus-within {
outline-color: ${({ theme }) => theme.colors.bases.primary[700]};
}
${StyledInput}, ${StyledSuffix} {
color: ${({ theme }) => theme.colors.bases.primary[800]}!important;
}
}
`

View File

@ -1,242 +0,0 @@
import { PublicodesExpression } from 'publicodes'
import { useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { styled } from 'styled-components'
import NumberInput from '@/components/conversation/NumberInput'
import { Appear } from '@/components/ui/animate'
import { useEngine } from '@/components/utils/EngineContext'
import { Message, NumberField } from '@/design-system'
import { Button, HelpButtonWithPopover } from '@/design-system/buttons'
import {
StyledInput,
StyledInputContainer,
StyledSuffix,
} from '@/design-system/field/TextField'
import { FlexCenter } from '@/design-system/global-style'
import { RotatingChevronIcon } from '@/design-system/icons'
import { Body, SmallBody } from '@/design-system/typography/paragraphs'
import {
MonthState,
Options,
réductionGénéraleDottedName,
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
}
type RémunérationBruteInput = {
unité: string
valeur: number
}
export default function RéductionGénéraleMoisParMoisRow({
monthName,
data,
index,
onRémunérationChange,
onOptionsChange,
}: Props) {
const { t, i18n } = useTranslation()
const language = i18n.language
const displayedUnit = '€'
const engine = useEngine()
const [isOptionVisible, setOptionVisible] = useState(false)
// 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: [],
},
}
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'
),
}
return (
<>
<tr>
<th scope="row">{monthName}</th>
<td>
<InputContainer>
<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}
/>
<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>
</InputContainer>
</td>
<td
id={`${réductionGénéraleDottedName.replace(
/\s|\./g,
'_'
)}-${monthName}`}
>
<MontantRéduction
rémunérationBrute={data.rémunérationBrute}
réductionGénérale={data.réductionGénérale}
displayedUnit={displayedUnit}
language={language}
warning={true}
/>
</td>
<td
id={`${réductionGénéraleDottedName.replace(
/\s|\./g,
'_'
)}__régularisation-${monthName}`}
>
<MontantRéduction
rémunérationBrute={data.rémunérationBrute}
réductionGénérale={data.régularisation}
displayedUnit={displayedUnit}
language={language}
/>
</td>
</tr>
{isOptionVisible && (
<StyledTableRow>
<td />
<td colSpan={4}>
<Appear id={`options-${monthName}`}>
<InputContainer>
<FlexDiv>
<StyledSmallBody id={`heures-${additionalHours}-label`}>
{additionalHoursLabels[additionalHours]}
</StyledSmallBody>
<HelpButtonWithPopover
type="info"
title={additionalHoursLabels[additionalHours]}
>
<HeuresSupplémentairesPopoverContent />
</HelpButtonWithPopover>
</FlexDiv>
<NumberFieldContainer>
<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"
/>
</NumberFieldContainer>
</InputContainer>
</Appear>
</td>
</StyledTableRow>
)}
</>
)
}
const HeuresSupplémentairesPopoverContent = () => (
<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 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 FlexDiv = styled.div`
${FlexCenter}
justify-content: end;
`
const InputContainer = styled.div`
${FlexCenter}
gap: ${({ theme }) => theme.spacings.md};
`
const NumberFieldContainer = styled.div`
max-width: 120px;
${StyledInputContainer} {
border-color: ${({ theme }) => theme.colors.bases.primary[800]};
background-color: 'rgba(255, 255, 255, 10%)';
&:focus-within {
outline-color: ${({ theme }) => theme.colors.bases.primary[700]};
}
${StyledInput}, ${StyledSuffix} {
color: ${({ theme }) => theme.colors.bases.primary[800]}!important;
}
}
`
const StyledSmallBody = styled(SmallBody)`
margin: 0;
color: ${({ theme }) => theme.colors.bases.primary[800]};
`