-
Notifications
You must be signed in to change notification settings - Fork 23
/
RevenuSelector.svelte
141 lines (130 loc) · 5.06 KB
/
RevenuSelector.svelte
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
<script context="module">
import Emoji from './Emoji.svelte';
import { formatValue, reduceAST } from 'publicodes';
import { engine } from '$lib/engine';
// We do a static analysis of the rules AST to search for a particular rule name.
// When in find it in a comparaison expression we retreive the value of the other
// side of the expression.
// For exemple in: applicable si: my . rule <= 400 €
// We will retrieve [400]
// We also need to handle mechanisms that are implemented using comparaisons such
// as “grille” or “barème”.
function findAllComparaisonsValue(dottedName, { searchedName, unit }) {
const comparaisonOperators = ['<', '<=', '>', '>='];
// TODO: There might be a better way to convert a parsed node into a given unit.
const convertValue = (node) => {
const valeur = formatValue(engine.evaluate(node)).replace(/\s/g, '');
if (valeur === '∞') {
return Infinity;
}
return engine.evaluate({ valeur, unité: unit }).nodeValue;
};
const accumulateThresholds = (acc, node) => {
if (node.nodeKind === 'grille' && node.explanation.assiette?.dottedName === searchedName) {
const thresholds = node.explanation.tranches.map(({ plafond }) => convertValue(plafond));
return [...acc, ...thresholds];
} else if (
node.nodeKind === 'operation' &&
comparaisonOperators.includes(node.operationKind)
) {
if (node.explanation[0]?.dottedName === searchedName) {
return [...acc, convertValue(node.explanation[1])];
} else if (node.explanation[1]?.dottedName === searchedName) {
return [...acc, convertValue(node.explanation[0])];
}
// Following reference like this is fragile. We could follow all references
// but would need some special handling for the dependency on parent.
// For now this works well enough.
} else if (node.nodeKind === 'reference' && node.name === 'ménage imposable') {
return [...acc, ...findAllComparaisonsValue(node.dottedName, { searchedName, unit })];
}
};
return reduceAST(accumulateThresholds, [], engine.getRule(dottedName));
}
</script>
<script>
import MultipleChoiceAnswer from './MultipleChoiceAnswer.svelte';
import { slide } from 'svelte/transition';
import { answers } from '$lib/stores';
export let goals;
let value;
const uniq = (l) => [...new Set(l)];
const thresholds = uniq(
goals
.flatMap((name) =>
findAllComparaisonsValue(name, {
searchedName: 'revenu fiscal de référence',
unit: '€/mois'
})
)
.filter((x) => x !== Infinity)
.map((x) => Math.round(x))
.sort((a, b) => a - b)
);
// Not all statically detected thresholds are impactful for the current computation
// as some of them might be in inactive branches of the computation.
// We evaluate all the thresholds and only keep the one that induice a change in the result.
const engineBis = engine.shallowCopy();
$: displayedThresholds = [0, ...thresholds]
.reduce(
(acc, revenu) => {
// HACK: The problem with thresholds evaluation is that their
// value might depend on other answers, for exemple the bike
// price. This implies that in some case we remove relevant
// thresholds. This should be fixed with a more clever static
// analysis.
//
// The line below is used to reactively recompute thresholds
// when a answer is given.
$answers;
engineBis.setSituation({
...engine.parsedSituation,
'revenu fiscal de référence': `${revenu + 1} €/mois`
});
const montantAides = engineBis.evaluate('aides').nodeValue;
if (montantAides === acc.dernierMontant) {
return acc;
} else {
return {
thresholds: [...acc.thresholds, revenu],
dernierMontant: montantAides
};
}
},
{ thresholds: [], dernierMontant: null }
)
.thresholds.slice(1);
let showExplanations = false;
$: $answers['revenu fiscal de référence'] = value && `${value} €/mois`;
</script>
{#if displayedThresholds.length > 0}
<div class="mt-6">
Quel est votre revenu net mensuel (quotient familial) ? <span
title="Plus d’informations"
class="cursor-pointer"
on:click={() => (showExplanations = !showExplanations)}><Emoji emoji="ℹ" /></span
>
{#if showExplanations}
<p class="m-4 mt-2 text-gray-600 text-sm" transition:slide|local={{ duration: 100 }}>
Le montant des aides dépend de votre revenu par part de quotient familial. Sur votre avis
d'imposition cela correspond au montant du « revenu fiscal de référence » divisé par le
nombre de parts du quotient familial, puis divisé par 12.
</p>
{/if}
<div class="flex gap-2 mt-2 flex-wrap">
{#each displayedThresholds as threshold}
<MultipleChoiceAnswer value={threshold - 1} bind:group={value}
>moins de <strong class="font-semibold">{formatValue(threshold)} €</strong
></MultipleChoiceAnswer
>
{/each}
<MultipleChoiceAnswer
value={displayedThresholds[displayedThresholds.length - 1] + 1}
bind:group={value}
>plus de <strong class="font-semibold"
>{formatValue(displayedThresholds[displayedThresholds.length - 1] + 1)} €</strong
></MultipleChoiceAnswer
>
</div>
</div>
{/if}