style: version mobile du simulateur rgcp
parent
424e26a5a5
commit
c42da3ac18
|
@ -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', '24 000 €')
|
||||
cy.get(inputSelector).first().should('have.value', '22 800 €')
|
||||
})
|
||||
|
||||
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', '1 900 €')
|
||||
})
|
||||
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 €')
|
||||
})
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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')}
|
||||
<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};
|
||||
`
|
|
@ -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 && (
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
`
|
|
@ -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')}
|
||||
<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]};
|
||||
`
|
Loading…
Reference in New Issue