diff --git a/modele-social/règles/dirigeant.yaml b/modele-social/règles/dirigeant.yaml index 64d8c78dd..fdaff10fc 100644 --- a/modele-social/règles/dirigeant.yaml +++ b/modele-social/règles/dirigeant.yaml @@ -128,6 +128,7 @@ dirigeant . auto-entrepreneur . seuils dépassés . notification: dirigeant . auto-entrepreneur . net de cotisations: titre: Revenu net de cotisations arrondi: oui + unité: €/an identifiant court: auto-entrepreneur-net résumé: Avant impôt question: Quel revenu avant impôt voulez-vous toucher ? @@ -234,10 +235,9 @@ dirigeant . auto-entrepreneur . cotisations et contributions . contribution form - sinon: 0.1% dirigeant . auto-entrepreneur . chiffre d'affaires: question: Quel est votre chiffre d'affaires ? - résumé: Montant total des recettes brutes + résumé: Montant total des recettes brutes (hors taxe) unité: €/an arrondi: oui - non applicable si: entreprise . activité . mixte inversion numérique: avec: - net de cotisations diff --git a/modele-social/règles/déclaration-revenu-indépendant.yaml b/modele-social/règles/déclaration-revenu-indépendant.yaml index 583f85859..3a7b78f97 100644 --- a/modele-social/règles/déclaration-revenu-indépendant.yaml +++ b/modele-social/règles/déclaration-revenu-indépendant.yaml @@ -72,13 +72,8 @@ aide déclaration revenu indépendant 2020 . nature de l'activité . libérale: références: fiche Wikipedia: https://fr.m.wikipedia.org/wiki/Profession_libérale -<<<<<<< HEAD aide déclaration revenu indépendant 2020 . nature de l'activité . commerciale ou industrielle: - remplace: entreprise . catégorie d'activité . commerciale ou industrielle -======= -aide déclaration revenu indépendant 2019 . nature de l'activité . commerciale ou industrielle: remplace: entreprise . activité . commerciale ou industrielle ->>>>>>> 76a45672 ((auto-entrepreneur) Ajoute les revenus mixtes) formule: nature de l'activité = 'commerciale ou industrielle' description: | ### Activité commerciale diff --git a/modele-social/règles/entreprise-établissement.yaml b/modele-social/règles/entreprise-établissement.yaml index f8e9ff574..d5e00bc53 100644 --- a/modele-social/règles/entreprise-établissement.yaml +++ b/modele-social/règles/entreprise-établissement.yaml @@ -68,22 +68,19 @@ entreprise . chiffre d'affaires . vente restauration hébergement: - activité . service ou vente = 'vente' - activité . mixte titre: Vente de biens, restauration, hebergement - par défaut: - le maximum de: - - dirigeant . auto-entrepreneur . chiffre d'affaires - - 0 €/an résumé: Chiffre d'affaires hors taxe question: Quel est le chiffre d'affaires issus de la vente de bien, restauration ou hébergement ? unité: €/an + par défaut: + produit: + assiette: dirigeant . auto-entrepreneur . chiffre d'affaires + facteur: + variations: + - si: activité = 'libérale' + alors: 30% + - sinon: 70% + arrondi: oui plancher: 0€/an - inversion numérique: - avec: - - dirigeant . auto-entrepreneur . net de cotisations - - dirigeant . auto-entrepreneur . net après impôt - abattement: - applicable si: activité . mixte - valeur: 2 / 3 - unité: '%' description: | ### Vente de biens Il s’agit du chiffre d'affaires de toutes les opérations comportant @@ -114,19 +111,16 @@ entreprise . chiffre d'affaires . prestations de service . BIC: - activité . service ou vente = 'service' - activité . mixte unité: €/an - par défaut: - le maximum de: - - dirigeant . auto-entrepreneur . chiffre d'affaires - - 0 €/an + par défaut: + produit: + assiette: dirigeant . auto-entrepreneur . chiffre d'affaires + facteur: + variations: + - si: activité = 'libérale' + alors: 0 + - sinon: 30% plancher: 0€/an - inversion numérique: - avec: - - dirigeant . auto-entrepreneur . net de cotisations - - dirigeant . auto-entrepreneur . net après impôt - abattement: - applicable si: activité . mixte - valeur: 2 / 3 - unité: '%' + arrondi: oui résumé: Chiffre d'affaires hors taxe titre: Prestations de service commerciales ou artisanales question: Quel est le chiffre d'affaires issus de prestations de service commerciales ou artisanales ? @@ -146,23 +140,21 @@ entreprise . chiffre d'affaires . prestations de service . BNC: titre: Prestations de service libérale résumé: Chiffre d'affaires hors taxe question: Quel est le chiffre d'affaires issus de prestations de service libérale ? - par défaut: - le maximum de: - - dirigeant . auto-entrepreneur . chiffre d'affaires - - 0 €/an + par défaut: + produit: + assiette: dirigeant . auto-entrepreneur . chiffre d'affaires + facteur: + variations: + - si: activité = 'libérale' + alors: 70% + - sinon: 0.0000001% # On veut qu'elle reste manquante en fonction du CA + arrondi: oui applicable si: une de ces conditions: - activité = 'libérale' - activité . mixte plancher: 0€/an - inversion numérique: - avec: - - dirigeant . auto-entrepreneur . net de cotisations - - dirigeant . auto-entrepreneur . net après impôt - abattement: - applicable si: activité . mixte - valeur: 2 / 3 - unité: '%' + description: | Ce sont toutes les opérations dont l'activité intellectuelle tient un rôle essentiel. diff --git a/mon-entreprise/source/components/CurrencyInput/CurrencyInput.tsx b/mon-entreprise/source/components/CurrencyInput/CurrencyInput.tsx index d48a2254f..9b9a9e4db 100644 --- a/mon-entreprise/source/components/CurrencyInput/CurrencyInput.tsx +++ b/mon-entreprise/source/components/CurrencyInput/CurrencyInput.tsx @@ -18,12 +18,16 @@ export default function CurrencyInput({ currencySymbol = '€', onChange, language, + missing, className, + style, + dottedName, ...forwardedProps }: CurrencyInputProps) { const valueProp = value ?? '' const [initialValue, setInitialValue] = useState(valueProp) const [currentValue, setCurrentValue] = useState(valueProp) + const onChangeDebounced = useMemo( () => debounceTimeout && onChange @@ -48,7 +52,10 @@ export default function CurrencyInput({ // Only trigger the `onChange` event if the value has changed -- and not // only its formating, we don't want to call it when a dot is added in `12.` // for instance - if (!nextValue.current || /(\.$)|(^\.)|(-$)/.exec(nextValue.current)) { + if ( + !nextValue.current || + /(\.$)|(^\.)|(-$)|(^0+$)/.exec(nextValue.current) + ) { return } event.persist() @@ -68,14 +75,13 @@ export default function CurrencyInput({ // Autogrow the input const valueLength = currentValue.toString().length const width = `${5 + (valueLength - 5) * 0.75}em` - return (
5 ? { style: { width } } : {})} + style={{ ...(valueLength > 5 ? { width } : {}), ...style }} + onFocus={() => inputRef.current?.select()} onClick={() => inputRef.current?.focus()} > - {!currentValue && isCurrencyPrefixed && currencySymbol} {!isCurrencyPrefixed && <> €} diff --git a/mon-entreprise/source/components/SimulationGoals.tsx b/mon-entreprise/source/components/SimulationGoals.tsx index ed9004e18..830392bdd 100644 --- a/mon-entreprise/source/components/SimulationGoals.tsx +++ b/mon-entreprise/source/components/SimulationGoals.tsx @@ -1,11 +1,14 @@ import { updateSituation } from 'Actions/actions' +import classnames from 'classnames' import Animate from 'Components/ui/animate' import { DottedName } from 'modele-social' -import { UNSAFE_isNotApplicable } from 'publicodes' +import { formatValue, UNSAFE_isNotApplicable } from 'publicodes' import { createContext, useContext, useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import { situationSelector } from 'Selectors/simulationSelectors' import RuleInput from './conversation/RuleInput' +import RuleLink from './RuleLink' +import AnimatedTargetValue from './ui/AnimatedTargetValue' import { useEngine } from './utils/EngineContext' type SimulationGoalsProps = { @@ -13,7 +16,7 @@ type SimulationGoalsProps = { children: React.ReactNode } -const InitialRenderContext = createContext(false) +const InitialRenderContext = createContext(true) export function SimulationGoals({ className = '', @@ -44,14 +47,16 @@ type SimulationGoalProps = { dottedName: DottedName labelWithTitle?: boolean small?: boolean - titleLevel?: number + appear?: boolean + editable?: boolean } export function SimulationGoal({ dottedName, labelWithTitle = false, small = false, - titleLevel = 2, + appear = true, + editable = true, }: SimulationGoalProps) { const dispatch = useDispatch() const engine = useEngine() @@ -60,6 +65,7 @@ export function SimulationGoal({ const evaluation = engine.evaluate(dottedName) const rule = engine.getRule(dottedName) const initialRender = useContext(InitialRenderContext) + const [isFocused, setFocused] = useState(false) if ( isNotApplicable === true || (!(dottedName in situation) && @@ -68,11 +74,15 @@ export function SimulationGoal({ ) { return null } - + const displayAsInput = + dottedName in situation || + isFocused || + initialRender || + Object.keys(situation).length === 0 return (
  • - -
    + +
    + {small && }
    - dispatch(updateSituation(dottedName, x))} - useSwitch - /> + {editable ? ( + <> + setFocused(true)} + onBlur={() => setFocused(false)} + onChange={(x) => dispatch(updateSituation(dottedName, x))} + useSwitch + /> + + ) : ( + <> + + + {formatValue(evaluation, { displayedUnit: '€' })} + + + )}
    diff --git a/mon-entreprise/source/components/TargetSelection.css b/mon-entreprise/source/components/TargetSelection.css index a5227df4a..d892c9bba 100644 --- a/mon-entreprise/source/components/TargetSelection.css +++ b/mon-entreprise/source/components/TargetSelection.css @@ -24,29 +24,42 @@ #targetSelection .targets > li.small-target { border-top: none; + padding: 0.4rem 1rem; } -#targetSelection .targets > li.small-target .targetInput { + +#targetSelection .targets > li.small-target .targetInput, +#targetSelection .targets > li.small-target .editableTarget { border-width: 1px; + border-radius: 0.2rem; padding-top: 0; padding-bottom: 0; } #targetSelection .targets > li { - border-top: 1px solid rgba(255, 255, 255, 0.5); + border-top: 1px dashed rgba(255, 255, 255, 0.6); padding: 0.8rem 1rem; margin-left: -1rem; margin-right: -1rem; } - -.light #targetSelection .targets > li:not(:first-child) { +.light #targetSelection .targets > li { border-top: 1px solid rgba(0, 0, 0, 0.1); } +.light #targetSelection .targets > li.small-target, +.light #targetSelection .targets > li:not(.small-target):first-of-type { + border-top: none; +} #targetSelection .targets > li .main { display: flex; + flex-wrap: wrap; align-items: center; justify-content: space-between; } +#targetSelection .targets > li .guide-lecture { + flex: 1; + border-bottom: 1px dashed rgba(255, 255, 255, 0.25); + margin-left: 1rem; +} #targetSelection li .header { display: flex; @@ -94,7 +107,7 @@ #targetSelection .targetInputOrValue { font-size: 115%; - margin-left: 0.6rem; + min-width: 5.5rem; text-align: right; display: flex; flex-direction: column; @@ -110,26 +123,26 @@ text-decoration: none; } -#targetSelection .targetInputOrValue > :not(.targetInput) { - margin: 0 0.6rem; -} - #targetSelection input { margin: 2.7px 0; } #targetSelection .targetInput { width: 5.5em; + margin-left: 1rem; max-width: 7.5em; text-align: right; background: rgba(255, 255, 255, 0.2); - padding: 0; padding: 0.2rem 0.6rem; border-radius: 0.3rem; - border: 2px solid; + border: 1px solid; font-size: inherit; } +#targetSelection .targetInput.focused { + box-shadow: 0 0 0px 1px white; +} + .light #targetSelection .targetInput { color: var(--darkColor); border-color: var(--darkColor); @@ -142,23 +155,19 @@ #targetSelection .editableTarget { max-width: 7.5em; + width: 5.5em; + display: inline-block; text-align: right; - padding: 0 2px; + padding: 0.2rem 0.6rem; + + background: transparent; + border: 1px dashed rgba(255, 255, 255, 0.5); + border-radius: 0.3rem; + font-size: inherit; } -#targetSelection .targetInputBottomBorder { - margin: 0; - padding: 0 2px; - height: 0; - overflow: hidden; -} - -#targetSelection .editableTarget + .targetInputBottomBorder { - border-bottom: 1px dashed #ffffff91; -} - #targetSelection .unit { margin-left: 0.4em; font-size: 110%; diff --git a/mon-entreprise/source/components/TargetSelection.tsx b/mon-entreprise/source/components/TargetSelection.tsx index 6e0382672..c2854ebda 100644 --- a/mon-entreprise/source/components/TargetSelection.tsx +++ b/mon-entreprise/source/components/TargetSelection.tsx @@ -1,5 +1,5 @@ -import { setActiveTarget, updateSituation } from 'Actions/actions' -import InputSuggestions from 'Components/conversation/InputSuggestions' +import { updateSituation } from 'Actions/actions' +import classnames from 'classnames' import Value, { Condition } from 'Components/EngineValue' import PeriodSwitch from 'Components/PeriodSwitch' import RuleLink from 'Components/RuleLink' @@ -21,18 +21,17 @@ import { reduceAST, RuleNode, } from 'publicodes' -import { isNil } from 'ramda' -import { Fragment, useCallback, useContext, useEffect, useState } from 'react' +import { Fragment, useCallback, useContext, useState } from 'react' import emoji from 'react-easy-emoji' import { Trans, useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' import { useLocation } from 'react-router-dom' import { RootState } from 'Reducers/rootReducer' import { - firstStepCompletedSelector, situationSelector, targetUnitSelector, } from 'Selectors/simulationSelectors' +import InputSuggestions from './conversation/InputSuggestions' import CurrencyInput from './CurrencyInput/CurrencyInput' import './TargetSelection.css' @@ -102,19 +101,7 @@ const Target = ({ dottedName }: TargetProps) => { unité: useSelector(targetUnitSelector), arrondi: 'oui', }) - const situation = useSelector(situationSelector) - const target: TargetType = { ...evaluation, ...rule.rawNode, ...rule } - const dispatch = useDispatch() - const onSuggestionClick = useCallback( - (value) => { - dispatch(updateSituation(dottedName, value)) - }, - [target.dottedName, dispatch] - ) - const isActive = - dottedName in situation || Object.keys(situation).length === 0 - const isSmallTarget = !rule.rawNode.question || (dottedName in evaluation.missingVariables && @@ -134,34 +121,14 @@ const Target = ({ dottedName }: TargetProps) => {
    - {isSmallTarget && ( - - )} - + {isSmallTarget && }
    - {isActive && ( - -
    - -
    -
    - )}
  • @@ -193,30 +160,35 @@ const Header = ({ target }: { target: TargetType }) => { type TargetInputOrValueProps = { target: TargetType - isActive: boolean isSmallTarget: boolean } function TargetInputOrValue({ target, - isActive, isSmallTarget, }: TargetInputOrValueProps) { const { language } = useTranslation().i18n const colors = useContext(ThemeColorsContext) const dispatch = useDispatch() - const [isActiveOrFocused, setActive] = useState(isActive) - useEffect(() => setActive(isActive), [isActive]) + const [isFocused, setFocused] = useState(false) const targetUnit = useSelector(targetUnitSelector) const engine = useContext(EngineContext) + const situation = useSelector(situationSelector) const value = (engine.evaluate({ valeur: target.dottedName, unité: targetUnit, arrondi: 'oui', }).nodeValue as number) ?? undefined - const blurValue = useInversionFail() && !isActiveOrFocused - + const blurValue = useInversionFail() && !isFocused + const onSuggestionClick = useCallback( + (value) => { + dispatch(updateSituation(target.dottedName, value)) + }, + [target.dottedName, dispatch] + ) + const isSituationEmpty = Object.keys(situation).length === 0 + const isActive = target.dottedName in situation const onChange = useCallback( (evt) => dispatch( @@ -228,54 +200,68 @@ function TargetInputOrValue({ [targetUnit, target, dispatch] ) return ( - - {target.question ? ( - <> - {!isActiveOrFocused && } - { - setActive(true) - }} - language={language} - /> - - {formatValue(value, { language, displayedUnit: '€' })} + <> + + {target.question ? ( + <> + {!isFocused && } + { + setFocused(true) + }} + onBlur={() => setTimeout(() => setFocused(false), 100)} + language={language} + /> + + ) : ( + + {value && Number.isNaN(value) ? ( + '—' + ) : ( + + {formatValue(value, { displayedUnit: '€', language })} + + )} - - ) : ( - - {value && Number.isNaN(value) ? ( - '—' - ) : ( - - {formatValue(value, { displayedUnit: '€', language })} - - )} - + )} + {target.dottedName.includes('prix du travail') && } + {target.dottedName === 'contrat salarié . rémunération . net' && ( + + )} + + {(isActive || isFocused) && ( +
    + +
    + +
    +
    +
    )} - {target.dottedName.includes('prix du travail') && } - {target.dottedName === 'contrat salarié . rémunération . net' && ( - - )} -
    + ) } function TitreRestaurant() { diff --git a/mon-entreprise/source/components/conversation/Conversation.tsx b/mon-entreprise/source/components/conversation/Conversation.tsx index f053d08a8..c4845f38a 100644 --- a/mon-entreprise/source/components/conversation/Conversation.tsx +++ b/mon-entreprise/source/components/conversation/Conversation.tsx @@ -1,5 +1,5 @@ -import { stepAction, goToQuestion, updateSituation } from 'Actions/actions' -import RuleInput, { RuleInputProps } from 'Components/conversation/RuleInput' +import { goToQuestion, stepAction, updateSituation } from 'Actions/actions' +import RuleInput, { InputProps } from 'Components/conversation/RuleInput' import QuickLinks from 'Components/QuickLinks' import * as Animate from 'Components/ui/animate' import { EngineContext } from 'Components/utils/EngineContext' @@ -41,7 +41,7 @@ export default function Conversation({ customEndMessages }: ConversationProps) { dispatch(stepAction(currentQuestion, source)) } - const onChange: RuleInputProps['onChange'] = (value) => { + const onChange: InputProps['onChange'] = (value) => { dispatch(updateSituation(currentQuestion, value)) } diff --git a/mon-entreprise/source/components/conversation/DateInput.tsx b/mon-entreprise/source/components/conversation/DateInput.tsx index 4c9bd3ee9..99ad934dd 100644 --- a/mon-entreprise/source/components/conversation/DateInput.tsx +++ b/mon-entreprise/source/components/conversation/DateInput.tsx @@ -1,21 +1,8 @@ -import { - InputCommonProps, - RuleInputProps, -} from 'Components/conversation/RuleInput' -import { RuleNode } from 'publicodes' +import { InputProps } from 'Components/conversation/RuleInput' import { useCallback, useMemo } from 'react' import styled from 'styled-components' import InputSuggestions from './InputSuggestions' -type DateInputProps = { - onChange: InputCommonProps['onChange'] - id: InputCommonProps['id'] - onSubmit: RuleInputProps['onSubmit'] - value: InputCommonProps['value'] - suggestions: RuleNode['suggestions'] - required: RuleInputProps['required'] -} - export default function DateInput({ suggestions, onChange, @@ -23,7 +10,7 @@ export default function DateInput({ onSubmit, required, value, -}: DateInputProps) { +}: InputProps) { const dateValue = useMemo(() => { if (!value || typeof value !== 'string') return undefined const [day, month, year] = value.split('/') diff --git a/mon-entreprise/source/components/conversation/RuleInput.tsx b/mon-entreprise/source/components/conversation/RuleInput.tsx index a9032e76b..c59d6ec7f 100644 --- a/mon-entreprise/source/components/conversation/RuleInput.tsx +++ b/mon-entreprise/source/components/conversation/RuleInput.tsx @@ -8,7 +8,7 @@ import ToggleSwitch from 'Components/ui/ToggleSwitch' import { EngineContext } from 'Components/utils/EngineContext' import { DottedName } from 'modele-social' import Engine, { ASTNode, formatValue, reduceAST } from 'publicodes' -import { Evaluation } from 'publicodes/dist/types/AST/types' +import { EvaluatedNode, Evaluation } from 'publicodes/dist/types/AST/types' import { RuleNode } from 'publicodes/dist/types/rule' import React, { useContext } from 'react' import { useTranslation } from 'react-i18next' @@ -17,30 +17,23 @@ import ParagrapheInput from './ParagrapheInput' import SelectEuropeCountry from './select/SelectEuropeCountry' import TextInput from './TextInput' -type Value = any -export type RuleInputProps = { +export type Props = Omit< + React.HTMLAttributes, + 'onChange' | 'defaultValue' +> & { + required?: boolean dottedName: Name - onChange: (value: Value | null) => void + onChange: (value: Parameters['evaluate']>[0]) => void useSwitch?: boolean isTarget?: boolean - autoFocus?: boolean - required?: boolean - id?: string - className?: string onSubmit?: (source: string) => void } -export type InputCommonProps = Pick< - RuleInputProps, - 'onChange' | 'autoFocus' | 'className' -> & +export type InputProps = Props & Pick & { question: RuleNode['rawNode']['question'] - key: string - id: string - value: any //TODO EvaluatedRule['nodeValue'] + value: EvaluatedNode['nodeValue'] missing: boolean - required: boolean } export const binaryQuestion = [ @@ -56,30 +49,25 @@ export default function RuleInput({ dottedName, onChange, useSwitch = false, - id, isTarget = false, - autoFocus = false, - required = true, - className, onSubmit = () => null, -}: RuleInputProps) { + ...props +}: Props) { const engine = useContext(EngineContext) const rule = engine.getRule(dottedName) const evaluation = engine.evaluate(dottedName) const language = useTranslation().i18n.language const value = evaluation.nodeValue - const commonProps: InputCommonProps = { - key: dottedName, + const commonProps: InputProps = { + dottedName, value, missing: !!evaluation.missingVariables[dottedName], onChange, - autoFocus, - className, - required, title: rule.title, - id: id ?? dottedName, + id: props.id ?? dottedName, question: rule.rawNode.question, suggestions: rule.suggestions, + ...props, } if (getVariant(engine.getRule(dottedName))) { return ( @@ -149,12 +137,12 @@ export default function RuleInput({ return ( <> onChange({ valeur: evt.target.value, unité })} /> diff --git a/mon-entreprise/source/pages/Simulateurs/AutoEntrepreneur.tsx b/mon-entreprise/source/pages/Simulateurs/AutoEntrepreneur.tsx index 90c6b6c0c..5b6b5127d 100644 --- a/mon-entreprise/source/pages/Simulateurs/AutoEntrepreneur.tsx +++ b/mon-entreprise/source/pages/Simulateurs/AutoEntrepreneur.tsx @@ -1,6 +1,7 @@ import { updateSituation } from 'Actions/actions' +import Conversation from 'Components/conversation/Conversation' import { Condition } from 'Components/EngineValue' -import RuleLink from 'Components/RuleLink' +import SearchButton from 'Components/SearchButton' import SimulateurWarning from 'Components/SimulateurWarning' import { SimulationGoal, SimulationGoals } from 'Components/SimulationGoals' import StackedBarChart from 'Components/StackedBarChart' @@ -12,41 +13,47 @@ import { useDispatch } from 'react-redux' import AidesCovid from '../../components/simulationExplanation/AidesCovid' export default function AutoEntrepreneur() { - const dispatch = useDispatch() - return ( <> - - + + + -

    - -

    - - - + +
  • +
      + + + +
    +
  • + ) @@ -67,22 +75,37 @@ function ActivitéMixte() { const defaultCheked = !!useEngine().evaluate('entreprise . activité . mixte') .nodeValue const dispatch = useDispatch() + return ( -