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é ?
+
+
+
+
+
+
+ Mois |
+ Situation liée à la crise sanitaire |
+ Montant de la réduction |
+
+
+
+
+
+ {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}
+ />,
+ ]
+ })}
+
+
+
+
+
+
+
+
+
+
+ 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 ? (
) : (