diff --git a/package-lock.json b/package-lock.json index 7cac0e6c889314d51d8f8d6bbfd27ba626eed4f3..bb4b8f04650b1c14a9d5799ec57fb4bcdbe843fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "leximpact-socio-fiscal-ui", - "version": "0.0.679", + "version": "0.0.681", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "leximpact-socio-fiscal-ui", - "version": "0.0.679", + "version": "0.0.681", "devDependencies": { "@auditors/core": "^0.7.0", "@fontsource/lato": "^5.0.5", diff --git a/src/app.postcss b/src/app.postcss index bddd107a24444d53e84abf6775d6f5f4ae56ba2b..34477717ae822379b3af53d7ccbb2e85bcf18378 100644 --- a/src/app.postcss +++ b/src/app.postcss @@ -57,7 +57,7 @@ /* *******--- CARTES ---******* */ /* CARTE : conteneur avec des coins arrondis, de l'ombre et des états au survol/clic */ .lx-card { - @apply bg-white hover:bg-neutral-50 active:bg-neutral-100 rounded-lg border shadow-md hover:shadow-lg hover:scale-105 transition-all; + @apply bg-white hover:bg-gray-50 active:bg-gray-100 rounded-lg border shadow-md hover:shadow-lg hover:scale-105 transition-all; } /* carte avec une bordure inférieure le-jaune-dark */ .lx-card-underline-le-vert { @@ -65,7 +65,7 @@ } /* carte grise avec une bordure inférieure le-jaune-dark */ .lx-card-bg-gray-underline-le-vert { - @apply lx-card-underline-le-vert bg-neutral-100/70 hover:bg-neutral-200/50 active:bg-neutral-200/70; + @apply lx-card-underline-le-vert bg-gray-100/70 hover:bg-gray-200/50 active:bg-gray-200/70; } /* *******--- ANIMATION ---******* */ .lx-opacity-0-unclickable { diff --git a/src/lib/components/NavBar.svelte b/src/lib/components/NavBar.svelte index a4bcd11dd637617705206f119c299912dff381b8..f2fe067a58a69ccbdff67baae716ba25e0739a8b 100644 --- a/src/lib/components/NavBar.svelte +++ b/src/lib/components/NavBar.svelte @@ -14,6 +14,7 @@ import { goto } from "$app/navigation" import { page } from "$app/stores" import NavBarSearch from "$lib/components/search/NavBarSearch.svelte" + import { withLinkedVariableNames } from "$lib/decompositions" import type { DisplayMode } from "$lib/displays" import { trackSearchVariable } from "$lib/matomo" import type { NavbarConfig } from "$lib/navbar" @@ -590,12 +591,17 @@ {/if} </ul> {:else} + <span class="inline-flex items-center gap-1 px-3 pt-3 font-bold"> + Recherchez parmi les {withLinkedVariableNames.length} dispositifs couverts + par le simulateur + <iconify-icon icon="ri-arrow-up-line" /> + </span> <ul class="max-h-64 list-none overflow-y-auto py-2 md:max-h-[80vh]"> - <li class="inline-block px-3 pb-2 text-gray-500">Suggestions</li> + <li class="inline-block px-3 py-2 text-gray-500">Suggestions</li> {#each dispositifsTypes as dispositif} <li> <button - class="w-full flex items-center gap-5 px-3 py-2 2xl:py-3 hover:bg-gray-200/70 active:bg-gray-200 transition" + class="w-full flex items-center gap-3 px-3 py-1 2xl:py-3 hover:bg-gray-200/70 active:bg-gray-200 transition" on:click={() => { $searchParameterName = dispositif.parametersVariableName focused = false @@ -606,11 +612,11 @@ on:touchend={() => (preventBlur = false)} > <img - class="max-h-8 md:max-h-10" + class="max-h-6 md:max-h-8" src={dispositif.icon} alt="Icône {dispositif.title}" /> - <span class="font-bold text-start text-lg 2xl:text-xl"> + <span class="text-start text-sm 2xl:text-base"> {dispositif.title} </span> </button> diff --git a/src/lib/components/SelectChip.svelte b/src/lib/components/SelectChip.svelte new file mode 100644 index 0000000000000000000000000000000000000000..c0e745517c7aea027c4fc395951088cc9b8fa918 --- /dev/null +++ b/src/lib/components/SelectChip.svelte @@ -0,0 +1,66 @@ +<script lang="ts"> + import { clickOutside } from "$lib/click_outside" + + export let label: string + export let open: boolean = false + export let options: { label: string; shortLabel: string; value: string }[] + export let value: string | undefined = undefined + export let showCheckIcon: boolean = false + + let chipButton: HTMLButtonElement + + $: if (value === undefined && options.length > 0) { + value = options[0].value + } + + $: optionByValue = Object.fromEntries( + options.map((option) => [option.value, option]), + ) +</script> + +<div class="relative {$$props.class ?? ''}"> + <button + bind:this={chipButton} + class="relative flex items-center gap-1 px-3 py-1 bg-le-bleu-light rounded-full overflow-hidden transition after:absolute after:inset-0 after:rounded-full after:transition hover:after:bg-black/5 active:after:bg-black/10" + class:after:bg-black={open} + class:after:bg-opacity-5={open} + on:click={() => (open = !open)} + > + {#if showCheckIcon} + <iconify-icon class="text-lg" icon="ri-check-line" noobserver /> + {/if} + {label} + {#if value !== undefined} + : {@html optionByValue[value].shortLabel} + {/if} + <iconify-icon class="text-lg" icon="ri-arrow-down-s-line" noobserver /> + </button> + + {#if open} + <ul + class="absolute -bottom-1 translate-y-full left-0 py-1.5 bg-white border shadow-md rounded-lg" + use:clickOutside={{ + callback: () => (open = false), + excluded: [chipButton], + }} + > + {#each options as option} + {@const selected = option.value === value} + <li> + <button + class="w-full text-nowrap text-start px-3 py-1.5" + class:hover:bg-gray-100={!selected} + class:active:bg-gray-200={!selected} + class:bg-le-bleu-light={selected} + on:click={() => { + value = option.value + open = false + }} + > + {@html option.label} + </button> + </li> + {/each} + </ul> + {/if} +</div> diff --git a/src/lib/components/test_cases/TestCaseDixiemes.svelte b/src/lib/components/test_cases/TestCaseDixiemes.svelte deleted file mode 100644 index 4186bc0b767fee6d3d0f491ae27391069bd8eb9f..0000000000000000000000000000000000000000 --- a/src/lib/components/test_cases/TestCaseDixiemes.svelte +++ /dev/null @@ -1,297 +0,0 @@ -<script lang="ts"> - import { scaleByInstantFromBrackets } from "@openfisca/json-model" - import { createEventDispatcher, getContext } from "svelte" - import type { Writable } from "svelte/store" - - import TestCasePictos from "$lib/components/test_cases/TestCasePictos.svelte" - import Tooltip from "$lib/components/Tooltip.svelte" - import VariableValueChange from "$lib/components/variables/VariableValueChange.svelte" - import { - decompositionCoreByName, - decompositionCoreByNameByReformName, - type EvaluationByName, - } from "$lib/decompositions" - import { - asScaleParameter, - getParameter, - rootParameter, - rootParameterByReformName, - } from "$lib/parameters" - import type { Situation } from "$lib/situations" - import { valueFormatter } from "$lib/values" - import { - variableSummaryByName, - variableSummaryByNameByReformName, - } from "$lib/variables" - - export let testCases: Situation[] - export let variableName: string - export let year: number - - const billName = getContext("billName") as Writable<string | undefined> - let descriptionsOpen = false - const dispatch = createEventDispatcher() - const evaluationByNameArray = getContext("evaluationByNameArray") as Writable< - EvaluationByName[] - > - const formatCurrency = valueFormatter(0, "currency-EUR", false) - const formatLongOrdinalSup = (n: number) => { - const rule = ordinalPluralRules.select(n) - const suffix = longOrdinalSuffixes.get(rule) - return `${n}<sup>${suffix}</sup>` - } - const longOrdinalSuffixes = new Map([ - ["other", "ème"], - ["one", "er"], - ]) - const ordinalPluralRules = new Intl.PluralRules("fr-FR", { type: "ordinal" }) - - let selectedDixieme = "1" - - // Note: A reform decomposition is always more complete than a decomposition before reform. - // And the children of a reform decomposition always contain the children of the decomposition - // before reform. - // → Non reform decomposition is not needed. - $: latestDecompositionCoreByName = - decompositionCoreByNameByReformName[$billName] ?? decompositionCoreByName - $: latestDecompositionCore = latestDecompositionCoreByName[variableName] - $: decomposition = - latestDecompositionCore === undefined - ? undefined - : { - ...latestDecompositionCore, - variableName, - } - - // Note: A reform variable is always more complete than a variable before reform. - // But it may contain different formulas, with different parameters & variables. - $: latestVariableSummaryByName = - variableSummaryByNameByReformName[$billName] ?? variableSummaryByName - $: variable = latestVariableSummaryByName[variableName] - - $: if (decomposition === undefined && variable === undefined) { - console.error(`Variable "${variableName}" not found`) - } - - $: shortLabel = - decomposition?.short_label ?? - variable?.short_label ?? - decomposition?.name ?? - variable?.name - - $: testCasesByDixieme = testCases.reduce( - (result: { [dixieme: string]: [Situation, number][] }, testCase, index) => { - if (testCase.dixieme !== undefined) { - const dixieme = `${testCase.dixieme}` - if (!Object.hasOwn(result, dixieme)) { - result[dixieme] = [] - } - result[dixieme].push([testCase, index]) - } - return result - }, - {}, - ) - - // Note: A reform parameters tree is always more complete than a parameters tree before reform. - // And the children of a reform node parameter always contain the children of the node parameter - // before reform (albeit with some different value parameters). - $: billRootParameter = rootParameterByReformName[$billName] ?? rootParameter - - $: decilesNiveauDeVieParameter = asScaleParameter( - getParameter(billRootParameter, "deciles_niveau_de_vie"), - ) - - $: decilesNiveauDeVieScaleByInstant = scaleByInstantFromBrackets( - decilesNiveauDeVieParameter.brackets, - ) - - $: decilesNiveauDeVieInstantScaleCouplesArray = Object.entries( - decilesNiveauDeVieScaleByInstant, - ).sort(([instant1], [instant2]) => instant2.localeCompare(instant1)) - - $: decilesNiveauDeVieLatestInstantScaleCouple = - decilesNiveauDeVieInstantScaleCouplesArray[0] - - $: decilesNiveauDeVieScaleAtInstant = - decilesNiveauDeVieLatestInstantScaleCouple[1] - - $: console.log(decilesNiveauDeVieScaleAtInstant) -</script> - -<span class="inline-block font-bold text-lg" - >Cas types distribués par niveau de vie :</span -> - -<div class="mt-2 hidden md:flex justify-between gap-1 px-2 overflow-x-scroll"> - {#each Object.keys(testCasesByDixieme) as dixieme} - <button - class="flex flex-col justify-between items-center px-2 py-1 text-sm bg-neutral-100 border-y-4 border-transparent border-opacity-80 shadow" - class:shrink-0={dixieme === "1" || dixieme === "10"} - class:z-20={selectedDixieme === dixieme} - class:bg-white={selectedDixieme === dixieme} - class:border-t-neutral-500={selectedDixieme === dixieme} - class:hover:bg-neutral-50={selectedDixieme !== dixieme} - class:hover:border-t-neutral-300={selectedDixieme !== dixieme} - class:shadow={selectedDixieme !== dixieme} - on:click={() => (selectedDixieme = dixieme)} - > - <span class="font-semibold"> - {#if dixieme !== "1"} - à partir de - {/if} - {formatCurrency( - decilesNiveauDeVieScaleAtInstant[Number(dixieme) - 2]?.threshold - ?.value ?? 0, - )} - {#if dixieme === "1"} - - {formatCurrency( - decilesNiveauDeVieScaleAtInstant[Number(dixieme) - 1]?.threshold - ?.value ?? ".", - )} - {/if} - </span> - <br /> - <span>{@html formatLongOrdinalSup(Number(dixieme))} décile</span> - </button> - {/each} -</div> -<div - class="md:hidden w-full bg-neutral-100 active:bg-neutral-200 border-y-4 border-t-neutral-500 border-b-neutral-100 active:border-b-neutral-200 overflow-hidden shadow-md transition" -> - <select - bind:value={selectedDixieme} - class="w-full bg-transparent font-bold tracking-wide border-[12px] border-transparent outline-0" - > - {#each Object.keys(testCasesByDixieme) as dixieme} - <option value={dixieme}> - {@html formatLongOrdinalSup(Number(dixieme))} décile - </option> - {/each} - </select> -</div> -<div - class="relative z-10 grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-2 sm:gap-5 p-3 bg-white shadow-[0_-4px_6px_-1px_rgb(0,0,0,0.1),0_-2px_4px_-2px_rgb(0,0,0,0.1)]" -> - {#each testCasesByDixieme[selectedDixieme] as [situation, situationIndex]} - <div class="flex flex-col items-start"> - <button - class="group flex-1 flex flex-col p-2 lx-card-bg-gray-underline-le-vert" - class:gap-2={variableName !== undefined} - on:click={() => dispatch("select", situationIndex)} - > - <div class="flex-1 flex items-center gap-3"> - <div class="shrink-0 grid grid-cols-6"> - <TestCasePictos - classes="[&>svg]:w-7 [&>svg]:h-7 col-span-3 last:odd:col-start-3 justify-center" - {situation} - {year} - /> - </div> - <span class="flex-1 text-start text-sm"> - {situation.title} - </span> - </div> - <div class="self-stretch flex justify-between"> - <span class="text-sm text-gray-500"> - {#if variableName !== undefined} - {shortLabel} : - <VariableValueChange - bold={true} - evaluationByName={$evaluationByNameArray[situationIndex]} - inline - name={variableName} - /> - {/if} - </span> - <iconify-icon - class="text-le-vert-400 text-xl self-end group-hover:text-le-jaune-very-dark" - icon="ri-arrow-right-line" - /> - </div> - </button> - <button - class="lx-link-text mt-1 text-sm text-gray-500" - on:click={() => (descriptionsOpen = !descriptionsOpen)} - > - <iconify-icon - class="text-xl align-[-0.25rem] transition-transform duration-300" - class:rotate-90={descriptionsOpen} - icon="ri-arrow-right-s-line" - /> - Représentativité du cas type - </button> - {#if descriptionsOpen} - <span class="mt-1 pl-5 pr-1 text-sm"> - {situation.description} - </span> - {/if} - </div> - {/each} -</div> - -<div class="flex flex-col items-end mb-4"> - <Tooltip arrowClass="bg-gray-100" widthClass="w-80" initialPlacement="bottom"> - <span class="underline decoration-dotted hover:text-black" - >Qu'est-ce qu’un décile ?</span - > - <div - slot="tooltip" - class="overflow-hidden bg-white text-sm font-light rounded-lg border border-gray-200 shadow-md" - > - <div class="py-2 px-3 bg-gray-100 border-b border-gray-200"> - <h3 class="font-semibold text-gray-900"> - Qu'est-ce qu'un décile de niveau de vie ? - </h3> - </div> - <div class="py-2 px-3 font-normal text-black"> - <p> - Chaque décile de niveau de vie représente 10% de la population - française agrégée par ménages classés par ordre croissant de niveau de - vie. Le 1er décile concerne donc les ménages les plus pauvres ; le - 10ème décile, les ménages les plus aisés. - </p> - <p class="mt-2"> - Le niveau de vie d'un ménage correspond à son revenu disponible après - tous les prélèvements et compléments de ressources, le tout divisé par - son nombre d'unité de consommation (peu ou prou le nombre de personnes - composant le foyer). - </p> - </div> - </div> - </Tooltip> - - <Tooltip arrowClass="bg-gray-100" widthClass="w-80" initialPlacement="bottom"> - <span class="underline decoration-dotted hover:text-black" - >Comment sont conçus ces cas types ?</span - > - <div - slot="tooltip" - class="overflow-hidden bg-white text-sm font-light rounded-lg border border-gray-200 shadow-md" - > - <div class="py-2 px-3 bg-gray-100 border-b border-gray-200"> - <h3 class="font-semibold text-gray-900"> - Comment sont conçus ces cas types ? - </h3> - </div> - <div class="py-2 px-3 font-normal text-black"> - <p> - LexImpact s'est appuyé sur la base POTE 2021 des déclarations d'impôt - sur le revenu des foyers fiscaux distribuée par la DGFIP. À partir de - cette base représentative de la population française, LexImpact a - étudié chacun des déciles de niveau de vie pour en extraire les - caractéristiques les plus fréquentes : - </p> - <ul class="list-inside list-disc"> - <li>composition familiale,</li> - <li>activité et revenus</li> - <li>lieu de vie et statut locataire ou propriétaire</li> - </ul> - <p class="mt-2"> - La méthodologie appliquée est décrite pour chaque cas type en cliquant - sur le bouton "Représentativité du cas type". - </p> - </div> - </div> - </Tooltip> -</div> diff --git a/src/lib/components/test_cases/TestCaseFilters.svelte b/src/lib/components/test_cases/TestCaseFilters.svelte index f888b421e7f7de993116bbc2dc060054624d3d88..29a9c7fbcd80753e55696b1a9e5580de555b4d50 100644 --- a/src/lib/components/test_cases/TestCaseFilters.svelte +++ b/src/lib/components/test_cases/TestCaseFilters.svelte @@ -4,8 +4,9 @@ import type { Writable } from "svelte/store" import { page } from "$app/stores" - import { clickOutside } from "$lib/click_outside" + import SelectChip from "$lib/components/SelectChip.svelte" import TestCaseSummary from "$lib/components/test_cases/TestCaseSummary.svelte" + import Tooltip from "$lib/components/Tooltip.svelte" import VariableValueChange from "$lib/components/variables/VariableValueChange.svelte" import { decompositionCoreByName, @@ -30,6 +31,7 @@ } from "$lib/variables" export let displayMode: DisplayMode + export let showOnlyDeciles: boolean = false export let testCases: Situation[] export let variableName: string | undefined = undefined export let year: number @@ -185,18 +187,12 @@ }, ] + let filterNiveauDeVieValue: string | undefined = undefined + $: filterValueByName = Object.fromEntries( filters.map((filter) => [filter.name, filter.value]), ) - const filterNiveauDeVie: { - open: boolean - value?: string - label?: string - } = { - open: false, - } - $: selectDecilesNiveauDeVie = [ { label: "Tous", @@ -215,13 +211,18 @@ })), ] as { label: string; shortLabel: string; value: string }[] - $: filteredTestCases = filterTestCases(filterValueByName, filterNiveauDeVie) + $: filteredTestCases = filterTestCases( + filterValueByName, + showOnlyDeciles, + filterNiveauDeVieValue, + ) function filterTestCases( filterValueByName: { [filter: string]: boolean | string }, - filterNiveauDeVie: { value?: string; label?: string }, + showOnlyDeciles: boolean, + filterNiveauDeVie: string | undefined, ): [Situation, number][] { const filtered: [Situation, number][] = [] for (const [index, situation] of testCases.entries()) { @@ -341,12 +342,14 @@ continue } } - if (filterNiveauDeVie.value !== undefined) { - if (filterNiveauDeVie.value === "tous") { + if (!showOnlyDeciles) { + filterNiveauDeVieValue = undefined + } else if (filterNiveauDeVie !== undefined) { + if (filterNiveauDeVie === "tous") { if (situation.dixieme === undefined) { continue } - } else if (`${situation.dixieme}` !== filterNiveauDeVie.value) { + } else if (`${situation.dixieme}` !== filterNiveauDeVie) { continue } } @@ -381,74 +384,99 @@ </div> <label class="mt-6 inline-flex items-center cursor-pointer"> - <input - type="checkbox" - class="sr-only peer" - on:input={({ target }) => { - filterNiveauDeVie.value = target.checked - ? selectDecilesNiveauDeVie[0].value - : undefined - filterNiveauDeVie.label = target.checked - ? selectDecilesNiveauDeVie[0].shortLabel - : undefined - }} - /> + <input bind:checked={showOnlyDeciles} class="sr-only peer" type="checkbox" /> <div class="relative w-11 h-6 bg-gray-400 peer-focus:outline-none peer-focus:ring-4 rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-400 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-le-bleu" /> <span class="inline-flex items-center gap-1.5 ms-3 text-sm font-medium text-gray-900" >Afficher uniquement les cas types dont la représentativité a été définie - <!-- <div class="bg-le-bleu w-2 h-2" /><iconify-icon--> - <!-- class="text-[#b6b8ff] text-xl -ml-5"--> - <!-- icon="material-symbols:award-star-rounded"--> - <!-- />--> </span> </label> -{#if filterNiveauDeVie.value} +{#if showOnlyDeciles} <div class="mt-2 flex flex-wrap gap-x-2 gap-y-1"> - <div - class="z-20 relative" - use:clickOutside={{ - callback: () => (filterNiveauDeVie.open = false), - }} + <SelectChip + bind:value={filterNiveauDeVieValue} + class="z-20" + label="Niveau de vie" + options={selectDecilesNiveauDeVie} + showCheckIcon={true} + /> + </div> + + <div class="flex flex-col items-end"> + <Tooltip + arrowClass="bg-gray-100" + widthClass="w-80" + initialPlacement="bottom" > - <button - class="relative flex items-center gap-1 px-3 py-1 bg-le-bleu-light rounded-full overflow-hidden transition after:absolute after:inset-0 after:rounded-full after:transition hover:after:bg-black/5 active:after:bg-black/10" - class:after:bg-black={filterNiveauDeVie.open} - class:after:bg-opacity-5={filterNiveauDeVie.open} - on:click={() => (filterNiveauDeVie.open = !filterNiveauDeVie.open)} + <span class="underline decoration-dotted hover:text-black" + >Qu'est-ce qu’un décile ?</span > - <iconify-icon class="text-lg" icon="ri-check-line" noobserver /> - Niveau de vie : {@html filterNiveauDeVie.label} - <iconify-icon class="text-lg" icon="ri-arrow-down-s-line" noobserver /> - </button> - {#if filterNiveauDeVie.open} - <ul - class="absolute -bottom-1 translate-y-full left-0 py-1.5 bg-white border shadow-md rounded-lg" - > - {#each selectDecilesNiveauDeVie as option} - {@const selected = option.value === filterNiveauDeVie.value} - <li> - <button - class="w-full text-nowrap text-start px-3 py-1.5" - class:hover:bg-gray-100={!selected} - class:active:bg-gray-200={!selected} - class:bg-le-bleu-light={selected} - on:click={() => { - filterNiveauDeVie.value = option.value - filterNiveauDeVie.label = option.shortLabel - filterNiveauDeVie.open = false - }} - > - {@html option.label} - </button> - </li> - {/each} - </ul> - {/if} - </div> + <div + slot="tooltip" + class="overflow-hidden bg-white text-sm font-light rounded-lg border border-gray-200 shadow-md" + > + <div class="py-2 px-3 bg-gray-100 border-b border-gray-200"> + <h3 class="font-semibold text-gray-900"> + Qu'est-ce qu'un décile de niveau de vie ? + </h3> + </div> + <div class="py-2 px-3 font-normal text-black"> + <p> + Chaque décile de niveau de vie représente 10% de la population + française agrégée par ménages classés par ordre croissant de niveau + de vie. Le 1er décile concerne donc les ménages les plus pauvres ; + le 10ème décile, les ménages les plus aisés. + </p> + <p class="mt-2"> + Le niveau de vie d'un ménage correspond à son revenu disponible + après tous les prélèvements et compléments de ressources, le tout + divisé par son nombre d'unité de consommation (peu ou prou le nombre + de personnes composant le foyer). + </p> + </div> + </div> + </Tooltip> + + <Tooltip + arrowClass="bg-gray-100" + widthClass="w-80" + initialPlacement="bottom" + > + <span class="underline decoration-dotted hover:text-black" + >Comment sont conçus ces cas types ?</span + > + <div + slot="tooltip" + class="overflow-hidden bg-white text-sm font-light rounded-lg border border-gray-200 shadow-md" + > + <div class="py-2 px-3 bg-gray-100 border-b border-gray-200"> + <h3 class="font-semibold text-gray-900"> + Comment sont conçus ces cas types ? + </h3> + </div> + <div class="py-2 px-3 font-normal text-black"> + <p> + LexImpact s'est appuyé sur la base POTE 2021 des déclarations + d'impôt sur le revenu des foyers fiscaux distribuée par la DGFIP. À + partir de cette base représentative de la population française, + LexImpact a étudié chacun des déciles de niveau de vie pour en + extraire les caractéristiques les plus fréquentes : + </p> + <ul class="list-inside list-disc"> + <li>composition familiale,</li> + <li>activité et revenus</li> + <li>lieu de vie et statut locataire ou propriétaire</li> + </ul> + <p class="mt-2"> + La méthodologie appliquée est décrite pour chaque cas type en + cliquant sur le bouton "Représentativité du cas type". + </p> + </div> + </div> + </Tooltip> </div> {/if} @@ -462,61 +490,73 @@ > {#each filteredTestCases as [testCase, index]} <div> - <div class="relative lx-card"> + <div class="relative lx-card-underline-le-vert"> <button - class="group text-sm rounded-md overflow-hidden" + class="group w-full flex flex-col items-stretch text-sm rounded-md overflow-hidden" on:click={() => dispatch("select", index)} type="button" > - <div> - <div - class="bg-gray-100 p-4 pb-2 text-left" - id="situation_{index}_case_summary" - > - <TestCaseSummary - {displayMode} - mode="select" - situation={testCase} - situationIndex={index} - valuesByCalculationNameByVariableName={$valuesByCalculationNameByVariableNameArray[ - index - ]} - {year} - /> - </div> + <div + class="bg-gray-100 p-4 pb-2 text-left" + id="situation_{index}_case_summary" + > + <TestCaseSummary + {displayMode} + mode="select" + situation={testCase} + situationIndex={index} + valuesByCalculationNameByVariableName={$valuesByCalculationNameByVariableNameArray[ + index + ]} + {year} + /> + </div> + <div + class="flex flex-col p-2" + class:bg-gray-100={variableName === undefined} + class:border-t={variableName !== undefined} + > {#if variableName !== undefined} - <div class="flex flex-col gap-2 bg-white p-3"> - <span class="text-start">{shortLabel} :</span> - <div class="flex gap-5 text-3xl font-semibold"> - <VariableValueChange - evaluationByName={$evaluationByNameArray[index]} - legend - name={variableName} - /> - </div> - </div> + <p class="block px-1 text-start text-sm text-gray-500"> + {shortLabel} : + <VariableValueChange + bold={true} + evaluationByName={$evaluationByNameArray[index]} + inline + name={variableName} + /> + </p> {/if} + <iconify-icon + class="block text-le-vert-400 text-xl self-end group-hover:text-le-jaune-very-dark" + icon="ri-arrow-right-line" + /> </div> - <span class="block px-2 py-1 text-start border-t"> - {testCase.title} - </span> </button> {#if testCase.dixieme !== undefined} <div - class="absolute -right-3 top-0 -translate-y-1/2 flex items-center" + class="absolute -right-7 top-0 -translate-y-1/2 flex items-center" > <div - class="flex items-center text-sm bg-le-bleu-light pl-2 -mr-5 rounded-full" + class="flex items-center text-sm bg-le-bleu-light pl-2 pr-4 rounded-l-full" > {@html formatLongOrdinalSup(testCase.dixieme)} décile - <div class="ml-3 w-3 h-3 bg-le-bleu" /> </div> - <iconify-icon - class=" text-[#b6b8ff]" - icon="material-symbols:award-star-rounded" - width="28" - height="28" - /> + <svg + class="w-7 h-7 -translate-x-1/2" + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 24 24" + ><path + class="text-[#b6b8ff]" + fill="currentColor" + d="M8.65 20H6q-.825 0-1.412-.587T4 18v-2.65L2.075 13.4q-.275-.3-.425-.662T1.5 12t.15-.737t.425-.663L4 8.65V6q0-.825.588-1.412T6 4h2.65l1.95-1.925q.3-.275.663-.425T12 1.5t.738.15t.662.425L15.35 4H18q.825 0 1.413.588T20 6v2.65l1.925 1.95q.275.3.425.663t.15.737t-.15.738t-.425.662L20 15.35V18q0 .825-.587 1.413T18 20h-2.65l-1.95 1.925q-.3.275-.662.425T12 22.5t-.737-.15t-.663-.425z" + /> + <path + class="text-le-bleu" + fill="currentColor" + d="m12 14.475l1.925 1.15q.275.175.538-.012t.187-.513l-.5-2.175l1.7-1.475q.25-.225.15-.537t-.45-.338l-2.225-.175l-.875-2.075q-.125-.3-.45-.3t-.45.3l-.875 2.075l-2.225.175q-.35.025-.45.338t.15.537l1.7 1.475l-.5 2.175q-.075.325.188.513t.537.012z" + /> + </svg> </div> {/if} </div> @@ -545,7 +585,7 @@ <div class="mt-6 flex flex-col items-center gap-4 py-36 bg-gray-100 rounded-lg" > - Aucun cas type ne correspond à vos critères de filtre. + Aucun cas type ne correspond aux filtres sélectionnés. <button class="mb-8 flex items-center gap-1.5 py-1.5 px-3 bg-white hover:bg-neutral-200 active:bg-neutral-300 rounded-lg font-semibold text-le-gris-dispositif-dark hover:text-black text-sm tracking-wider uppercase transition-all duration-200 ease-out-back s-y_bCXRrkrYfP" on:click={() => { diff --git a/src/lib/components/test_cases/TestCaseGraph.svelte b/src/lib/components/test_cases/TestCaseGraph.svelte index 794d4dc197d52020ae921a4e159c58b8472433ed..72b573a829569b3ac4c0a0712ff68801e1cbf654 100644 --- a/src/lib/components/test_cases/TestCaseGraph.svelte +++ b/src/lib/components/test_cases/TestCaseGraph.svelte @@ -168,8 +168,6 @@ niveauDeVieByCalculationName["revaluation"], ) - $: console.log("deciles", deciles) - $: childrenId = Object.values(familySituation).reduce( (children: string[], family) => [ ...children, diff --git a/src/lib/components/variables/VariableDetail.svelte b/src/lib/components/variables/VariableDetail.svelte index d5449fe563b8b3dd67cad21f2ae0336b6b2420dc..e60445fec88574b39d98cbeefeb75422f5c3d0cf 100644 --- a/src/lib/components/variables/VariableDetail.svelte +++ b/src/lib/components/variables/VariableDetail.svelte @@ -14,7 +14,7 @@ import Accordion from "$lib/components/accordion/Accordion.svelte" import AccordionItem from "$lib/components/accordion/AccordionItem.svelte" import PictoBudgetEtat from "$lib/components/pictos/PictoBudgetEtat.svelte" - import TestCaseDixiemes from "$lib/components/test_cases/TestCaseDixiemes.svelte" + import TestCaseFilters from "$lib/components/test_cases/TestCaseFilters.svelte" import TestCasePictos from "$lib/components/test_cases/TestCasePictos.svelte" import VariableDetailBudget from "$lib/components/variables/VariableDetailBudget.svelte" import VariableValueChange from "$lib/components/variables/VariableValueChange.svelte" @@ -23,6 +23,7 @@ decompositionCoreByNameByReformName, type EvaluationByName, } from "$lib/decompositions" + import type { DisplayMode } from "$lib/displays" import { memoUrlByName } from "$lib/memos" import type { Situation } from "$lib/situations" import { newSimulationUrl } from "$lib/urls" @@ -32,12 +33,14 @@ variableSummaryByNameByReformName, } from "$lib/variables" + export let displayMode: DisplayMode export let name: string const billName = getContext("billName") as Writable<string | undefined> const budgetSimulation = getContext("budgetSimulation") as Writable< BudgetSimulation | undefined > + let budgetSimulationOutdated = false const dateFormatter = new Intl.DateTimeFormat("fr-FR", { dateStyle: "medium", }).format @@ -118,8 +121,12 @@ ) $: if (name) { - // Only called when the variable name changes - requestBudgetCalculations() + budgetSimulationOutdated = true + setTimeout(() => { + // Only called when the variable name changes + requestBudgetCalculations() + budgetSimulationOutdated = false + }, 500) } function requestBudgetCalculations() { @@ -264,8 +271,11 @@ title="Impacts budgétaires" > {#if budgetVariablesName.has(name)} - {#if $budgetSimulation === undefined} - Chargement des données budgétaires... + {#if budgetSimulationOutdated || $budgetSimulation === undefined} + <div class="flex flex-col gap-2 animate-pulse-2"> + <div class="self-stretch h-6 mr-64 bg-neutral-300" /> + <div class="self-stretch h-6 mr-48 bg-neutral-300" /> + </div> {:else if $budgetSimulation.errors != null && $budgetSimulation.errors.length > 0} {#each $budgetSimulation.errors as error} <span>{error}</span> @@ -351,31 +361,35 @@ </div> {/if} <span class="flex-1 text-start text-sm"> - {title} + <span class="font-bold">{title.split("|")[0]}</span> + {#if title.split("|")[1] !== undefined} + {#if title.split("|")[0].length > 0} + | + {/if} + {title.split("|")[1]} + {/if} </span> </div> - <div class="flex justify-between"> - <span class="text-sm text-gray-500"> - {shortLabel} : - <span class="inline-block"> - {#each indices as index, i} - {#if i > 0} - vs - {/if} - <VariableValueChange - bold={true} - evaluationByName={$evaluationByNameArray[index]} - inline - {name} - /> - {/each} - </span> + <span class="text-sm text-gray-500"> + {shortLabel} : + <span class="inline-block"> + {#each indices as index, i} + {#if i > 0} + vs + {/if} + <VariableValueChange + bold={true} + evaluationByName={$evaluationByNameArray[index]} + inline + {name} + /> + {/each} </span> - <iconify-icon - class="text-le-vert-400 text-xl self-end group-hover:text-le-jaune-very-dark" - icon="ri-arrow-right-line" - /> - </div> + </span> + <iconify-icon + class="text-le-vert-400 text-xl self-end group-hover:text-le-jaune-very-dark" + icon="ri-arrow-right-line" + /> </a> {/each} </div> @@ -385,15 +399,18 @@ > {shortLabel} </span >» avec des cas types représentatifs : </span> - <TestCaseDixiemes + <TestCaseFilters + {displayMode} on:select={({ detail }) => goto( newSimulationUrl({ + ...displayMode, parametersVariableName: name, testCasesIndex: [detail], tab: "dispositif", }), )} + showOnlyDeciles={true} testCases={$testCases} variableName={name} year={$year} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 532af997010d45d8d0a1f7077c161d9108641b5b..98b09e82f57ffcf5f19716b701c26ce085baf3c8 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1865,34 +1865,35 @@ >{title.split("|")[0]}</span > {#if title.split("|")[1] !== undefined} - | {title.split("|")[1]} + {#if title.split("|")[0].length > 0} + | + {/if} + {title.split("|")[1]} {/if} </span> </div> - <div class="flex justify-between"> - <span class="text-sm text-gray-500"> - {shortLabel} : - <span class="inline-block"> - {#each indices as index, i} - {#if i > 0} - vs - {/if} - <VariableValueChange - bold={true} - evaluationByName={$evaluationByNameArray[ - index - ]} - inline - name={displayMode.parametersVariableName} - /> - {/each} - </span> + <span class="text-sm text-gray-500"> + {shortLabel} : + <span class="inline-block"> + {#each indices as index, i} + {#if i > 0} + vs + {/if} + <VariableValueChange + bold={true} + evaluationByName={$evaluationByNameArray[ + index + ]} + inline + name={displayMode.parametersVariableName} + /> + {/each} </span> - <iconify-icon - class="text-le-vert-400 text-xl self-end group-hover:text-le-jaune-very-dark" - icon="ri-arrow-right-line" - /> - </div> + </span> + <iconify-icon + class="text-le-vert-400 text-xl self-end group-hover:text-le-jaune-very-dark" + icon="ri-arrow-right-line" + /> </a> {/each} </div> @@ -2127,6 +2128,7 @@ {#if $variableModalOpen && displayMode.parametersVariableName !== undefined} <VariableDetail + {displayMode} name={displayMode.parametersVariableName} on:close={() => ($variableModalOpen = false)} /> diff --git a/src/routes/accueil/+page.svelte b/src/routes/accueil/+page.svelte index bb2a8cbf16dc8dc2c98ac69d2889730805253c6a..ad2a04dc6141645e12069a1b460a51a1b5d02054 100644 --- a/src/routes/accueil/+page.svelte +++ b/src/routes/accueil/+page.svelte @@ -1108,6 +1108,7 @@ {#if displayMode.parametersVariableName !== undefined} <VariableDetail + {displayMode} name={displayMode.parametersVariableName} on:close={() => goto(