diff --git a/site/source/pages/Simulateurs/ExonerationCovid/FormulaireS1S1Bis.tsx b/site/source/pages/Simulateurs/ExonerationCovid/FormulaireS1S1Bis.tsx new file mode 100644 index 000000000..5c197a432 --- /dev/null +++ b/site/source/pages/Simulateurs/ExonerationCovid/FormulaireS1S1Bis.tsx @@ -0,0 +1,424 @@ +import { DottedNames } from 'exoneration-covid' +import Engine, { + ASTNode, + EvaluatedNode, + Evaluation, + formatValue, + PublicodesExpression, +} from 'publicodes' +import { EngineContext } from '@/components/utils/EngineContext' +import { useContext, Key } from 'react' +import { H3 } from '@/design-system/typography/heading' +import { Trans, useTranslation } from 'react-i18next' +import { Grid } from '@mui/material' +import { Spacing } from '@/design-system/layout' +import { Item, Select } from '@/design-system/field/Select' +import styled from 'styled-components' +import { baseParagraphStyle } from '@/design-system/typography/paragraphs' + +const Json = (props: any) =>
{JSON.stringify(props, null, 2)}
+ +const Th = styled.th` + flex: 2; + word-break: break-word; +` + +const Td = styled.td` + flex: 2; + word-break: break-word; +` + +const Tr = styled.tr` + display: flex; + align-items: center; + flex: 1; + word-break: break-word; + padding: 1rem; + + ${Td}:last-child { + text-align: right; + } + ${Td}:first-child, ${Td}:last-child { + flex: 1; + } +` + +const Thead = styled.thead` + background: ${({ theme }) => theme.colors.bases.primary[200]}; + color: ${({ theme }) => theme.colors.bases.primary[700]}; + border-radius: 0.35rem 0.35rem 0 0; + + ${Th}:first-child, ${Th}:last-child { + flex: 1; + } +` + +const Tbody = styled.tbody` + border-radius: 0 0 0.35rem 0.35rem; + + ${Tr}:nth-child(odd) { + background: ${({ theme }) => theme.colors.extended.grey[200]}; + } +` + +const Table = styled.table` + display: flex; + flex-direction: column; + text-align: left; + border: 1px solid ${({ theme }) => theme.colors.extended.grey[400]}; + border-radius: 0.35rem; + ${baseParagraphStyle} + font-weight: bold; +` + +const Empty = styled.div` + display: inline-block; + background: ${({ theme }) => theme.colors.extended.grey[300]}; + padding: 0.25rem 1rem; + border-radius: 0.25rem; + font-weight: 500; + font-size: 0.9rem; +` + +const Recap = styled.div` + background: ${({ theme }) => theme.colors.bases.primary[600]}; + border-radius: 0.25rem; + padding: 1.5rem; + ${baseParagraphStyle} + line-height: 1.5rem; + color: white; + + hr { + border-color: ${({ theme }) => theme.colors.bases.primary[500]}; + margin-bottom: 1rem; + width: 100%; + } +` + +const Bold = styled.div` + font-weight: 700; + margin-bottom: 0.5rem; +` + +const Italic = styled.div` + font-style: italic; + margin-bottom: 0.5rem; +` + +const GrandTotal = styled.div` + font-size: 1.25rem; + line-height: 1.5rem; + font-weight: 700; +` + +const Total = styled.div` + display: flex; + justify-content: flex-end; + margin-bottom: 0.5rem; +` + +type RowProps = { + title?: string + total?: number + items: EvaluatedNode[] + onSelectionChange?: (key: Key) => void + defaultSelectedKey?: Key +} + +const Row = ({ + title, + total, + items, + onSelectionChange, + defaultSelectedKey, +}: RowProps) => { + const { t } = useTranslation() + + const choices = { + 'LFSS 600': [ + t('Interdiction d’accueil du public (600 €)'), + t('Baisse d’au moins 50% du chiffre d’affaires (600 €)'), + t('Baisse d’au moins 65% du chiffre d’affaires (600 €)'), + ], + 'LFSS 300': [t("Baisse entre 30% à 64% du chiffre d'affaires (300 €)")], + LFR1: [t('Eligibilité aux mois de mars, avril ou mai 2021 (250 €)')], + } + + return ( + + {title} + + {items.length > 0 ? ( + + ) : ( + + Mois non concerné + + )} + + {total ? total : '–'} + + ) +} + +const getTotalByMonth = (engine: Engine) => { + type ParsedSituation = typeof engine.parsedSituation + + const { notMonthSituation, onlyMonthSituation } = Object.entries( + engine.parsedSituation + ).reduce( + (acc, [dotName, node]) => { + if (!dotName.startsWith('mois . ')) { + acc.notMonthSituation[dotName] = node + } else { + acc.onlyMonthSituation[dotName] = node + } + + return acc + }, + { + notMonthSituation: {} as ParsedSituation, + onlyMonthSituation: {} as ParsedSituation, + } + ) + + const notMonthEngine = engine.shallowCopy().setSituation(notMonthSituation) + + const missingVarOfLFR1Applicable = Object.fromEntries( + Object.keys(notMonthEngine.evaluate('LFR1 applicable').missingVariables) + .map((missingDottedName): [string, ASTNode] | null => + engine.parsedSituation[missingDottedName] + ? [missingDottedName, engine.parsedSituation[missingDottedName]] + : null + ) + .filter((x: T | null): x is T => Boolean(x)) + ) + + const ret = Object.fromEntries( + Object.entries(onlyMonthSituation).map(([monthDottedName, node]) => { + const targetDottedName = 'nodeValue' in node && (node.nodeValue as string) + + const monthEngine = notMonthEngine.shallowCopy().setSituation({ + ...notMonthSituation, + ...(targetDottedName === 'LFR1' ? missingVarOfLFR1Applicable : {}), + [monthDottedName]: node, + }) + + const value = monthEngine.evaluate({ valeur: targetDottedName }) + + return [monthDottedName, value] + }) + ) + + return ret +} + +export const FormulaireS1S1Bis = ({ + onChange, +}: { + onChange?: (dottedName: DottedNames, value: PublicodesExpression) => void +}) => { + const engine = useContext(EngineContext) as Engine + + const step1Situation = Object.fromEntries( + Object.entries(engine.parsedSituation).filter( + ([dotName]) => !dotName.startsWith('mois . ') + ) + ) + const step1Engine = engine.shallowCopy().setSituation(step1Situation) + + const step1LFSS600 = step1Engine.evaluate('LFSS 600') + const step1LFSS300 = step1Engine.evaluate('LFSS 300') + const step1LFR1 = step1Engine.evaluate('LFR1') + + const LFSS600 = engine.evaluate('LFSS 600') + const LFSS300 = engine.evaluate('LFSS 300') + const LFR1 = engine.evaluate('LFR1') + const exoS2 = engine.evaluate('exonération S2') + const total = engine.evaluate('montant total') + + const totalByMonth = getTotalByMonth(engine) + + if (!engine.evaluate('mois').nodeValue) { + return null + } + + const months = Object.entries(engine.parsedRules).filter( + ([, { explanation: e }]) => + e.parents.length === 1 && + e.parents[0].nodeKind === 'reference' && + e.parents[0].name === 'mois' + ) + + let emptyMonthIndex: number[] = [] + let isAnyRowShowed = false + + return ( + <> +

+ + Quelle était votre situation liée à la crise sanitaire durant vos mois + d’activité ? + +

+ + + + + + + + + + + + + {months.flatMap(([dotName, node], i) => { + const showRow = engine.evaluate(dotName).nodeValue !== null + + if (isAnyRowShowed && !showRow) { + emptyMonthIndex.push(i) + } + if (!showRow) { + return [] + } + + isAnyRowShowed = true + const previousEmptyMonth = emptyMonthIndex.map( + (i) => + months[i] && ( + + ) + ) + emptyMonthIndex = [] + + const items = [step1LFSS600, step1LFSS300, step1LFR1].filter( + (node) => + node.nodeKind === 'reference' && + node.dottedName && + dotName + ' . ' + node.dottedName in engine.parsedRules && + engine.evaluate(dotName + ' . ' + node.dottedName).nodeValue !== + null + ) + + const astNode = engine.parsedSituation[dotName] + + return [ + ...previousEmptyMonth, + )) || + undefined + } + onSelectionChange={(key) => { + onChange?.(dotName as DottedNames, `'${key}'`) + }} + key={dotName} + />, + ] + })} + +
MoisSituation liée à la crise sanitaireMontant de la réduction
+ + + + + + + + + Dispositif loi de financement de la sécurité sociale (LFSS) pour + 2021 + + + + Mesure d’interdiction d’accueil du public ou baisse d’au moins + 50% (ou 65% à compter de décembre 2021) du chiffre d’affaires + + + + + + {formatValue(LFSS600)} + + + + + + + Baisse entre 30% à 64% du chiffre d'affaires + + + + + {formatValue(LFSS300)} + + + +
+ + + + + + Dispositif loi de finances rectificative (LFR1) pour 2021 + + + Éligibilité aux mois de mars, avril ou mai 2021 + + + + + {formatValue(LFR1)} + + + +
+ + + + + + Montant de l’exonération sociale liée à la crise sanitaire sur + l’année 2021 + + + + + + {formatValue(total)} + + +
+ + + + ) +} diff --git a/site/source/pages/Simulateurs/ExonerationCovid/index.tsx b/site/source/pages/Simulateurs/ExonerationCovid/index.tsx index 82909a9d3..c4e14d86c 100644 --- a/site/source/pages/Simulateurs/ExonerationCovid/index.tsx +++ b/site/source/pages/Simulateurs/ExonerationCovid/index.tsx @@ -2,7 +2,7 @@ import exonerationCovid, { DottedNames } from 'exoneration-covid' import Engine, { PublicodesExpression } from 'publicodes' import { EngineProvider } from '@/components/utils/EngineContext' import RuleInput from '@/components/conversation/RuleInput' -import { useState, useCallback } from 'react' +import { useState, useCallback, useRef, useEffect } from 'react' import Value from '@/components/EngineValue' import { H3 } from '@/design-system/typography/heading' import { Trans } from 'react-i18next' @@ -10,10 +10,14 @@ import { Grid } from '@mui/material' import { Button } from '@/design-system/buttons' import { Spacing } from '@/design-system/layout' import { useLocation } from 'react-router' - -const covidEngine = new Engine(exonerationCovid) +import { FormulaireS1S1Bis } from './FormulaireS1S1Bis' export default function ExonérationCovid() { + // Use ref to keep state with react fast refresh + const { current: exoCovidEngine } = useRef( + new Engine(exonerationCovid) + ) + const rootDottedNames = [ 'secteur', "début d'activité", @@ -26,11 +30,15 @@ export default function ExonérationCovid() { [key in typeof rootDottedNames[number]]?: string } + useEffect(() => { + window.scrollTo(0, 0) + }, [location]) + const [situation, setSituation] = useState< Partial> >(() => { const defaultSituation = { ...params } - covidEngine.setSituation(defaultSituation) + exoCovidEngine.setSituation(defaultSituation) return defaultSituation }) @@ -39,27 +47,41 @@ export default function ExonérationCovid() { (name: DottedNames, value: PublicodesExpression | undefined) => { const newSituation = { ...situation, [name]: value } setSituation(newSituation) - covidEngine.setSituation(newSituation) + exoCovidEngine.setSituation(newSituation) }, - [situation] + [exoCovidEngine, situation] ) + const setStep1Situation = useCallback(() => { + const step1Situation = Object.fromEntries( + Object.entries(situation).filter( + ([dotName]) => !dotName.startsWith('mois . ') + ) + ) + setSituation(step1Situation) + exoCovidEngine.setSituation(step1Situation) + }, [exoCovidEngine, situation]) + const step2 = rootDottedNames.every((names) => params[names]) return ( <> - + {step2 ? ( - <>Page 2 + <> + + ) : ( <> -

{covidEngine.getRule('secteur').rawNode.question}

+

{exoCovidEngine.getRule('secteur').rawNode.question}

updateSituation('secteur', value)} /> -

{covidEngine.getRule("début d'activité").rawNode.question}

+

+ {exoCovidEngine.getRule("début d'activité").rawNode.question} +

@@ -69,7 +91,9 @@ export default function ExonérationCovid() { /> -

{covidEngine.getRule("lieu d'exercice").rawNode.question}

+

+ {exoCovidEngine.getRule("lieu d'exercice").rawNode.question} +

updateSituation("lieu d'exercice", value)} @@ -79,7 +103,7 @@ export default function ExonérationCovid() { - + {step2 ? ( ) : (