Merge pull request #706 from betagouv/graphique-repartition
Ajout d'un graphique repartitonpull/726/head
commit
b58c98d2df
|
@ -21,7 +21,6 @@
|
|||
"dependencies": {
|
||||
"@babel/polyfill": "^7.4.0",
|
||||
"@babel/runtime": "^7.3.4",
|
||||
"@researchgate/react-intersection-observer": "^0.7.3",
|
||||
"classnames": "^2.2.5",
|
||||
"color-convert": "^1.9.2",
|
||||
"core-js": "^3.2.1",
|
||||
|
@ -51,7 +50,7 @@
|
|||
"react-router-dom": "^5.1.1",
|
||||
"react-select": "^1.2.1",
|
||||
"react-select-fast-filter-options": "^0.2.3",
|
||||
"react-spring": "^5.8.0",
|
||||
"react-spring": "=8.0.27",
|
||||
"react-syntax-highlighter": "^10.1.1",
|
||||
"react-transition-group": "^2.2.1",
|
||||
"react-virtualized": "^9.20.0",
|
||||
|
|
|
@ -1,134 +1,98 @@
|
|||
/* @flow */
|
||||
|
||||
import Observer from '@researchgate/react-intersection-observer'
|
||||
import withColours from 'Components/utils/withColours'
|
||||
import { ThemeColoursContext } from 'Components/utils/withColours'
|
||||
import Value from 'Components/Value'
|
||||
import { findRuleByDottedName } from 'Engine/rules'
|
||||
import React, { useState } from 'react'
|
||||
import React, { useContext } from 'react'
|
||||
import emoji from 'react-easy-emoji'
|
||||
import { connect } from 'react-redux'
|
||||
import { config, Spring } from 'react-spring'
|
||||
import { compose } from 'redux'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { config, animated, useSpring } from 'react-spring'
|
||||
import { flatRulesSelector } from 'Selectors/analyseSelectors'
|
||||
import répartitionSelector from 'Selectors/repartitionSelectors'
|
||||
import { isIE } from '../utils'
|
||||
import './Distribution.css'
|
||||
import './PaySlip'
|
||||
import RuleLink from './RuleLink'
|
||||
import type { ThemeColours } from 'Components/utils/withColours'
|
||||
import type { Répartition } from 'Types/ResultViewTypes.js'
|
||||
import useDisplayOnIntersecting from 'Components/utils/useDisplayOnIntersecting'
|
||||
|
||||
type Props = ?Répartition & {
|
||||
colours: ThemeColours
|
||||
}
|
||||
|
||||
const ANIMATION_SPRING = config.gentle
|
||||
function Distribution({
|
||||
colours: { colour },
|
||||
rules,
|
||||
// $FlowFixMe
|
||||
...distribution
|
||||
}: Props) {
|
||||
const [branchesInViewport, setBranchesInViewport] = useState([])
|
||||
|
||||
const handleBrancheInViewport = branche => (event, unobserve) => {
|
||||
if (!event.isIntersecting) {
|
||||
return
|
||||
}
|
||||
unobserve()
|
||||
setBranchesInViewport(branchesInViewport => [
|
||||
branche,
|
||||
...branchesInViewport
|
||||
])
|
||||
}
|
||||
export default function Distribution() {
|
||||
const distribution = useSelector(répartitionSelector)
|
||||
|
||||
if (!Object.values(distribution).length) {
|
||||
return null
|
||||
}
|
||||
|
||||
const {
|
||||
répartition,
|
||||
cotisationMaximum,
|
||||
total,
|
||||
cotisations,
|
||||
salaireChargé,
|
||||
salaireNet
|
||||
} = distribution
|
||||
return (
|
||||
<>
|
||||
<div className="distribution-chart__container">
|
||||
{répartition.map(
|
||||
([brancheDottedName, { partPatronale, partSalariale }]) => {
|
||||
const branche = findRuleByDottedName(rules, brancheDottedName),
|
||||
brancheInViewport =
|
||||
branchesInViewport.indexOf(brancheDottedName) !== -1
|
||||
const montant = brancheInViewport
|
||||
? partPatronale + partSalariale
|
||||
: 0
|
||||
|
||||
return (
|
||||
<Observer
|
||||
key={brancheDottedName}
|
||||
threshold={[0.5]}
|
||||
onChange={handleBrancheInViewport(brancheDottedName)}>
|
||||
<Spring
|
||||
config={ANIMATION_SPRING}
|
||||
to={{
|
||||
flex: montant / cotisationMaximum,
|
||||
opacity: montant ? 1 : 0
|
||||
}}>
|
||||
{styles => (
|
||||
<div
|
||||
className="distribution-chart__item"
|
||||
style={{
|
||||
opacity: styles.opacity
|
||||
}}>
|
||||
<BranchIcône icône={branche.icons} />
|
||||
<div className="distribution-chart__item-content">
|
||||
<p className="distribution-chart__counterparts">
|
||||
<span className="distribution-chart__branche-name">
|
||||
<RuleLink {...branche} />
|
||||
</span>
|
||||
<br />
|
||||
<small>{branche.summary}</small>
|
||||
</p>
|
||||
<ChartItemBar {...{ styles, colour, montant, total }} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Spring>
|
||||
</Observer>
|
||||
)
|
||||
}
|
||||
{distribution.répartition.map(
|
||||
([brancheDottedName, { partPatronale, partSalariale }]) => (
|
||||
<DistributionBranch
|
||||
key={brancheDottedName}
|
||||
{...{
|
||||
brancheDottedName,
|
||||
partPatronale,
|
||||
partSalariale,
|
||||
distribution
|
||||
}}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<div className="distribution-chart__total">
|
||||
<span />
|
||||
<RuleLink {...salaireNet} />
|
||||
<Value {...salaireNet} unit="€" maximumFractionDigits={0} />
|
||||
<span>+</span>
|
||||
<RuleLink {...cotisations} />
|
||||
<Value {...cotisations} unit="€" maximumFractionDigits={0} />
|
||||
<span />
|
||||
<div className="distribution-chart__total-border" />
|
||||
<span>=</span>
|
||||
<RuleLink {...salaireChargé} />
|
||||
<Value {...salaireChargé} unit="€" maximumFractionDigits={0} />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default compose(
|
||||
withColours,
|
||||
connect(state => ({
|
||||
...répartitionSelector(state),
|
||||
rules: flatRulesSelector(state)
|
||||
}))
|
||||
)(Distribution)
|
||||
|
||||
let ChartItemBar = ({ styles, colour, montant, total }) => (
|
||||
const ANIMATION_SPRING = config.gentle
|
||||
function DistributionBranch({
|
||||
brancheDottedName,
|
||||
partPatronale,
|
||||
partSalariale,
|
||||
distribution
|
||||
}) {
|
||||
const rules = useSelector(flatRulesSelector)
|
||||
const [intersectionRef, brancheInViewport] = useDisplayOnIntersecting({
|
||||
threshold: 0.5
|
||||
})
|
||||
const colours = useContext(ThemeColoursContext)
|
||||
const branche = findRuleByDottedName(rules, brancheDottedName)
|
||||
const montant = brancheInViewport ? partPatronale + partSalariale : 0
|
||||
const styles = useSpring({
|
||||
config: ANIMATION_SPRING,
|
||||
to: {
|
||||
flex: montant / distribution.cotisationMaximum,
|
||||
opacity: montant ? 1 : 0
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<animated.div
|
||||
ref={intersectionRef}
|
||||
className="distribution-chart__item"
|
||||
style={{ opacity: styles.opacity }}>
|
||||
<BranchIcône icône={branche.icons} />
|
||||
<div className="distribution-chart__item-content">
|
||||
<p className="distribution-chart__counterparts">
|
||||
<span className="distribution-chart__branche-name">
|
||||
<RuleLink {...branche} />
|
||||
</span>
|
||||
<br />
|
||||
<small>{branche.summary}</small>
|
||||
</p>
|
||||
<ChartItemBar
|
||||
{...{
|
||||
styles,
|
||||
colour: colours.colour,
|
||||
montant,
|
||||
total: distribution.total
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</animated.div>
|
||||
)
|
||||
}
|
||||
|
||||
let ChartItemBar = ({ styles, colour, montant }) => (
|
||||
<div className="distribution-chart__bar-container">
|
||||
<div
|
||||
<animated.div
|
||||
className="distribution-chart__bar"
|
||||
style={{
|
||||
backgroundColor: colour,
|
||||
|
|
|
@ -1,12 +1,18 @@
|
|||
import { T } from 'Components'
|
||||
import Distribution from 'Components/Distribution'
|
||||
import PaySlip from 'Components/PaySlip'
|
||||
import { compose } from 'ramda'
|
||||
import React, { useRef } from 'react'
|
||||
import StackedBarChart from 'Components/StackedBarChart'
|
||||
import { ThemeColoursContext } from 'Components/utils/withColours'
|
||||
import { getRuleFromAnalysis } from 'Engine/rules'
|
||||
import React, { useRef, useContext } from 'react'
|
||||
import emoji from 'react-easy-emoji'
|
||||
import { Trans } from 'react-i18next'
|
||||
import { connect } from 'react-redux'
|
||||
import { usePeriod } from 'Selectors/analyseSelectors'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
analysisWithDefaultsSelector,
|
||||
usePeriod
|
||||
} from 'Selectors/analyseSelectors'
|
||||
import * as Animate from 'Ui/animate'
|
||||
|
||||
class ErrorBoundary extends React.Component {
|
||||
|
@ -24,22 +30,23 @@ class ErrorBoundary extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
export default compose(
|
||||
connect(state => ({
|
||||
showDistributionFirst: !state.conversationSteps.foldedSteps.length
|
||||
}))
|
||||
)(function SalaryExplanation({ showDistributionFirst }) {
|
||||
export default function SalaryExplanation() {
|
||||
const showDistributionFirst = useSelector(
|
||||
state => !state.conversationSteps.foldedSteps.length
|
||||
)
|
||||
const distributionRef = useRef({})
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<Animate.fromTop key={showDistributionFirst}>
|
||||
{showDistributionFirst ? (
|
||||
<>
|
||||
<RevenueRepatitionSection />
|
||||
<DistributionSection />
|
||||
<PaySlipSection />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<RevenueRepatitionSection />
|
||||
<div css="text-align: center">
|
||||
<button
|
||||
className="ui__ small simple button"
|
||||
|
@ -87,7 +94,34 @@ export default compose(
|
|||
</Animate.fromTop>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
function RevenueRepatitionSection() {
|
||||
const analysis = useSelector(analysisWithDefaultsSelector)
|
||||
const getRule = getRuleFromAnalysis(analysis)
|
||||
const { t } = useTranslation()
|
||||
const { palettes } = useContext(ThemeColoursContext)
|
||||
|
||||
return (
|
||||
<section>
|
||||
<h2>Répartition du total chargé</h2>
|
||||
<StackedBarChart
|
||||
data={[
|
||||
{
|
||||
...getRule('contrat salarié . rémunération . net après impôt'),
|
||||
name: t('Revenu disponible'),
|
||||
color: palettes[0][0]
|
||||
},
|
||||
{ ...getRule('impôt'), name: t('Impôts'), color: palettes[1][0] },
|
||||
{
|
||||
...getRule('contrat salarié . cotisations'),
|
||||
color: palettes[1][1]
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function PaySlipSection() {
|
||||
const period = usePeriod()
|
||||
|
|
|
@ -6,21 +6,15 @@ import PageFeedback from 'Components/Feedback/PageFeedback'
|
|||
import SearchButton from 'Components/SearchButton'
|
||||
import TargetSelection from 'Components/TargetSelection'
|
||||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { firstStepCompletedSelector } from 'Selectors/analyseSelectors'
|
||||
import { simulationProgressSelector } from 'Selectors/progressSelectors'
|
||||
import * as Animate from 'Ui/animate'
|
||||
import Progress from 'Ui/Progress'
|
||||
|
||||
export default connect(state => ({
|
||||
firstStepCompleted: firstStepCompletedSelector(state),
|
||||
progress: simulationProgressSelector(state)
|
||||
}))(function Simulation({
|
||||
firstStepCompleted,
|
||||
explanations,
|
||||
customEndMessages,
|
||||
progress
|
||||
}) {
|
||||
export default function Simulation({ explanations, customEndMessages }) {
|
||||
const firstStepCompleted = useSelector(firstStepCompletedSelector)
|
||||
const progress = useSelector(simulationProgressSelector)
|
||||
return (
|
||||
<>
|
||||
<TargetSelection />
|
||||
|
@ -71,4 +65,4 @@ export default connect(state => ({
|
|||
)}
|
||||
</>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -0,0 +1,117 @@
|
|||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import RuleLink from 'Components/RuleLink'
|
||||
import useDisplayOnIntersecting from 'Components/utils/useDisplayOnIntersecting'
|
||||
import { animated, useSpring } from 'react-spring'
|
||||
import { capitalise0 } from '../utils'
|
||||
|
||||
const BarStack = styled.div`
|
||||
display: flex;
|
||||
border-radius: 0.4em;
|
||||
overflow: hidden;
|
||||
`
|
||||
|
||||
const BarItem = styled.div`
|
||||
height: 26px;
|
||||
border-right: 2px solid white;
|
||||
transition: width 0.3s ease-out;
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
`
|
||||
|
||||
const BarStackLegend = styled.div`
|
||||
display: flex;
|
||||
margin-top: 10px;
|
||||
flex-direction: column;
|
||||
|
||||
@media (min-width: 800px) {
|
||||
flex-direction: row;
|
||||
text-align: center;
|
||||
}
|
||||
`
|
||||
|
||||
const BarStackLegendItem = styled.div`
|
||||
flex: 1 1 0px;
|
||||
color: #555;
|
||||
|
||||
strong {
|
||||
display: inline-block;
|
||||
color: #111;
|
||||
margin-left: 8px;
|
||||
}
|
||||
`
|
||||
|
||||
const SmallCircle = styled.span`
|
||||
display: inline-block;
|
||||
height: 11px;
|
||||
width: 11px;
|
||||
margin-right: 10px;
|
||||
border-radius: 100%;
|
||||
`
|
||||
|
||||
function integerAndDecimalParts(value) {
|
||||
const integer = Math.floor(value)
|
||||
const decimal = value - integer
|
||||
return { integer, decimal }
|
||||
}
|
||||
|
||||
// This function calculates rounded percentages so that the sum of all
|
||||
// returned values is always 100. For instance: [60, 30, 10].
|
||||
export function roundedPercentages(values) {
|
||||
const sum = (a = 0, b) => a + b
|
||||
const total = values.reduce(sum)
|
||||
const percentages = values.map(value =>
|
||||
integerAndDecimalParts((value / total) * 100)
|
||||
)
|
||||
const totalRoundedPercentage = percentages.map(v => v.integer).reduce(sum)
|
||||
const indexesToIncrement = percentages
|
||||
.map((percentage, index) => ({ ...percentage, index }))
|
||||
.sort((a, b) => b.decimal - a.decimal)
|
||||
.map(({ index }) => index)
|
||||
.splice(0, 100 - totalRoundedPercentage)
|
||||
|
||||
return percentages.map(
|
||||
({ integer }, index) =>
|
||||
integer + (indexesToIncrement.includes(index) ? 1 : 0)
|
||||
)
|
||||
}
|
||||
|
||||
export default function StackedBarChart({ data }) {
|
||||
const [intersectionRef, displayChart] = useDisplayOnIntersecting({
|
||||
threshold: 0.5
|
||||
})
|
||||
const percentages = roundedPercentages(data.map(d => d.nodeValue))
|
||||
const dataWithPercentage = data.map((data, index) => ({
|
||||
...data,
|
||||
percentage: percentages[index]
|
||||
}))
|
||||
|
||||
const styles = useSpring({ opacity: displayChart ? 1 : 0 })
|
||||
|
||||
return (
|
||||
<animated.div ref={intersectionRef} style={styles}>
|
||||
<BarStack>
|
||||
{dataWithPercentage.map(({ dottedName, color, percentage }) => (
|
||||
<BarItem
|
||||
style={{
|
||||
width: `${percentage}%`,
|
||||
backgroundColor: color || 'green'
|
||||
}}
|
||||
key={dottedName}
|
||||
/>
|
||||
))}
|
||||
</BarStack>
|
||||
<BarStackLegend>
|
||||
{dataWithPercentage.map(({ percentage, color, ...rule }) => (
|
||||
<BarStackLegendItem key={rule.dottedName}>
|
||||
<SmallCircle style={{ backgroundColor: color }} />
|
||||
<RuleLink {...rule}>{capitalise0(rule.name)}</RuleLink>
|
||||
<strong>{percentage} %</strong>
|
||||
</BarStackLegendItem>
|
||||
))}
|
||||
</BarStackLegend>
|
||||
</animated.div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import { expect } from 'chai'
|
||||
import { roundedPercentages } from './StackedBarChart'
|
||||
|
||||
describe('roundedPercentages', () => {
|
||||
it('rounds correctly', () => {
|
||||
expect(roundedPercentages([500, 250, 250])).to.deep.equal([50, 25, 25])
|
||||
expect(roundedPercentages([501, 251, 248])).to.deep.equal([50, 25, 25])
|
||||
expect(roundedPercentages([506, 257, 237])).to.deep.equal([50, 26, 24])
|
||||
expect(roundedPercentages([509, 259, 232])).to.deep.equal([51, 26, 23])
|
||||
expect(roundedPercentages([503, 253, 244])).to.deep.equal([50, 25, 25])
|
||||
})
|
||||
})
|
|
@ -6,8 +6,8 @@ import {
|
|||
Spring,
|
||||
Trail,
|
||||
Transition
|
||||
} from 'react-spring'
|
||||
import type { SpringConfig } from 'react-spring'
|
||||
} from 'react-spring/renderprops'
|
||||
import type { SpringConfig } from 'react-spring/renderprops'
|
||||
import type { Node } from 'react'
|
||||
|
||||
type Props = {
|
||||
|
@ -32,11 +32,11 @@ export const fromBottom = ({
|
|||
config={config}
|
||||
from={{ opacity: 0, y: 20 }}
|
||||
leave={{ opacity: 0, y: -20 }}
|
||||
to={{ opacity: 1, y: 0 }}>
|
||||
to={{ opacity: 1, y: 0 }}
|
||||
items={children}>
|
||||
{/* eslint-disable-next-line react/display-name */}
|
||||
{React.Children.map(children, (item, i) => ({ y, ...style }) => (
|
||||
{item => ({ y, ...style }) => (
|
||||
<animated.div
|
||||
key={i}
|
||||
style={{
|
||||
transform: y.interpolate(y =>
|
||||
y !== 0 ? `translate3d(0, ${y}px,0)` : 'none'
|
||||
|
@ -46,7 +46,7 @@ export const fromBottom = ({
|
|||
}}>
|
||||
{item}
|
||||
</animated.div>
|
||||
))}
|
||||
)}
|
||||
</Trail>
|
||||
)
|
||||
export const fromTop = ({
|
||||
|
@ -62,11 +62,11 @@ export const fromTop = ({
|
|||
config={config}
|
||||
leave={{ opacity: 0, y: 20 }}
|
||||
from={{ opacity: 0, y: -20 }}
|
||||
to={{ opacity: 1, y: 0 }}>
|
||||
to={{ opacity: 1, y: 0 }}
|
||||
items={children}>
|
||||
{/* eslint-disable-next-line react/display-name */}
|
||||
{React.Children.map(children, (item, i) => ({ y, ...style }) => (
|
||||
{item => ({ y, ...style }) => (
|
||||
<animated.div
|
||||
key={i}
|
||||
style={{
|
||||
transform: y.interpolate(y =>
|
||||
y ? `translate3d(0, ${y}px,0)` : 'none'
|
||||
|
@ -76,7 +76,7 @@ export const fromTop = ({
|
|||
}}>
|
||||
{item}
|
||||
</animated.div>
|
||||
))}
|
||||
)}
|
||||
</Trail>
|
||||
)
|
||||
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
import { useEffect, useRef, useState } from 'react'
|
||||
|
||||
export default function({ root = null, rootMargin, threshold = 0 }) {
|
||||
const ref = useRef()
|
||||
const [wasOnScreen, setWasOnScreen] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
setWasOnScreen(entry.isIntersecting)
|
||||
observer.unobserve(ref.current)
|
||||
}
|
||||
},
|
||||
{
|
||||
root,
|
||||
rootMargin,
|
||||
threshold
|
||||
}
|
||||
)
|
||||
const node = ref.current
|
||||
if (ref.current) {
|
||||
observer.observe(node)
|
||||
}
|
||||
return () => {
|
||||
observer.unobserve(node)
|
||||
}
|
||||
}, [root, rootMargin, threshold]) // Empty array ensures that effect is only run on mount and unmount
|
||||
|
||||
return [ref, wasOnScreen]
|
||||
}
|
|
@ -53,6 +53,21 @@ const lightenColour = (hex, x) => {
|
|||
const [h, s, l] = convert.hex.hsl(hex.split('#')[1])
|
||||
return '#' + convert.hsl.hex([h, s, Math.max(2, Math.min(l + x, 98))])
|
||||
}
|
||||
|
||||
const generateDarkenVariations = (numberOfVariation, [h, s, l]) => {
|
||||
return [...Array(numberOfVariation).keys()].map(
|
||||
i => '#' + convert.hsl.hex([h, s, l * 0.8 ** i])
|
||||
)
|
||||
}
|
||||
|
||||
const deriveAnalogousPalettes = hex => {
|
||||
const [h, s, l] = convert.hex.hsl(hex.split('#')[1])
|
||||
return [
|
||||
generateDarkenVariations(4, [(h - 45) % 360, 0.75 * s, l]),
|
||||
generateDarkenVariations(4, [(h + 45) % 360, 0.75 * s, l])
|
||||
]
|
||||
}
|
||||
|
||||
const generateTheme = (themeColour?: ?string): ThemeColours => {
|
||||
let // Use the default theme colour if the host page hasn't made a choice
|
||||
colour = themeColour || '#2975D1',
|
||||
|
@ -70,7 +85,8 @@ const generateTheme = (themeColour?: ?string): ThemeColours => {
|
|||
: 'rgba(0, 0, 0, .6)',
|
||||
lighterTextColour = darkColour + '99',
|
||||
lighterInverseTextColour = lightenTextColour(inverseTextColour),
|
||||
textColourOnWhite = textColour === '#ffffff' ? colour : '#333'
|
||||
textColourOnWhite = textColour === '#ffffff' ? colour : '#333',
|
||||
palettes = deriveAnalogousPalettes(colour)
|
||||
|
||||
return {
|
||||
colour,
|
||||
|
@ -84,7 +100,8 @@ const generateTheme = (themeColour?: ?string): ThemeColours => {
|
|||
lightColour,
|
||||
lighterColour,
|
||||
lightestColour,
|
||||
darkestColour
|
||||
darkestColour,
|
||||
palettes
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -103,6 +103,8 @@ Fiche de paie: Payslip
|
|||
Détail annuel des cotisations: Annual detail of my contributions
|
||||
Voir la répartition des cotisations: View contribution breakdown
|
||||
Cotisations: Contributions
|
||||
Revenu disponible: Disposable income
|
||||
Impôts: Taxes
|
||||
payslip:
|
||||
notice: This simulation helps you understand your French payslip, but it should not be used as one. For further details, check <1>service-public.fr (French)</1>.
|
||||
heures: 'Hours worked per month: '
|
||||
|
|
|
@ -3181,7 +3181,7 @@
|
|||
- nom: impôt
|
||||
icônes: 🏛️
|
||||
description: Cet ensemble de formules est un modèle ultra-simplifié de l'impôt sur le revenu, qui ne prend en compte que l'abattement 10%, le barème et la décôte.
|
||||
titre: impôt sur le revenu
|
||||
titre: impôts sur le revenu
|
||||
période: flexible
|
||||
unité: €
|
||||
formule:
|
||||
|
|
|
@ -1,41 +1,72 @@
|
|||
import { T } from 'Components'
|
||||
import Warning from 'Components/SimulateurWarning'
|
||||
import Simulation from 'Components/Simulation'
|
||||
import StackedBarChart from 'Components/StackedBarChart'
|
||||
import indépendantConfig from 'Components/simulationConfigs/auto-entrepreneur.yaml'
|
||||
import withSimulationConfig from 'Components/simulationConfigs/withSimulationConfig'
|
||||
import { compose } from 'ramda'
|
||||
import React from 'react'
|
||||
import { ThemeColoursContext } from 'Components/utils/withColours'
|
||||
import { getRuleFromAnalysis } from 'Engine/rules'
|
||||
import { analysisWithDefaultsSelector } from 'Selectors/analyseSelectors'
|
||||
import { useSelector } from 'react-redux'
|
||||
import React, { useContext } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Helmet } from 'react-helmet'
|
||||
import { withTranslation } from 'react-i18next'
|
||||
import { AvertissementProtectionSocialeIndépendants } from './Indépendant'
|
||||
|
||||
const AutoEntrepreneur = ({ t }) => (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>
|
||||
{t(
|
||||
'simulateurs.auto-entrepreneur.page.titre',
|
||||
'Auto-entrepreneur : simulateur officiel de revenus et de cotisations'
|
||||
)}
|
||||
</title>
|
||||
<meta
|
||||
name="description"
|
||||
content={t(
|
||||
'simulateurs.auto-entrepreneur.page.description',
|
||||
"Estimez vos revenus en tant qu'auto-entrepreneur à partir de votre chiffre d'affaire. Prise en compte de toutes les cotisations et de l'impôt sur le revenu. Simulateur officiel de l'Urssaf"
|
||||
)}
|
||||
const AutoEntrepreneur = () => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>
|
||||
{t(
|
||||
'simulateurs.auto-entrepreneur.page.titre',
|
||||
'Auto-entrepreneur : simulateur officiel de revenus et de cotisations'
|
||||
)}
|
||||
</title>
|
||||
<meta
|
||||
name="description"
|
||||
content={t(
|
||||
'simulateurs.auto-entrepreneur.page.description',
|
||||
"Estimez vos revenus en tant qu'auto-entrepreneur à partir de votre chiffre d'affaire. Prise en compte de toutes les cotisations et de l'impôt sur le revenu. Simulateur officiel de l'Urssaf"
|
||||
)}
|
||||
/>
|
||||
</Helmet>
|
||||
<h1>
|
||||
<T k="simulateurs.auto-entrepreneur.titre">
|
||||
Simulateur de revenus auto-entrepreneur
|
||||
</T>
|
||||
</h1>
|
||||
<Warning simulateur="auto-entreprise" />
|
||||
<Simulation explanations={<ExplanationSection />} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default withSimulationConfig(indépendantConfig)(AutoEntrepreneur)
|
||||
|
||||
function ExplanationSection() {
|
||||
const analysis = useSelector(analysisWithDefaultsSelector)
|
||||
const getRule = getRuleFromAnalysis(analysis)
|
||||
const { t } = useTranslation()
|
||||
const { palettes } = useContext(ThemeColoursContext)
|
||||
|
||||
return (
|
||||
<section>
|
||||
<h2>Répartition du chiffre d'affaire</h2>
|
||||
<StackedBarChart
|
||||
data={[
|
||||
{
|
||||
...getRule('revenu net après impôt'),
|
||||
color: palettes[0][0]
|
||||
},
|
||||
{ ...getRule('impôt'), color: palettes[1][0] },
|
||||
{
|
||||
...getRule('auto-entrepreneur . cotisations et contributions'),
|
||||
name: t('Cotisations'),
|
||||
color: palettes[1][1]
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</Helmet>
|
||||
<h1>
|
||||
<T k="simulateurs.auto-entrepreneur.titre">
|
||||
Simulateur de revenus auto-entrepreneur
|
||||
</T>
|
||||
</h1>
|
||||
<Warning simulateur="auto-entreprise" />
|
||||
<Simulation explanation={<AvertissementProtectionSocialeIndépendants />} />
|
||||
</>
|
||||
)
|
||||
export default compose(
|
||||
withTranslation(),
|
||||
withSimulationConfig(indépendantConfig)
|
||||
)(AutoEntrepreneur)
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,73 +1,71 @@
|
|||
import { React, T } from 'Components'
|
||||
import { ThemeColoursContext } from 'Components/utils/withColours'
|
||||
import { T } from 'Components'
|
||||
import { getRuleFromAnalysis } from 'Engine/rules'
|
||||
import Warning from 'Components/SimulateurWarning'
|
||||
import Simulation from 'Components/Simulation'
|
||||
import StackedBarChart from 'Components/StackedBarChart'
|
||||
import indépendantConfig from 'Components/simulationConfigs/indépendant.yaml'
|
||||
import withSimulationConfig from 'Components/simulationConfigs/withSimulationConfig'
|
||||
import { compose } from 'ramda'
|
||||
import emoji from 'react-easy-emoji'
|
||||
import { analysisWithDefaultsSelector } from 'Selectors/analyseSelectors'
|
||||
import React, { useContext } from 'react'
|
||||
import { Helmet } from 'react-helmet'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export default compose(withSimulationConfig(indépendantConfig))(
|
||||
function Indépendant() {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>
|
||||
{t(
|
||||
'simulateurs.indépendant.page.titre',
|
||||
'Indépendant : simulateur officiel de revenus et de cotisations'
|
||||
)}
|
||||
</title>
|
||||
<meta
|
||||
name="description"
|
||||
content={t(
|
||||
'simulateurs.indépendant.page.description',
|
||||
"Estimez vos revenus en tant qu'indépendant à partir de votre chiffre d'affaire (pour les EI et les gérants EURL et SARL majoritaires). Prise en compte de toutes les cotisations et de l'impôt sur le revenu. Simulateur officiel de l'Urssaf"
|
||||
)}
|
||||
/>
|
||||
</Helmet>
|
||||
<h1>
|
||||
<T k="simulateurs.indépendant.titre">
|
||||
Simulateur de revenus pour indépendants
|
||||
</T>
|
||||
</h1>
|
||||
<Warning />
|
||||
<Simulation
|
||||
explanation={
|
||||
<>
|
||||
<AvertissementForfaitIndépendants />
|
||||
<AvertissementProtectionSocialeIndépendants />
|
||||
</>
|
||||
}
|
||||
export default withSimulationConfig(indépendantConfig)(function Indépendant() {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>
|
||||
{t(
|
||||
'simulateurs.indépendant.page.titre',
|
||||
'Indépendant : simulateur officiel de revenus et de cotisations'
|
||||
)}
|
||||
</title>
|
||||
<meta
|
||||
name="description"
|
||||
content={t(
|
||||
'simulateurs.indépendant.page.description',
|
||||
"Estimez vos revenus en tant qu'indépendant à partir de votre chiffre d'affaire (pour les EI et les gérants EURL et SARL majoritaires). Prise en compte de toutes les cotisations et de l'impôt sur le revenu. Simulateur officiel de l'Urssaf"
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
)
|
||||
</Helmet>
|
||||
<h1>
|
||||
<T k="simulateurs.indépendant.titre">
|
||||
Simulateur de revenus pour indépendants
|
||||
</T>
|
||||
</h1>
|
||||
<Warning />
|
||||
<Simulation explanations={<ExplanationSection />} />
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
let AvertissementForfaitIndépendants = () => (
|
||||
<p className="ui__ notice">
|
||||
{emoji('💶')}{' '}
|
||||
<T k="simulateurs.indépendant.explication1">
|
||||
Notre estimation prend en compte les <em>cotisations réelles</em> dues par
|
||||
le travailleur indépendant. Pendant la première année de son activité, il
|
||||
paiera un forfait réduit (une somme de l'ordre de 1300€ / an pour un
|
||||
artisan bénéficiant de l'ACRE)... mais il sera régularisé l'année suivante
|
||||
selon ce montant réel.
|
||||
</T>
|
||||
</p>
|
||||
)
|
||||
function ExplanationSection() {
|
||||
const analysis = useSelector(analysisWithDefaultsSelector)
|
||||
const getRule = getRuleFromAnalysis(analysis)
|
||||
const { t } = useTranslation()
|
||||
const { palettes } = useContext(ThemeColoursContext)
|
||||
|
||||
export let AvertissementProtectionSocialeIndépendants = () => (
|
||||
<p className="ui__ notice">
|
||||
{emoji('☂️')}{' '}
|
||||
<T k="simulateurs.indépendant.explication1">
|
||||
Les assurances chômage et accidents du travail ne sont pas prises en
|
||||
charge au sein de la Sécurité sociale des indépendants. La retraite basée
|
||||
sur le revenu professionnel est généralement plus faible. Pour être
|
||||
couvert le professionnel peut souscrire des assurances complémentaires.
|
||||
</T>
|
||||
</p>
|
||||
)
|
||||
return (
|
||||
<section>
|
||||
<h2>Répartition du chiffre d'affaire</h2>
|
||||
<StackedBarChart
|
||||
data={[
|
||||
{
|
||||
...getRule('revenu net après impôt'),
|
||||
name: t('Revenu disponible'),
|
||||
color: palettes[0][0]
|
||||
},
|
||||
{ ...getRule('impôt'), color: palettes[1][0] },
|
||||
{
|
||||
...getRule('indépendant . cotisations et contributions'),
|
||||
name: t('Cotisations'),
|
||||
color: palettes[1][1]
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
|
22
yarn.lock
22
yarn.lock
|
@ -744,7 +744,7 @@
|
|||
"@babel/plugin-transform-react-jsx-self" "^7.0.0"
|
||||
"@babel/plugin-transform-react-jsx-source" "^7.0.0"
|
||||
|
||||
"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.4.0":
|
||||
"@babel/runtime@^7.1.2", "@babel/runtime@^7.4.0":
|
||||
version "7.4.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.4.5.tgz#582bb531f5f9dc67d2fcb682979894f75e253f12"
|
||||
integrity sha512-TuI4qpWZP6lGOGIuGWtp9sPluqYICmbk8T/1vpSysqJxRPkudh/ofFWyqdcMsDf2s7KvDL4/YHgKyvcS3g9CJQ==
|
||||
|
@ -948,15 +948,6 @@
|
|||
promise-limit "^2.5.0"
|
||||
puppeteer "^1.7.0"
|
||||
|
||||
"@researchgate/react-intersection-observer@^0.7.3":
|
||||
version "0.7.4"
|
||||
resolved "https://registry.yarnpkg.com/@researchgate/react-intersection-observer/-/react-intersection-observer-0.7.4.tgz#9360274611beebd801e3c068294ddf2ab0d4b163"
|
||||
integrity sha512-4F291saKAP9I25Qe1ePflvm1DLLA43GlBIZfpMFYWofph7CAm+19nT8xwkQqSszg4PwZa5BpkaI4tAEJHtlj3w==
|
||||
dependencies:
|
||||
invariant "^2.2.2"
|
||||
prop-types "^15.6.0"
|
||||
warning "^3.0.0"
|
||||
|
||||
"@sindresorhus/is@^0.7.0":
|
||||
version "0.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd"
|
||||
|
@ -8196,12 +8187,13 @@ react-side-effect@^1.1.0:
|
|||
exenv "^1.2.1"
|
||||
shallowequal "^1.0.1"
|
||||
|
||||
react-spring@^5.8.0:
|
||||
version "5.9.2"
|
||||
resolved "https://registry.yarnpkg.com/react-spring/-/react-spring-5.9.2.tgz#4c4e048ffce24755eaa60acd2f3fe4a3d686ef16"
|
||||
integrity sha512-ZDCBm4OaYuuSER3k7EELn9AKGs98HI23qMRL93zUUw6AlyBmqoU+eAfouGRx47gVkZkH+hArJXBHf4UGYpH9Og==
|
||||
react-spring@=8.0.27:
|
||||
version "8.0.27"
|
||||
resolved "https://registry.yarnpkg.com/react-spring/-/react-spring-8.0.27.tgz#97d4dee677f41e0b2adcb696f3839680a3aa356a"
|
||||
integrity sha512-nDpWBe3ZVezukNRandTeLSPcwwTMjNVu1IDq9qA/AMiUqHuRN4BeSWvKr3eIxxg1vtiYiOLy4FqdfCP5IoP77g==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.0.0"
|
||||
"@babel/runtime" "^7.3.1"
|
||||
prop-types "^15.5.8"
|
||||
|
||||
react-syntax-highlighter@^10.1.1:
|
||||
version "10.3.5"
|
||||
|
|
Loading…
Reference in New Issue