Merge pull request #856 from betagouv/revert-816-ui-indeps

Revert "Aide à la déclaration des indépendants"
pull/857/head
Johan Girod 2020-01-24 18:07:32 +01:00 committed by GitHub
commit 00187921a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 572 additions and 668 deletions

View File

@ -8,7 +8,7 @@ import './CurrencyInput.css'
type CurrencyInputProps = NumberFormatProps & {
value?: string | number
debounce?: number
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void
currencySymbol?: string
language?: Parameters<typeof currencyFormat>[0]
}
@ -18,7 +18,6 @@ export default function CurrencyInput({
debounce: debounceTimeout,
currencySymbol = '€',
onChange,
onSubmit,
language,
className,
...forwardedProps

View File

@ -180,15 +180,13 @@ const Target = ({ target, initialRender }) => {
</div>
{isActiveInput && (
<Animate.fromTop>
<div css="display: flex; justify-content: flex-end">
<InputSuggestions
suggestions={target.suggestions}
onFirstClick={value => {
dispatch(updateSituation(target.dottedName, value))
}}
unit={target.defaultUnit}
/>
</div>
<InputSuggestions
suggestions={target.suggestions}
onFirstClick={value => {
dispatch(updateSituation(target.dottedName, value))
}}
unit={target.defaultUnit}
/>
</Animate.fromTop>
)}
</div>

View File

@ -1,21 +1,16 @@
import { goToQuestion, validateStepWithValue } from 'Actions/actions'
import QuickLinks from 'Components/QuickLinks'
import InputComponent from 'Engine/RuleInput'
import getInputComponent from 'Engine/getInputComponent'
import { findRuleByDottedName } from 'Engine/rules'
import React from 'react'
import emoji from 'react-easy-emoji'
import { Trans } from 'react-i18next'
import { useDispatch, useSelector } from 'react-redux'
import { RootState } from 'Reducers/rootReducer'
import {
currentQuestionSelector,
flatRulesSelector,
nextStepsSelector
} from 'Selectors/analyseSelectors'
import { currentQuestionSelector, flatRulesSelector, nextStepsSelector } from 'Selectors/analyseSelectors'
import * as Animate from 'Ui/animate'
import Aide from './Aide'
import './conversation.css'
import FormDecorator from './FormDecorator'
export type ConversationProps = {
customEndMessages?: React.ReactNode
@ -44,16 +39,15 @@ export default function Conversation({ customEndMessages }: ConversationProps) {
setDefault()
}
}
const DecoratedInputComponent = FormDecorator(InputComponent)
return flatRules && nextSteps.length ? (
return nextSteps.length ? (
<>
<Aide />
<div tabIndex={0} style={{ outline: 'none' }} onKeyDown={handleKeyDown}>
{currentQuestion && (
<React.Fragment key={currentQuestion}>
<Animate.fadeIn>
<DecoratedInputComponent dottedName={currentQuestion} />
{getInputComponent(flatRules)(currentQuestion)}
</Animate.fadeIn>
<div className="ui__ answer-group">
{previousAnswers.length > 0 && (

View File

@ -1,3 +1,4 @@
import { FormDecorator } from 'Components/conversation/FormDecorator'
import { normalizeDate, normalizeDateString } from 'Engine/date'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -17,7 +18,12 @@ const DateField = styled.input`
`}
`
export default function DateInput({ suggestions, onChange, onSubmit, value }) {
export default FormDecorator('input')(function DateInput({
suggestions,
setFormValue,
submit,
value
}) {
const { language } = useTranslation().i18n
// Refs for focus handling
@ -47,7 +53,7 @@ export default function DateInput({ suggestions, onChange, onSubmit, value }) {
if (!normalizedDate) {
return
}
onChange(normalizedDate)
setFormValue(normalizedDate)
}, [normalizedDate])
// If value change, replace state
@ -65,9 +71,9 @@ export default function DateInput({ suggestions, onChange, onSubmit, value }) {
<InputSuggestions
suggestions={suggestions}
onFirstClick={value => {
onChange(normalizeDateString(value as string))
setFormValue(normalizeDateString(value as string))
}}
onSecondClick={() => onSubmit('suggestion')}
onSecondClick={() => submit('suggestion')}
/>
</div>
@ -118,10 +124,8 @@ export default function DateInput({ suggestions, onChange, onSubmit, value }) {
value={date.year}
/>
</div>
{onSubmit && (
<SendButton disabled={!normalizedDate} onSubmit={onSubmit} />
)}
<SendButton {...{ disabled: !normalizedDate, submit }} />
</div>
</>
)
}
})

View File

@ -1,13 +1,11 @@
import { updateSituation } from 'Actions/actions'
import classNames from 'classnames'
import Explicable from 'Components/conversation/Explicable'
import { findRuleByDottedName } from 'Engine/rules'
import { serializeUnit } from 'Engine/units'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { useDispatch, useSelector } from 'react-redux'
import {
flatRulesSelector,
situationSelector
} from 'Selectors/analyseSelectors'
import { situationSelector } from 'Selectors/analyseSelectors'
/*
This higher order component wraps "Form" components (e.g. Question.js), that represent user inputs,
@ -17,41 +15,40 @@ Read https://medium.com/@dan_abramov/mixins-are-dead-long-live-higher-order-comp
to understand those precious higher order components.
*/
export default function FormDecorator(RenderField) {
return function FormStep({ dottedName }) {
export const FormDecorator = formType => RenderField =>
function FormStep({ fieldName, question, inversion, unit, ...otherProps }) {
const dispatch = useDispatch()
const situation = useSelector(situationSelector)
const flatRules = useSelector(flatRulesSelector)
const language = useTranslation().i18n.language
const submit = source =>
dispatch({
type: 'STEP_ACTION',
name: 'fold',
step: dottedName,
step: fieldName,
source
})
const setFormValue = value => {
dispatch(updateSituation(dottedName, value))
dispatch(updateSituation(fieldName, value))
}
return (
<div className="step">
<h3>
{findRuleByDottedName(flatRules, dottedName).question}{' '}
<Explicable dottedName={dottedName} />
</h3>
<div className={classNames('step', formType)}>
<div className="unfoldedHeader">
<h3>
{question} {!inversion && <Explicable dottedName={fieldName} />}
</h3>
</div>
<fieldset>
<RenderField
dottedName={dottedName}
value={situation[dottedName]}
onChange={setFormValue}
onSubmit={submit}
rules={flatRules}
name={fieldName}
value={situation[fieldName]}
setFormValue={setFormValue}
submit={submit}
unit={serializeUnit(unit, situation[fieldName], language)}
{...otherProps}
/>
</fieldset>
</div>
)
}
}

View File

@ -1,36 +1,37 @@
import { ThemeColorsContext } from 'Components/utils/colors'
import { currencyFormat } from 'Engine/format'
import { serializeUnit } from 'Engine/units'
import { compose } from 'ramda'
import React, { useCallback, useContext } from 'react'
import { useTranslation } from 'react-i18next'
import NumberFormat from 'react-number-format'
import { debounce } from '../../utils'
import { FormDecorator } from './FormDecorator'
import InputSuggestions from './InputSuggestions'
import SendButton from './SendButton'
// TODO: fusionner Input.js et CurrencyInput.js
export default function Input({
export default compose(FormDecorator('input'))(function Input({
suggestions,
onChange,
onSubmit,
setFormValue,
submit,
dottedName,
value,
unit
}) {
const colors = useContext(ThemeColorsContext)
const debouncedOnChange = useCallback(debounce(750, onChange), [])
const debouncedSetFormValue = useCallback(debounce(750, setFormValue), [])
const { language } = useTranslation().i18n
const { thousandSeparator, decimalSeparator } = currencyFormat(language)
return (
<div className="step input">
<>
<div css="width: 100%">
<InputSuggestions
suggestions={suggestions}
onFirstClick={value => {
onChange(value)
setFormValue(value)
}}
onSecondClick={() => onSubmit && onSubmit('suggestion')}
onSecondClick={() => submit('suggestion')}
/>
</div>
@ -44,18 +45,16 @@ export default function Input({
allowEmptyFormatting={true}
style={{ border: `1px solid ${colors.textColorOnWhite}` }}
onValueChange={({ floatValue }) => {
debouncedOnChange(floatValue)
debouncedSetFormValue(floatValue)
}}
value={value}
autoComplete="off"
/>
<label className="suffix" htmlFor={'step-' + dottedName}>
{serializeUnit(unit, value, language)}
{unit}
</label>
{onSubmit && (
<SendButton disabled={value === undefined} onSubmit={onSubmit} />
)}
<SendButton {...{ disabled: value === undefined, submit }} />
</div>
</div>
</>
)
}
})

View File

@ -24,7 +24,7 @@ export default function InputSuggestions({
if (!suggestions) return null
return (
<div css="display: flex; align-items: baseline; ">
<div css="display: flex; align-items: baseline; justify-content: flex-end;">
<small>Suggestions :</small>
{toPairs(suggestions).map(([text, value]: [string, number]) => {

View File

@ -0,0 +1,5 @@
.binaryQuestionList {
display: flex;
align-items: center;
justify-content: flex-end;
}

View File

@ -1,9 +1,11 @@
import classnames from 'classnames'
import { ThemeColorsContext } from 'Components/utils/colors'
import { is } from 'ramda'
import React, { useCallback, useContext } from 'react'
import { compose, is } from 'ramda'
import React, { useCallback, useContext, useState } from 'react'
import { Trans } from 'react-i18next'
import Explicable from './Explicable'
import { FormDecorator } from './FormDecorator'
import './Question.css'
import SendButton from './SendButton'
/* Ceci est une saisie de type "radio" : l'utilisateur choisit une réponse dans une liste, ou une liste de listes.
@ -22,42 +24,42 @@ import SendButton from './SendButton'
*/
export default function Question({
// FormDecorator permet de factoriser du code partagé par les différents types de saisie,
// dont Question est un example
export default compose(FormDecorator('question'))(function Question({
choices,
onSubmit,
dottedName,
onChange,
submit,
name,
setFormValue,
value: currentValue
}) {
const colors = useContext(ThemeColorsContext)
const handleChange = useCallback(
const [touched, setTouched] = useState(false)
const onChange = useCallback(
value => {
onChange(value)
setFormValue(value)
setTouched(true)
},
[onChange]
[setFormValue]
)
const renderBinaryQuestion = () => {
return choices.map(({ value, label }) => (
<RadioLabel
key={value}
{...{
value,
css: 'margin-right: 0.6rem',
label,
currentValue,
onSubmit,
colors,
onChange: handleChange
}}
/>
))
return (
<div className="binaryQuestionList">
{choices.map(({ value, label }) => (
<RadioLabel
key={value}
{...{ value, label, currentValue, submit, colors, onChange }}
/>
))}
</div>
)
}
const renderChildren = choices => {
// seront stockées ainsi dans le state :
// [parent object path]: dotted fieldName relative to parent
// [parent object path]: dotted name relative to parent
const relativeDottedName = radioDottedName =>
radioDottedName.split(dottedName + ' . ')[1]
radioDottedName.split(name + ' . ')[1]
return (
<ul css="width: 100%">
@ -68,32 +70,32 @@ export default function Question({
value: 'non',
label: 'Aucun',
currentValue,
onSubmit,
submit,
colors,
dottedName: null,
onChange: handleChange
onChange
}}
/>
</li>
)}
{choices.children &&
choices.children.map(({ fieldName, title, dottedName, children }) =>
choices.children.map(({ name, title, dottedName, children }) =>
children ? (
<li key={fieldName} className="variant">
<li key={name} className="variant">
<div>{title}</div>
{renderChildren({ children })}
</li>
) : (
<li key={fieldName} className="variantLeaf">
<li key={name} className="variantLeaf">
<RadioLabel
{...{
value: relativeDottedName(dottedName),
label: title,
dottedName,
currentValue,
onSubmit,
submit,
colors,
onChange: handleChange
onChange
}}
/>
</li>
@ -108,46 +110,40 @@ export default function Question({
: renderChildren(choices)
return (
<div
className="step question"
css="margin-top: 0.6rem; display: flex; align-items: center; flex-wrap: wrap;"
>
<div css="margin-top: 0.6rem; display: flex; align-items: center; flex-wrap: wrap; justify-content: flex-end">
{choiceElements}
{onSubmit && <SendButton disabled={!currentValue} onSubmit={onSubmit} />}
<SendButton
{...{
disabled: !touched,
colors,
error: false,
submit
}}
/>
</div>
)
}
})
export let RadioLabel = props => (
let RadioLabel = props => (
<>
<RadioLabelContent {...props} />
<Explicable dottedName={props.dottedName} />
</>
)
function RadioLabelContent({
value,
label,
currentValue,
onChange,
onSubmit,
css
}) {
function RadioLabelContent({ value, label, currentValue, onChange, submit }) {
let labelStyle = value === '_' ? { fontWeight: 'bold' } : null,
selected = value === currentValue
const click = value => () => {
if (currentValue == value && onSubmit) onSubmit('dblClick')
if (currentValue == value) submit('dblClick')
}
return (
<label
key={value}
style={labelStyle}
css={css}
className={classnames('radio', 'userAnswerButton', 'ui__', 'button', {
selected
})}
className={classnames('radio', 'userAnswerButton', { selected })}
>
<Trans>{label}</Trans>
<input

View File

@ -0,0 +1,24 @@
import FormDecorator from 'Components/conversation/FormDecorator'
import React from 'react'
import { useTranslation } from 'react-i18next'
export default FormDecorator('rhetorical-question')(
function RhetoricalQuestion({ value: currentValue, submit, possibleChoice }) {
const { t } = useTranslation()
if (!possibleChoice) return null // No action possible, don't render an answer
let { text, value } = possibleChoice
return (
<span className="answer">
<label key={value} className="radio userAnswerButton">
<input
type="radio"
checked={value === currentValue}
onClick={submit}
value={value}
/>
{t(text)}
</label>
</span>
)
}
)

View File

@ -3,13 +3,13 @@ import { Trans } from 'react-i18next'
type SendButtonProps = {
disabled: boolean
onSubmit: (cause: string) => void
submit: (cause: string) => void
}
export default function SendButton({ disabled, onSubmit }: SendButtonProps) {
const getAction = useCallback(cause => (!disabled ? onSubmit(cause) : null), [
export default function SendButton({ disabled, submit }: SendButtonProps) {
const getAction = useCallback(cause => (!disabled ? submit(cause) : null), [
disabled,
onSubmit
submit
])
useEffect(() => {
const handleKeyDown = ({ key }: KeyboardEvent) => {
@ -25,7 +25,7 @@ export default function SendButton({ disabled, onSubmit }: SendButtonProps) {
return (
<button
className="ui__ plain button "
className="ui__ button plain"
css="margin-left: 1.2rem"
disabled={disabled}
onClick={() => getAction('accept')}

View File

@ -1,3 +1,86 @@
.scrollIndication {
margin: 0.6em 0;
font-size: 110%;
}
.scrollIndication.down {
margin-bottom: 1em;
}
#resultsScrollElement,
#myScrollToElement {
text-align: center;
}
#foldedSteps {
padding: 1em 0;
margin-bottom: 1em;
}
#foldedSteps .header button {
display: block;
margin: 0 auto 1em;
}
#foldedSteps button i {
margin-right: 0.3em;
font-size: 110%;
vertical-align: top;
}
#foldedSteps button {
border: none;
font-weight: 500;
}
#myScrollToElement {
padding-top: 0.3em;
}
.step {
position: relative;
}
.step {
opacity: 1;
}
.step:first-child {
opacity: 1;
}
.unfoldedHeader {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.step.completed .edit:hover {
background: #333350;
color: white;
}
/* Our little help icon */
.help-button {
display: inline-block;
margin-top: 0.5em;
line-height: 1.1em;
border-radius: 1em;
font-size: 90%;
color: #777;
border: 1px solid;
background: none;
text-align: center;
cursor: pointer;
color: #aaa;
text-transform: uppercase;
font-size: 60%;
padding: 0.25em 0.6em;
}
.help-button:hover {
color: #333;
border: 1px solid #333;
}
.step fieldset {
display: flex;
justify-content: flex-end;
@ -9,9 +92,8 @@
list-style-type: none;
}
.step fieldset .step.question .variantLeaf,
.step fieldset .step.question {
justify-content: flex-end;
.step fieldset > ul:not(.binaryQuestionList) {
width: 100%;
}
.step.question .variant {
@ -37,6 +119,7 @@
}
.step.question .variantLeaf {
display: flex;
justify-content: flex-end;
margin-bottom: 0.6em;
}
@ -51,6 +134,8 @@
.step label.radio {
text-align: center;
margin-left: 1rem;
/* margin-top: 1em; */
cursor: pointer;
background: none;
border-radius: 1em;
@ -59,6 +144,17 @@
font-size: 120%;
}
.resume {
transition: 1s display;
}
.answer-ignored {
font-size: 80%;
opacity: 0.8;
margin-left: 0.4em;
vertical-align: middle;
}
.step.question input[type='radio'] {
display: none;
}
@ -75,7 +171,7 @@
font-size: 120%;
padding: 0;
padding-right: 0.4em;
width: 10rem;
width: 8em;
text-align: right;
padding-left: 0.2em;
}
@ -95,6 +191,7 @@
}
.step input.suffixed {
width: 5rem;
margin: 0.6rem 0;
border-radius: 0.2em;
}
@ -110,25 +207,176 @@
outline: none;
}
.help-box {
position: absolute;
width: 100%;
height: 100%;
z-index: 1;
text-align: center;
}
.help-box p {
padding: 1em;
font-size: 90%;
font-style: italic;
}
.close-help {
cursor: pointer;
position: absolute;
top: 0;
right: 0;
font-size: 105%;
}
.close-text {
text-transform: uppercase;
font-size: 60%;
}
.close-text .icon {
font-size: 150%;
vertical-align: middle;
}
.step .send {
padding: 0.1em 0.4em 0em 1em;
background: none;
cursor: pointer;
border: 1px solid;
border-radius: 0.4em;
line-height: 0em;
}
.step .send:disabled {
opacity: 0.2;
}
.step .send i {
margin: 0 0.3em;
font-size: 160%;
}
.step .send .text {
text-transform: uppercase;
font-size: 135%;
line-height: 2em;
}
.answer {
margin-top: 0.6rem;
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
align-items: center;
}
.foldedQuestion .answer {
float: right;
}
.step-input-error {
position: absolute;
right: 0;
bottom: -1.5em;
font-size: 0.8em;
font-style: italic;
color: #c0392b;
font-weight: 600;
}
.step textarea {
vertical-align: middle;
margin-right: 1em;
}
#share-link {
color: white;
padding: 0.3em 0.3em;
display: inline-block;
margin-top: 0.3em;
border-radius: 0.25em;
}
#share-icon {
font-size: 200%;
vertical-align: middle;
line-height: 0em;
margin-left: 0.3em;
}
.info-zone {
font-size: 65%;
text-align: center;
font-style: italic;
color: #666;
line-height: 1.6em;
}
.input-tip {
height: 2em;
}
.input-tip p {
margin: 0.1em;
}
#show-advanced {
font-weight: bold;
}
/* Positioning the animated elements absolutely + transition-delay will make it possible
for the appearing element to appear without stacking up below the first one */
.input-tip {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
margin: auto;
}
.foldedQuestion {
margin-left: 0em;
}
.foldedQuestion .edit {
vertical-align: middle;
margin-left: 0.5em;
border: none;
padding-right: 0;
}
.foldedQuestion .borderWrapper {
padding: 0.1em 0;
display: inline-block;
width: calc(100% - 8em);
border-bottom: 1px solid #eee;
}
.foldedQuestion:last-of-type .borderWrapper {
border-bottom: none;
}
.conversationContainer {
flex: 1;
padding-bottom: 1rem;
margin-bottom: 1rem;
}
.step label.userAnswerButton {
border: 1px solid var(--color) !important;
text-transform: none !important;
border: 1px solid var(--color);
background-color: white;
color: var(--textColorOnWhite);
}
.step label.userAnswerButton.selected {
background: var(--color);
border: 1px solid var(--color);
color: var(--textColor);
}
@media (hover) {
.step label.userAnswerButton:hover:not(.selected) {
.step label.userAnswerButton:hover {
background: var(--color);
border: 1px solid var(--color);
color: var(--textColor);
transition: all 0.05s;
}
}

View File

@ -1,6 +1,7 @@
import React, { useCallback, useMemo, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { debounce } from '../../../utils'
import { FormDecorator } from '../FormDecorator'
async function tauxVersementTransport(codeCommune) {
const response = await fetch(
@ -25,7 +26,10 @@ async function searchCommunes(input) {
return json
}
export default function Select({ onChange, onSubmit }) {
export default FormDecorator('select')(function Select({
setFormValue,
submit
}) {
const [searchResults, setSearchResults] = useState()
const [isLoading, setLoadingState] = useState(false)
@ -47,7 +51,7 @@ export default function Select({ onChange, onSubmit }) {
tauxVersementTransport(option.code)
.then(({ taux }) => {
// serialize to not mix our data schema and the API response's
onChange(
setFormValue(
JSON.stringify({
...option,
...(taux != undefined
@ -57,15 +61,15 @@ export default function Select({ onChange, onSubmit }) {
: {})
})
)
onSubmit()
submit()
})
.catch(error => {
//eslint-disable-next-line no-console
console.log(
'Erreur dans la récupération du taux de versement transport à partir du code commune',
error
) || onChange(JSON.stringify({ option }))
onSubmit() // eslint-disable-line no-console
) || setFormValue(JSON.stringify({ option }))
submit() // eslint-disable-line no-console
})
}
@ -135,4 +139,4 @@ export default function Select({ onChange, onSubmit }) {
})}
</>
)
}
})

View File

@ -1,14 +1,15 @@
import React, { useEffect, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import Worker from 'worker-loader!./SelectTauxRisque.worker.js'
import { FormDecorator } from '../FormDecorator'
const worker = new Worker()
function SelectComponent({ onChange, onSubmit, options }) {
function SelectComponent({ setFormValue, submit, options }) {
const [searchResults, setSearchResults] = useState()
let submitOnChange = option => {
option.text = +option['Taux net'].replace(',', '.')
onChange(option.text)
onSubmit()
setFormValue(option.text)
submit()
}
const { t } = useTranslation()
useEffect(() => {
@ -130,7 +131,7 @@ function SelectComponent({ onChange, onSubmit, options }) {
)
}
export default function Select(props) {
export default FormDecorator('select')(function Select(props) {
const [options, setOptions] = useState(null)
useEffect(() => {
fetch(
@ -153,4 +154,4 @@ export default function Select(props) {
if (!options) return null
return <SelectComponent {...props} options={options} />
}
})

View File

@ -1,4 +1,5 @@
situation:
période: année
dirigeant: artiste-auteur
unités par défaut: [€/an]
objectifs:

View File

@ -69,13 +69,6 @@
color: rgba(255, 255, 255, 0.8);
}
.ui__.card.plain .targetInput {
border-color: white;
border-color: var(--textColor);
color: white;
color: var(--textColor);
}
.ui__.interactive.card {
user-select: text;
-webkit-user-drag: none;

View File

@ -1,136 +0,0 @@
import Input from 'Components/conversation/Input'
import Question from 'Components/conversation/Question'
import SelectGéo from 'Components/conversation/select/SelectGéo'
import SelectAtmp from 'Components/conversation/select/SelectTauxRisque'
import SendButton from 'Components/conversation/SendButton'
import CurrencyInput from 'Components/CurrencyInput/CurrencyInput'
import ToggleSwitch from 'Components/ui/ToggleSwitch'
import { is, prop, unless } from 'ramda'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { DottedName, Rule } from 'Types/rule'
import DateInput from '../components/conversation/DateInput'
import { findRuleByDottedName, queryRule } from './rules'
export const binaryOptionChoices = [
{ value: 'non', label: 'Non' },
{ value: 'oui', label: 'Oui' }
]
type Value = string | number | object | boolean
type Props = {
rules: Array<Rule>
dottedName: DottedName
onChange: (value: Value) => void
useSwitch?: boolean
isTarget?: boolean
value?: Value
className?: string
onSubmit?: (value: Value) => void
}
// This function takes the unknown rule and finds which React component should
// be displayed to get a user input through successive if statements
// That's not great, but we won't invest more time until we have more diverse
// input components and a better type system.
export default function InputComponent({
rules,
dottedName,
onChange,
value,
useSwitch = false,
isTarget = false,
className,
onSubmit
}: Props) {
let rule = findRuleByDottedName(rules, dottedName)
let unit = rule.unit || rule.defaultUnit
let language = useTranslation().i18n.language
let commonProps = {
key: dottedName,
dottedName,
value,
onChange,
onSubmit,
className,
title: rule.title,
question: rule.question,
defaultValue: rule.defaultValue,
suggestions: rule.suggestions
}
if (getVariant(rule)) {
return (
<Question
{...commonProps}
choices={buildVariantTree(rules, dottedName)}
/>
)
}
if (rule.API && rule.API === 'géo')
return <SelectGéo {...{ ...commonProps }} />
if (rule.API) throw new Error("Le seul API implémenté est l'API géo")
if (rule.dottedName == 'contrat salarié . ATMP . taux collectif ATMP')
return <SelectAtmp {...commonProps} />
if (rule.type === 'date') {
return <DateInput {...commonProps} />
}
if (unit == null) {
return useSwitch ? (
<ToggleSwitch
defaultChecked={value === 'oui' || rule.defaultValue === 'oui'}
onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
onChange(evt.target.checked ? 'oui' : 'non')
}
/>
) : (
<Question {...commonProps} choices={binaryOptionChoices} />
)
}
if (unit?.numerators.includes('€') && isTarget && typeof value === 'number') {
return (
<>
<CurrencyInput
{...commonProps}
language={language}
debounce={600}
value={value}
name={dottedName}
className="targetInput"
onChange={evt => onChange(evt.target.value)}
/>
{onSubmit && <SendButton disabled={!value} onSubmit={onSubmit} />}
</>
)
}
return <Input {...commonProps} unit={unit} />
}
let getVariant = rule => queryRule(rule)('formule . une possibilité')
export let buildVariantTree = (allRules, path) => {
let rec = path => {
let node = findRuleByDottedName(allRules, path)
if (!node) throw new Error(`La règle ${path} est introuvable`)
let variant = getVariant(node),
variants = variant && unless(is(Array), prop('possibilités'))(variant),
shouldBeExpanded = variant && true, //variants.find( v => relevantPaths.find(rp => contains(path + ' . ' + v)(rp) )),
canGiveUp = variant && !variant['choix obligatoire']
return Object.assign(
node,
shouldBeExpanded
? {
canGiveUp,
children: (variants as any).map(v => rec(path + ' . ' + v))
}
: null
)
}
return rec(path)
}

View File

@ -0,0 +1,82 @@
import Input from 'Components/conversation/Input'
import Question from 'Components/conversation/Question'
import SelectGéo from 'Components/conversation/select/SelectGéo'
import SelectAtmp from 'Components/conversation/select/SelectTauxRisque'
import { is, pick, prop, unless } from 'ramda'
import React from 'react'
import DateInput from '../components/conversation/DateInput'
import { findRuleByDottedName, queryRule } from './rules'
// This function takes the unknown rule and finds which React component should be displayed to get a user input through successive if statements
// That's not great, but we won't invest more time until we have more diverse input components and a better type system.
// eslint-disable-next-line react/display-name
export default rules => dottedName => {
let rule = findRuleByDottedName(rules, dottedName)
let commonProps = {
key: dottedName,
fieldName: dottedName,
...pick(
['dottedName', 'title', 'question', 'defaultValue', 'suggestions'],
rule
)
}
if (getVariant(rule))
return (
<Question
{...commonProps}
choices={buildVariantTree(rules, dottedName)}
/>
)
if (rule.API && rule.API === 'géo')
return <SelectGéo {...{ ...commonProps }} />
if (rule.API) throw new Error("Le seul API implémenté est l'API géo")
if (rule.dottedName == 'contrat salarié . ATMP . taux collectif ATMP')
return <SelectAtmp {...commonProps} />
if (rule.type === 'date') {
return <DateInput {...commonProps} />
}
if (rule.unit == null && rule.defaultUnit == null)
return (
<Question
{...commonProps}
choices={[
{ value: 'non', label: 'Non' },
{ value: 'oui', label: 'Oui' }
]}
/>
)
// Now the numeric input case
return <Input {...commonProps} unit={rule.unit || rule.defaultUnit} />
}
let getVariant = rule => queryRule(rule)('formule . une possibilité')
let buildVariantTree = (allRules, path) => {
let rec = path => {
let node = findRuleByDottedName(allRules, path)
if (!node) throw new Error(`La règle ${path} est introuvable`)
let variant = getVariant(node),
variants = variant && unless(is(Array), prop('possibilités'))(variant),
shouldBeExpanded = variant && true, //variants.find( v => relevantPaths.find(rp => contains(path + ' . ' + v)(rp) )),
canGiveUp = variant && !variant['choix obligatoire']
return Object.assign(
node,
shouldBeExpanded
? {
canGiveUp,
children: variants.map(v => rec(path + ' . ' + v))
}
: null
)
}
return rec(path)
}

View File

@ -678,7 +678,6 @@ path:
sécuritéSociale: /social-security
simulateurs:
index: /simulators
dnrti: /dnrti
assimilé-salarié: /assimile-salarie
indépendant: /independant
auto-entrepreneur: /auto-entrepreneur

View File

@ -152,7 +152,6 @@ function existingCompany(state: Company | null = null, action): Company | null {
action.catégorieJuridique
)
return {
...state,
siren: state.siren,
statutJuridique,
dateDeCréation: action.dateDeCréation

View File

@ -199,7 +199,6 @@ function simulation(
if (state === null) {
return state
}
switch (action.type) {
case 'HIDE_CONTROL':
return { ...state, hiddenControls: [...state.hiddenControls, action.id] }
@ -248,21 +247,7 @@ function simulation(
}
return state
}
const existingCompanyReducer = (state, action: Action) => {
if (action.type.startsWith('EXISTING_COMPANY::') && state.simulation) {
return {
...state,
simulation: {
...state.simulation,
situation: {
...state.simulation.situation,
...getCompanySituation(state.inFranceApp.existingCompany)
}
}
}
}
return state
}
const mainReducer = (state, action: Action) =>
combineReducers({
lang,
@ -285,7 +270,6 @@ const mainReducer = (state, action: Action) =>
export default reduceReducers<RootState>(
mainReducer as any,
existingCompanyReducer as any,
storageRootReducer as any
) as Reducer<RootState>

View File

@ -3746,15 +3746,15 @@ dirigeant . indépendant . cotisations et contributions:
dirigeant . indépendant . cotisations et contributions . cotisations . déduction tabac:
applicable si: entreprise . catégorie d'activité . débit de tabac
unité par défaut: €/an
question: Quel est le montant des revenus issus de la vente de tabac que vous souhaitez exonérer de cotisation vieillesse ?
description: |
Si vous exercez une activité de débit de tabac simultanément à une activité commerciale, vous avez la possibilité dopter pour le calcul de votre cotisation dassurance vieillesse sur le seul revenu tiré de votre activité commerciale (en effet, les remises pour débit de tabac sont soumises par ailleurs à un prélèvement vieillesse particulier). Nous attirons cependant votre attention sur le fait quen cotisant sur une base moins importante, excluant les revenus de débit de tabac, vos droits à retraite pour lassurance vieillesse des commerçants en seront diminués.
par défaut: 0
par défaut: non
dirigeant . indépendant . cotisations et contributions . cotisations . déduction tabac . revenus déduits:
titre: revenu professionnel (avec déduction tabac)
applicable si: déduction tabac
unité par défaut: €/an
remplace:
règle: revenu professionnel
dans:

View File

@ -13,7 +13,6 @@ body,
min-height: 100%;
display: flex;
flex-direction: column;
/* overflow: auto; */
}
@media (min-width: 500px) {
@ -22,7 +21,6 @@ body,
flex: 1;
}
.app-container {
/* overflow: auto; */
min-height: 100vh;
height: auto;
}

View File

@ -197,7 +197,7 @@ type CompanySectionProps = {
company: Company | null
}
export const CompanySection = ({ company }: CompanySectionProps) => {
const CompanySection = ({ company }: CompanySectionProps) => {
const [searchModal, showSearchModal] = useState(false)
const [autoEntrepreneurModal, showAutoEntrepreneurModal] = useState(false)
const [DirigeantMajoritaireModal, showDirigeantMajoritaireModal] = useState(

View File

@ -5,22 +5,21 @@ import SimulateurWarning from 'Components/SimulateurWarning'
import config from 'Components/simulationConfigs/artiste-auteur.yaml'
import 'Components/TargetSelection.css'
import { formatValue } from 'Engine/format'
import InputComponent from 'Engine/RuleInput'
import { getRuleFromAnalysis } from 'Engine/rules'
import React, { createContext, useContext, useEffect, useState } from 'react'
import React, { useEffect, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import NumberFormat from 'react-number-format'
import { useDispatch, useSelector } from 'react-redux'
import { RootState } from 'Reducers/rootReducer'
import {
analysisWithDefaultsSelector,
flatRulesSelector,
parsedRulesSelector,
ruleAnalysisSelector,
situationSelector
} from 'Selectors/analyseSelectors'
import styled from 'styled-components'
import { DottedName } from 'Types/rule'
import Animate from 'Ui/animate'
import ToggleSwitch from 'Ui/ToggleSwitch'
export function useRule(dottedName: DottedName) {
const analysis = useSelector(analysisWithDefaultsSelector)
@ -28,7 +27,6 @@ export function useRule(dottedName: DottedName) {
return getRule(dottedName)
}
const InitialRenderContext = createContext(false)
function useInitialRender() {
const [initialRender, setInitialRender] = useState(true)
useEffect(() => {
@ -53,14 +51,27 @@ export default function ArtisteAuteur() {
<section className="ui__ light card">
<div id="targetSelection">
<ul className="targets">
<InitialRenderContext.Provider value={initialRender}>
<SimpleField dottedName="artiste-auteur . revenus . traitements et salaires" />
<SimpleField dottedName="artiste-auteur . revenus . BNC . recettes" />
<SimpleField dottedName="artiste-auteur . revenus . BNC . micro-bnc" />
<WarningRegimeSpecial />
<SimpleField dottedName="artiste-auteur . revenus . BNC . frais réels" />
<SimpleField dottedName="artiste-auteur . cotisations . option surcotisation" />
</InitialRenderContext.Provider>
<SimpleField
dottedName="artiste-auteur . revenus . traitements et salaires"
initialRender={initialRender}
/>
<SimpleField
dottedName="artiste-auteur . revenus . BNC . recettes"
initialRender={initialRender}
/>
<SimpleField
dottedName="artiste-auteur . revenus . BNC . micro-bnc"
initialRender={initialRender}
/>
<WarningRegimeSpecial />
<SimpleField
dottedName="artiste-auteur . revenus . BNC . frais réels"
initialRender={initialRender}
/>
<SimpleField
dottedName="artiste-auteur . cotisations . option surcotisation"
initialRender={initialRender}
/>
</ul>
</div>
</section>
@ -71,19 +82,17 @@ export default function ArtisteAuteur() {
type SimpleFieldProps = {
dottedName: DottedName
initialRender: boolean
}
function SimpleField({ dottedName }: SimpleFieldProps) {
const rule = useSelector(parsedRulesSelector)[dottedName]
function SimpleField({ dottedName, initialRender }: SimpleFieldProps) {
const rule = useRule(dottedName)
const dispatch = useDispatch()
const analysis = useSelector((state: RootState) => {
return ruleAnalysisSelector(state, { dottedName })
})
const initialRender = useContext(InitialRenderContext)
const flatRules = useSelector(flatRulesSelector)
const value = useSelector(situationSelector)[dottedName]
const onChange = x => dispatch(updateSituation(dottedName, x))
const analysis = useSelector((state: RootState) =>
ruleAnalysisSelector(state, { dottedName })
)
const situation = useSelector(situationSelector)
const [value, setValue] = useState(situation[dottedName])
if (!analysis.isApplicable) {
return null
@ -100,15 +109,36 @@ function SimpleField({ dottedName }: SimpleFieldProps) {
</label>
</div>
<div className="targetInputOrValue">
<InputComponent
className="targetInput"
isTarget
dottedName={dottedName}
rules={flatRules}
value={value}
onChange={onChange}
useSwitch
/>
{/* Super hacky */}
{analysis.unit !== undefined ? (
<NumberFormat
autoFocus
id={'step-' + dottedName}
thousandSeparator={' '}
suffix=" €"
allowEmptyFormatting={true}
onValueChange={({ floatValue }) => {
setValue(floatValue)
dispatch(updateSituation(dottedName, floatValue || 0))
}}
value={value}
autoComplete="off"
className="targetInput"
css={`
padding: 10px;
`}
/>
) : (
<ToggleSwitch
id={`step-${dottedName}`}
defaultChecked={rule.nodeValue}
onChange={evt =>
dispatch(
updateSituation(dottedName, evt.currentTarget.checked)
)
}
/>
)}
</div>
</div>
</Animate.appear>

View File

@ -1,283 +0,0 @@
import { setSimulationConfig, updateSituation } from 'Actions/actions'
import RuleLink from 'Components/RuleLink'
import 'Components/TargetSelection.css'
import { formatValue } from 'Engine/format'
import InputComponent from 'Engine/RuleInput'
import React, { useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { RootState } from 'Reducers/rootReducer'
import {
analysisWithDefaultsSelector,
flatRulesSelector,
nextStepsSelector,
ruleAnalysisSelector,
situationSelector
} from 'Selectors/analyseSelectors'
import styled from 'styled-components'
import { DottedName, Rule } from 'Types/rule'
import Animate from 'Ui/animate'
import { CompanySection } from '../Gérer/Home'
import { useRule } from './ArtisteAuteur'
const simulationConfig = {
objectifs: [
'dirigeant . indépendant . cotisations et contributions',
'dirigeant . rémunération totale'
],
situation: {
dirigeant: 'indépendant'
},
'unités par défaut': ['€/an']
}
export default function DNRTI() {
const dispatch = useDispatch()
const analysis = useSelector(analysisWithDefaultsSelector)
const company = useSelector(
(state: RootState) => state.inFranceApp.existingCompany
)
dispatch(setSimulationConfig(simulationConfig, true))
return (
<>
<h1>
Aide à la déclaration de revenus{' '}
<img src="https://img.shields.io/badge/-beta-blue" />
<br />
<small>Travailleurs indépendants</small>
</h1>
<p>
Cet outil vous permet de calculer les données à saisir dans votre
déclaration de revenus professionnels.
</p>
<FormWrapper>
<FormBlock>
<CompanySection company={company} />
<h2>Revenus d'activité</h2>
<SimpleField
dottedName="dirigeant . rémunération totale"
question="Quel est votre résultat fiscal ?"
/>
<SimpleField dottedName="entreprise . date de création" />
<SubSection dottedName="entreprise . catégorie d'activité" />
{/* PLNR */}
<SimpleField dottedName="dirigeant . indépendant . PLNR régime général" />
<SimpleField dottedName="dirigeant . indépendant . cotisations et contributions . cotisations . retraite complémentaire . taux spécifique PLNR" />
<SimpleField dottedName="dirigeant . indépendant . cotisations et contributions . cotisations . déduction tabac" />
<h3>Situation personnelle</h3>
<SimpleField dottedName="situation personnelle . RSA" />
<SubSection dottedName="situation personnelle . IJSS" />
<SubSection dottedName="dirigeant . indépendant . conjoint collaborateur" />
<h3>Exonérations</h3>
<SimpleField dottedName="entreprise . ACRE" />
<SimpleField dottedName="établissement . ZFU" />
<SubSection
dottedName="dirigeant . indépendant . cotisations et contributions . exonérations"
hideTitle
/>
<h3>International</h3>
<SimpleField dottedName="situation personnelle . domiciliation fiscale à l'étranger" />
<SubSection
dottedName="dirigeant . indépendant . revenus étrangers"
hideTitle
/>
{/* <h3>DOM - Départements d'Outre-Mer</h3>
<p>
<em>Pas encore implémenté</em>
</p> */}
</FormBlock>
<Results />
</FormWrapper>
</>
)
}
type SubSectionProp = {
dottedName: DottedName
hideTitle?: boolean
}
function SubSection({
dottedName: sectionDottedName,
hideTitle = false
}: SubSectionProp) {
const flatRules = useSelector(flatRulesSelector)
const ruleTitle = useRule(sectionDottedName)?.title
const nextSteps = useSelector(nextStepsSelector)
const situation = useSelector(situationSelector)
const title = hideTitle ? null : ruleTitle
const subQuestions = flatRules
.filter(
({ dottedName, question }) =>
Boolean(question) &&
dottedName.startsWith(sectionDottedName) &&
(Object.keys(situation).includes(dottedName) ||
nextSteps.includes(dottedName))
)
.sort(
(rule1, rule2) =>
nextSteps.indexOf(rule1.dottedName) -
nextSteps.indexOf(rule2.dottedName)
)
return (
<>
{!!subQuestions.length && title && <h3>{title}</h3>}
{subQuestions.map(({ dottedName }) => (
<SimpleField key={dottedName} dottedName={dottedName} />
))}
</>
)
}
type SimpleFieldProps = {
dottedName: DottedName
question?: Rule['question']
}
function SimpleField({ dottedName, question }: SimpleFieldProps) {
const dispatch = useDispatch()
const analysis = useSelector((state: RootState) => {
return ruleAnalysisSelector(state, { dottedName })
})
const rules = useSelector((state: RootState) => state.rules)
const value = useSelector(situationSelector)[dottedName]
const [currentValue, setCurrentValue] = useState(value)
const update = (value: unknown) => {
dispatch(updateSituation(dottedName, value))
dispatch({
type: 'STEP_ACTION',
name: 'fold',
step: dottedName
})
setCurrentValue(value)
}
useEffect(() => {
setCurrentValue(value)
}, [value])
if (!analysis.isApplicable) {
return null
}
return (
<Animate.fromTop>
<Question>
<p
css={`
border-left: 4px solid var(--lightColor);
border-radius: 3px;
padding-left: 12px;
margin-left: -12px;
`}
>
{question ?? analysis.question}
</p>
<InputComponent
rules={rules}
dottedName={dottedName}
onChange={update}
value={currentValue}
/>
{/* <Field dottedName={dottedName} onChange={onChange} /> */}
</Question>
</Animate.fromTop>
)
}
function Results() {
const cotisationsRule = useRule(
'dirigeant . indépendant . cotisations et contributions'
)
const revenusNet = useRule(
'dirigeant . indépendant . revenu net de cotisations'
)
const nonDeductible = useRule(
'dirigeant . indépendant . cotisations et contributions . CSG et CRDS'
)
function Link({ cotisation }) {
return (
<p className="ui__ lead">
<RuleLink dottedName={cotisation.dottedName}>
{cotisation.nodeValue
? formatValue({
value: cotisation.nodeValue,
language: 'fr',
unit: '€',
maximumFractionDigits: 0
})
: '-'}
</RuleLink>
</p>
)
}
if (!cotisationsRule.nodeValue) {
return null
}
return (
<ResultBlock>
<Animate.fromTop>
<ResultSubTitle>Vos cotisations</ResultSubTitle>
<Link cotisation={cotisationsRule} />
<ResultSubTitle>Vos revenus net</ResultSubTitle>
<Link cotisation={revenusNet} />
<ResultSubTitle>Cotisations non déductibles</ResultSubTitle>
<p className="ui__ notice">
Ce montant doit être réintégré au revenu net dans votre déclaration
fiscale.
</p>
<Link cotisation={nonDeductible} />
</Animate.fromTop>
</ResultBlock>
)
}
const FormWrapper = styled.div`
display: flex;
justify-content: space-between;
align-items: flex-start;
ul {
padding: 0;
}
`
const FormBlock = styled.section`
width: 63%;
padding: 0;
h3 {
margin-top: 50px;
}
select,
input[type='text'] {
font-size: 1.05em;
padding: 5px 10px;
}
`
const Question = styled.div`
margin-top: 1em;
`
const ResultBlock = styled.section`
position: sticky;
top: 3%;
padding: 3%;
width: 34%;
background: var(--lightestColor);
`
const ResultSubTitle = styled.h4`
&:not(:first-child) {
margin-top: 2em;
}
`
const ResultNumber = styled.strong`
display: block;
text-align: right;
`

View File

@ -8,7 +8,6 @@ import { Link, useLocation } from 'react-router-dom'
import ArtisteAuteur from './ArtisteAuteur'
import AssimiléSalarié from './AssimiléSalarié'
import AutoEntrepreneur from './AutoEntrepreneur'
import DNRTI from './dnrti'
import Home from './Home'
import Indépendant from './Indépendant'
import Salarié from './Salarié'
@ -28,7 +27,7 @@ export default function Simulateurs() {
return (
<>
<ScrollToTop key={pathname} />
{pathname !== sitePaths.simulateurs.index && !pathname.match('dnrti') && (
{pathname !== sitePaths.simulateurs.index && (
<div css="transform: translateY(2rem);">
{lastState?.fromGérer && (
<Link
@ -46,14 +45,15 @@ export default function Simulateurs() {
<Trans>Retour à la création</Trans>
</Link>
)}
{(!lastState || lastState?.fromSimulateurs) && (
<Link
to={sitePaths.gérer.index}
className="ui__ simple small push-left button"
>
<Trans>Voir les autres simulateurs</Trans>
</Link>
)}
{!lastState ||
(lastState?.fromSimulateurs && (
<Link
to={sitePaths.simulateurs.index}
className="ui__ simple small push-left button"
>
<Trans>Voir les autres simulateurs</Trans>
</Link>
))}
</div>
)}
<Switch>
@ -79,7 +79,6 @@ export default function Simulateurs() {
path={sitePaths.simulateurs['artiste-auteur']}
component={ArtisteAuteur}
/>
<Route path={sitePaths.simulateurs.dnrti} component={DNRTI} />
</Switch>
</>
)

View File

@ -102,8 +102,7 @@ export const constructLocalizedSitePath = (language: string) => {
'/comparaison-régimes-sociaux'
),
salarié: t('path.simulateurs.salarié', '/salarié'),
'artiste-auteur': t('path.simulateurs.artiste-auteur', '/artiste-auteur'),
dnrti: t('path.simulateurs.dnrti', '/dnrti')
'artiste-auteur': t('path.simulateurs.artiste-auteur', '/artiste-auteur')
},
économieCollaborative: {
index: t('path.économieCollaborative.index', '/économie-collaborative'),

View File

@ -1,23 +0,0 @@
date de création:
type: date
question: quelle est la date de création ?
durée entre deux dates:
unité: jours
formule:
durée:
depuis: date de création
jusqu'à: 01/01/2020
exemples:
- nom: Un an
situation:
date de création: 01/01/2019
valeur attendue: 365
- nom: Longtemps
situation:
date de création: 06/11/2012
valeur attendue: 2612
- nom: Un mois
situation:
date de création: 01/12/2019
valeur attendue: 31

View File

@ -1135,13 +1135,6 @@
resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.9.tgz#d868b6febb02666330410fe7f58f3c4b8258be7b"
integrity sha512-MNl+rT5UmZeilaPxAVs6YaPC2m6aA8rofviZbhbxpPpl61uKodfdQVsBtgJGTqGizEf02oW3tsVe7FYB8kK14A==
"@types/cleave.js@^1.4.1":
version "1.4.1"
resolved "https://registry.yarnpkg.com/@types/cleave.js/-/cleave.js-1.4.1.tgz#1eaa12dcbd1f2187fc580f0bf84dbc9f8834e333"
integrity sha512-53CbLKtK58uTfOCFOT1FMVKF5Zt1Onpk5sm37hY5cO3Uy4DfXzhQaU1PNMf53S7DJpEq1hWxZ+d2uN/W6YZh5w==
dependencies:
"@types/react" "*"
"@types/color-convert@^1.9.0":
version "1.9.0"
resolved "https://registry.yarnpkg.com/@types/color-convert/-/color-convert-1.9.0.tgz#bfa8203e41e7c65471e9841d7e306a7cd8b5172d"