diff --git a/src/lib/components/Toggletip.svelte b/src/lib/components/Toggletip.svelte new file mode 100644 index 0000000000000000000000000000000000000000..92cefb2f7aac9885fa3ae3c786219a9d129650d1 --- /dev/null +++ b/src/lib/components/Toggletip.svelte @@ -0,0 +1,144 @@ +<script lang="ts"> + import { arrow, flip, shift } from "@floating-ui/core" + import { + computePosition, + type Coords, + type Placement, + } from "@floating-ui/dom" + import { v4 as uuidV4 } from "uuid" + + export let allowFlip = true + export let arrowClass = "" + export let arrowBorderWidth = "1px" + export let classes = "" + export let initialPlacement: Placement = "bottom" + export let role = "presentation" + export let show = false + export let tag = "div" + export let widthClass = "w-1/3" + + let arrowCoords: (Partial<Coords> & { centerOffset: number }) | undefined + let arrowElement: HTMLElement | SVGSVGElement | null = null + let arrowSpace = 10 + let arrowHeight = 4 + let placement = initialPlacement + let referenceElement: HTMLElement | null = null + + let toggletipElement: HTMLElement | null = null + let uuid = uuidV4() + + $: updateToggletip(show) + + function updateToggletip(showToggletip: boolean) { + if (showToggletip) { + if ( + referenceElement === null || + toggletipElement === null || + arrowElement === null + ) + return + + positionToggletip(referenceElement, toggletipElement, arrowElement) + } + } + + async function positionToggletip( + referenceElement: HTMLElement, + toggletipElement: HTMLElement, + arrowElement: HTMLElement | SVGSVGElement, + ): Promise<void> { + const { + x, + y, + middlewareData, + placement: computedPlacement, + } = await computePosition(referenceElement, toggletipElement, { + placement: initialPlacement, + middleware: [ + ...(allowFlip ? [flip()] : []), + shift(), + arrow({ element: arrowElement }), + ], + }) + placement = computedPlacement + Object.assign(toggletipElement.style, { + left: `${x}px`, + top: `${y}px`, + }) + + if (middlewareData.arrow) { + arrowCoords = { ...middlewareData.arrow } + } + } +</script> + +<svelte:element + this={tag} + aria-describedby="toggletip-{uuid}" + class="inline-block {classes}" + {role} + bind:this={referenceElement} +> + <slot /> +</svelte:element> +<div + class="absolute z-50 {widthClass} {$$props.class ?? ''}" + id="toggletip-{uuid}" + bind:this={toggletipElement} + role="status" + style:padding-top={placement === "bottom" ? `${arrowSpace}px` : undefined} + style:padding-right={placement === "left" ? `${arrowSpace}px` : undefined} + style:padding-bottom={placement === "top" ? `${arrowSpace}px` : undefined} + style:padding-left={placement === "right" ? `${arrowSpace}px` : undefined} + style:display={show ? "block" : "none"} +> + <slot name="toggletip" /> + <div + bind:this={arrowElement} + class="{arrowClass} absolute w-1 h-1 after:absolute after:rotate-45 after:w-[0.5625rem] after:h-[0.5625rem] after:bg-inherit after:border-inherit after:top-[-2px] after:left-[-2.5px]" + style:top={placement === "top" + ? `calc(100% - ${arrowSpace + arrowHeight / 2}px)` + : arrowCoords?.y + ? `${arrowCoords?.y}px` + : undefined} + style:right={placement === "right" + ? `calc(100% - ${arrowSpace + arrowHeight / 2}px)` + : arrowCoords?.x + ? `${arrowCoords?.x}px` + : undefined} + style:bottom={placement === "bottom" + ? `calc(100% - ${arrowSpace + arrowHeight / 2}px)` + : arrowCoords?.y + ? `${arrowCoords?.y}px` + : undefined} + style:left={placement === "left" + ? `calc(100% - ${arrowSpace + arrowHeight / 2}px)` + : arrowCoords?.x + ? `${arrowCoords?.x}px` + : undefined} + style="--arrow-border-width: {arrowBorderWidth};" + class:after:border-t={placement === "bottom" || placement === "left"} + class:border-t-width={placement === "bottom" || placement === "left"} + class:after:border-r={placement === "top" || placement === "left"} + class:border-r-width={placement === "top" || placement === "left"} + class:after:border-b={placement === "top" || placement === "right"} + class:border-b-width={placement === "top" || placement === "right"} + class:after:border-l={placement === "bottom" || placement === "right"} + class:border-l-width={placement === "bottom" || placement === "right"} + ></div> +</div> + +<style> + .border-t-width::after { + border-top-width: var(--arrow-border-width); + } + .border-r-width::after { + border-right-width: var(--arrow-border-width); + } + .border-b-width::after { + border-bottom-width: var(--arrow-border-width); + } + .border-l-width::after { + border-left-width: var(--arrow-border-width); + } +</style> diff --git a/src/lib/components/test_cases/TestCaseEdit.svelte b/src/lib/components/test_cases/TestCaseEdit.svelte index 4a5c0536c9ea1cd395e45604f5e84f3a4a8befc6..cd564368ea4bc2260732e7347c7833f9ead80ce0 100644 --- a/src/lib/components/test_cases/TestCaseEdit.svelte +++ b/src/lib/components/test_cases/TestCaseEdit.svelte @@ -11,7 +11,6 @@ } from "@openfisca/json-model" import { createEventDispatcher, getContext } from "svelte" import type { Writable } from "svelte/store" - import { fade } from "svelte/transition" import { page } from "$app/stores" import type { RequestedCalculations } from "$lib/calculations" @@ -19,7 +18,11 @@ import PictoBigEnfant from "$lib/components/pictos/PictoBigEnfant.svelte" import PictoBigParent from "$lib/components/pictos/PictoBigParent.svelte" import RolePersonsEdit from "$lib/components/test_cases/RolePersonsEdit.svelte" + import TestCaseEditVariablesSearch from "$lib/components/test_cases/TestCaseEditVariablesSearch.svelte" + import Toggletip from "$lib/components/Toggletip.svelte" + import Tooltip from "$lib/components/Tooltip.svelte" import VariableInput from "$lib/components/variables/VariableInput.svelte" + import VariableReferredInputsPane from "$lib/components/variables/VariableReferredInputsPane.svelte" import { decompositionCoreByName } from "$lib/decompositions" import { entityByKey, personEntityKey } from "$lib/entities" import { @@ -28,6 +31,7 @@ type Situation, } from "$lib/situations" import { iterSituationVariablesName } from "$lib/situations" + import { valueFormatter } from "$lib/values" import type { ValuesByCalculationNameByVariableName } from "$lib/variables" import { variableSummaryByName } from "$lib/variables" @@ -38,20 +42,45 @@ export let situation: Situation export let situationIndex: number export let valuesByCalculationNameByVariableName: ValuesByCalculationNameByVariableName + export let variableName: string | undefined export let year: number let currentInputInstantsByVariableName = inputInstantsByVariableName let currentSituation = situation - let openMoreAggregatKids = false const dispatch = createEventDispatcher() const hiddenEntitiesKeyPlural = ($page.data.hiddenEntitiesKeyPlural ?? []) as string[] + let openMoreAggregatKids = false const personEntity = entityByKey[personEntityKey] as PersonEntity const requestedCalculations = getContext( "requestedCalculations", ) as Writable<RequestedCalculations> let showAdultsAlert = false let showAdultsAlertTimeout: number | undefined = undefined + let taxableHouseholdCountByChildrenCount: { + [childrenCount: number | string]: string + } = { + 0: "29 324 900", + 1: "4 658 436", + 2: "3 567 969", + 3: "1 260 473", + 4: "309 947", + 5: "81 293", + 6: "24 035", + 7: "10 488", + 8: "4 102", + 9: "1 699", + 10: "700", + 11: "327", + 12: "165", + 13: "89", + 14: "43", + 15: "moins de 12", + 16: "moins de 12", + 17: "moins de 12", + 18: "moins de 12", + 19: "moins de 12", + } let variablesName = new Set( Object.entries(decompositionCoreByName) .filter(([, decomposition]) => !decomposition.virtual) @@ -95,6 +124,22 @@ $: updateVariablesName(situation) + $: variablesNameSortOrder = [ + ...((adultsId?.length ?? 1) >= 2 ? ["statut_marital"] : []), + "salaire_de_base", + "retraite_brute", + "chomage_brut", + "activite", + "categorie_salarie", + "contrat_de_travail_type", + "contrat_de_travail", + "heures_remunerees_volume", + "statut_occupation_logement", + "loyer", + "depcom", + "depcom_foyer", + ] + function addGroup(key: string): void { const entity = entityByKey[key] const groupById = { @@ -208,9 +253,8 @@ ) } - async function changeAdultsCount({ target }: Event): Promise<void> { - const { value } = target as HTMLInputElement - const adultsCount = parseInt(value) + async function changeAdultsCount(value: string | number): Promise<void> { + const adultsCount = parseInt(`${value}`) while (adultsId.length < adultsCount) { let adultId: string for (let i = 1; i < 100; i++) { @@ -236,9 +280,8 @@ }, 5000) } - async function changeChildrenCount({ target }: Event): Promise<void> { - const { value } = target as HTMLInputElement - const childrenCount = parseInt(value) + async function changeChildrenCount(value: string | number): Promise<void> { + const childrenCount = parseInt(`${value}`) while (childrenId.length < childrenCount) { let childId: string for (let i = 1; i < 100; i++) { @@ -428,6 +471,24 @@ } } + function sortVariableNames(variablesName: Set<string>, order: string[]) { + const sortedVariableNames: Set<string> = new Set() + + for (const variableName of order) { + if (variablesName.has(variableName)) { + sortedVariableNames.add(variableName) + } + } + + for (const variableName of [...variablesName]) { + if (!sortedVariableNames.has(variableName)) { + sortedVariableNames.add(variableName) + } + } + + return sortedVariableNames + } + function updateInputInstantsByVariableName(inputInstantsByVariableName: { [name: string]: Set<string> }): void { @@ -458,356 +519,497 @@ } </script> -<h3 class="mx-5 mb-3 mt-3 text-xl">Composition du cas type :</h3> - -<section class="ml-4 mr-4 rounded border-gray-200 bg-gray-200 py-2 md:mr-0"> - <div - class="mx-4 flex flex-col items-start gap-2 sm:flex-row md:flex-col lg:flex-row" - > - <div class="flex w-full sm:w-1/2 md:w-full lg:w-1/2 items-start"> - <div class="flex flex-col w-1/2"> - <div class="mt-2 flex items-center justify-center"> - <PictoBigParent /> - <label class="mx-2"> - Adultes - <input - class="w-10 rounded border-none p-1 text-base focus:border-le-gris-dispositif-dark focus:text-le-gris-dispositif-dark" - min={1} - on:change={changeAdultsCount} - step="1" - type="number" - value={adultsId.length} - /> - </label> - </div> - <div class="relative flex mt-3"> - {#if showAdultsAlert && adultsId.length === 1} - <div - class="absolute bg-gray-100 text-sm text-yellow-900 p-2" - in:fade={{ delay: 200 }} - out:fade={{ duration: 100 }} - > - <iconify-icon - icon="ri-information-fill" - class="mr-1 align-[-0.2rem] text-base text-yellow-600" +<div class="relative h-full flex flex-col md:flex-row gap-3 px-3 md:px-5 pt-2"> + <section class="flex-[2] md:overflow-y-scroll flex flex-col gap-3 2xl:gap-12"> + <span + class="inline-block text-black text-base md:text-lg 2xl:text-xl leading-8" + > + Composition : + </span> + <div class="w-full grid grid-cols-2 gap-x-4"> + <div + class="flex flex-col justify-end items-center gap-1 px-4 py-2 bg-neutral-100 rounded-lg" + > + <PictoBigParent /> + <span class="mx-2 text-center">Adultes</span> + <div class="relative"> + <Toggletip + arrowClass="bg-white border-yellow-900" + arrowBorderWidth="2px" + widthClass="w-60" + initialPlacement="bottom" + show={showAdultsAlert} + > + <div class="flex gap-1"> + <button + disabled={adultsId.length <= 1} + class="flex items-center gap-1 p-1 enabled:hover:bg-gray-400 enabled:hover:bg-opacity-20 enabled:active:bg-gray-400 enabled:active:bg-opacity-40 rounded-lg disabled:text-neutral-400 enabled:text-neutral-600 enabled:hover:text-black text-sm tracking-wider uppercase transition-all duration-200 ease-out-back" + on:click={() => { + if (adultsId.length > 1) { + changeAdultsCount(adultsId.length - 1) + } + }} + title="Enlever un adulte" + > + <iconify-icon + class="align-[-0.2rem] text-xl" + icon="ri-subtract-line" + /> + </button> + <input + class="w-10 rounded border-none p-1 text-base shadow-sm focus:border-le-gris-dispositif-dark focus:text-le-gris-dispositif-dark" + min={1} + on:change={({ target }) => changeAdultsCount(target?.value)} + step="1" + type="number" + value={adultsId.length} /> - Veuillez vérifier le statut marital de cet adulte ⬇️. - {#if adultSituations !== undefined} - Actuellement, il est {getStatutMarital(adultSituations)}. - {/if} + <button + class="flex items-center gap-1 p-1 hover:bg-gray-400 hover:bg-opacity-20 active:bg-gray-400 active:bg-opacity-40 rounded-lg text-neutral-600 hover:text-black text-sm tracking-wider uppercase transition-all duration-200 ease-out-back" + on:click={() => changeAdultsCount(adultsId.length + 1)} + title="Ajouter un adulte" + > + <iconify-icon + class="align-[-0.2rem] text-xl" + icon="ri-add-line" + /> + </button> </div> - {:else if showAdultsAlert && adultsId.length > 1} <div - class="absolute bg-gray-100 text-sm text-yellow-900 p-2" - in:fade={{ delay: 200 }} - out:fade={{ duration: 100 }} + class="z-40 px-3 py-2 text-left text-gray-600 bg-white border-2 border-yellow-900 rounded-lg shadow-md" + slot="toggletip" > <iconify-icon icon="ri-information-fill" class="mr-1 align-[-0.2rem] text-base text-yellow-600" /> - Veuillez vérifier le statut marital de ces adultes ⬇️. - {#if adultSituations !== undefined} - Actuellement, ils sont : - <ul class="list-disc"> - {#each getStatutMarital(adultSituations) as statut} - <li class="ml-6">{statut}</li> - {/each} - </ul> + {#if adultsId.length === 1} + Veuillez vérifier le statut marital de cet adulte ➡️. + <br /> + {#if adultSituations !== undefined} + Actuellement, il est {getStatutMarital(adultSituations)}. + {/if} + {:else} + Veuillez vérifier le statut marital de ces adultes ➡️. + <br /> + {#if adultSituations !== undefined} + Actuellement, ils sont : + <ul class="list-disc"> + {#each getStatutMarital(adultSituations) as statut} + <li class="ml-6">{statut}</li> + {/each} + </ul> + {/if} {/if} </div> - {/if} + </Toggletip> </div> </div> - <div class="mt-2 flex w-1/2 items-center"> + + <div + class="relative flex flex-col justify-end items-center gap-1 px-4 py-2 bg-neutral-100 rounded-lg" + > <PictoBigEnfant /> - <label class="mx-2"> - <abbr title="Enfants ou personnes à charge">Enfants </abbr> + <abbr class="mx-2 text-center" title="Enfants ou personnes à charge"> + Enfants + </abbr> + <div class="flex gap-1"> + <button + disabled={childrenId.length <= 0} + class="flex items-center gap-1 p-1 enabled:hover:bg-gray-400 enabled:hover:bg-opacity-20 enabled:active:bg-gray-400 enabled:active:bg-opacity-40 rounded-lg disabled:text-neutral-400 enabled:text-neutral-600 enabled:hover:text-black text-sm tracking-wider uppercase transition-all duration-200 ease-out-back" + on:click={() => { + if (childrenId.length > 0) { + changeChildrenCount(childrenId.length - 1) + } + }} + title="Enlever un enfant" + > + <iconify-icon + class="align-[-0.2rem] text-xl" + icon="ri-subtract-line" + /> + </button> <input - class="w-10 rounded border-none p-1 text-base focus:border-le-gris-dispositif-dark focus:text-le-gris-dispositif-dark" + class="w-10 rounded border-none p-1 text-base shadow-sm focus:border-le-gris-dispositif-dark focus:text-le-gris-dispositif-dark" min={0} - on:change={changeChildrenCount} + on:change={({ target }) => changeChildrenCount(target?.value)} step="1" type="number" value={childrenId.length} /> - </label> - </div> - </div> - <div - class="my-2 w-full rounded-md bg-le-gris-dispositif-light p-3 sm:w-1/2 md:w-full lg:w-1/2" - > - <p class="font-serif text-base"> - En 2020, - {#if childrenId.length === 0} - <span class="font-bold">29 324 900 foyers fiscaux</span> déclarent aucun - enfant - {:else if childrenId.length === 1} - <span class="font-bold">4 658 436 foyers fiscaux</span> déclarent 1 enfant - {:else if childrenId.length === 2} - <span class="font-bold">3 567 969 foyers fiscaux</span> déclarent 2 enfants - {:else if childrenId.length === 3} - <span class="font-bold">1 260 473 foyers fiscaux</span> déclarent 3 enfants - {:else if childrenId.length === 4} - <span class="font-bold">309 947 foyers fiscaux</span> déclarent 4 enfants - {:else if childrenId.length === 5} - <span class="font-bold">81 293 foyers fiscaux</span> déclarent 5 enfants - {:else if childrenId.length === 6} - <span class="font-bold">24 035 foyers fiscaux</span> déclarent 6 enfants - {:else if childrenId.length === 7} - <span class="font-bold">10 488 foyers fiscaux</span> déclarent 7 enfants - {:else if childrenId.length === 8} - <span class="font-bold">4 102 foyers fiscaux</span> déclarent 8 enfants - {:else if childrenId.length === 9} - <span class="font-bold">1 699 foyers fiscaux</span> déclarent 9 enfants - {:else if childrenId.length === 10} - <span class="font-bold">700 foyers fiscaux</span> déclarent 10 enfants - {:else if childrenId.length === 11} - <span class="font-bold">327 foyers fiscaux</span> déclarent 11 enfants - {:else if childrenId.length === 12} - <span class="font-bold">165 foyers fiscaux</span> déclarent 12 enfants - {:else if childrenId.length === 13} - <span class="font-bold">89 foyers fiscaux</span> déclarent 13 enfants - {:else if childrenId.length === 14} - <span class="font-bold">43 foyers fiscaux</span> déclarent 14 enfants - {:else if childrenId.length === 15} - <span class="font-bold">moins de 12 foyers fiscaux</span> déclarent 15 - enfants - {:else if childrenId.length === 16} - <span class="font-bold">moins de 12 foyers fiscaux</span> déclarent 16 - enfants - {:else if childrenId.length === 17} - <span class="font-bold">moins de 12 foyers fiscaux</span> déclarent 17 - enfants - {:else if childrenId.length === 18} - <span class="font-bold">moins de 12 foyers fiscaux</span> déclarent 18 - enfants - {:else if childrenId.length === 19} - <span class="font-bold">moins de 12 foyers fiscaux</span> déclarent 19 - enfants - {:else if childrenId.length > 19} - <span class="font-bold">aucun foyer fiscal</span> ne déclare plus de 19 - enfants - {/if} de moins de 18 ans*. - </p> - <p - class="my-2 ml-10 justify-self-end text-right font-serif text-sm text-gray-700" - > - LexImpact, <span class="italic" - >agrégat extrait de la <a - class="lx-link-text" - href="https://www.casd.eu/source/declarations-dimpot-sur-le-revenu-des-foyers-fiscaux-formulaire-2042-et-annexes/" - rel="noreferrer" - target="_blank">base de donnée POTE</a + <button + class="flex items-center gap-1 p-1 hover:bg-gray-400 hover:bg-opacity-20 active:bg-gray-400 active:bg-opacity-40 rounded-lg text-neutral-600 hover:text-black text-sm tracking-wider uppercase transition-all duration-200 ease-out-back" + on:click={() => changeChildrenCount(childrenId.length + 1)} + title="Ajouter un enfant" > - publiée par la DGFIP</span - >, Millésime 2019, extraction du 03 novembre 2022. <br /> - </p> - <button - class="inline-block font-sans text-sm" - on:click={() => (openMoreAggregatKids = !openMoreAggregatKids)} + <iconify-icon class="align-[-0.2rem] text-xl" icon="ri-add-line" /> + </button> + </div> + </div> + + <div /> + + <div + class="bg-le-gris-dispositif-light px-1 pt-2.5 pb-0.5 -mt-2 text-sm rounded-b-lg" > - <span>* En savoir plus sur l'agrégat et la source</span> - {#if !openMoreAggregatKids}<iconify-icon - class="inline-flex h-5 w-5" - icon="ri:arrow-down-s-line" - /> - {:else} - <iconify-icon class="inline-flex h-5 w-5" icon="ri:arrow-up-s-line" /> - {/if} - </button> + <span> + En 2020, + <span class="font-bold"> + {#if Object.hasOwn(taxableHouseholdCountByChildrenCount, childrenId.length)} + {@const taxableHouseholdCount = + taxableHouseholdCountByChildrenCount[childrenId.length]} + {#if !isNaN(Number(taxableHouseholdCount.replaceAll(" ", "")))} + {valueFormatter( + 0, + "people", + true, + )(Number(taxableHouseholdCount.replaceAll(" ", "")))} + {:else} + {taxableHouseholdCount} + {/if} + foyers fiscaux + {:else} + aucun foyer fiscal + {/if} + </span> + {#if !Object.hasOwn(taxableHouseholdCountByChildrenCount, childrenId.length)} + {@const maxChildrenCount = Number( + Object.keys(taxableHouseholdCountByChildrenCount).sort( + (a, b) => Number(b) - Number(a), + )[0], + )} + ne déclare plus de {maxChildrenCount} enfant{maxChildrenCount > 1 + ? "s" + : ""} + {:else} + {#if childrenId.length === 0} + ne déclarent aucun + {:else} + déclarent {childrenId.length} + {/if} + enfant{childrenId.length > 1 ? "s" : ""} + {/if} + </span> - {#if openMoreAggregatKids === true} - <div - class="mt-2 rounded-b-sm bg-le-gris-dispositif-ultralight p-2 text-gray-700" + <Tooltip + arrowClass="bg-le-gris-dispositif-light border-transparent" + widthClass="w-80" + initialPlacement="top" > - <p class="class-sm mt-2 text-sm"> - <span class="font-bold" - >Qu'est ce que représente la base de données POTE ?</span - ><br /> - La base POTE rassemble les informations recensées à l’occasion de la - déclaration 2020 sur les revenus 2019 grâce au - <a - class="lx-link-text" - href="https://www.impots.gouv.fr/formulaire/2042/declaration-des-revenus" - rel="noreferrer" - target="_blank">formulaire n°2042 et ses annexes</a - >. - </p> - <p class="class-sm mt-2 text-sm"> - <span class="font-bold" - >Quel est le périmètre des enfants comptabilisés ?</span - ><br /> - Le périmètre englobe les enfants à charge, de moins de 18 ans au 1er - janvier de l'année de perception des revenus, ou nés durant la même année, - ou bien en situation de handicap quel que soit leur âge. Les enfants - mariés ou en résidence alternée ne sont pas comptés. Ces agrégats proviennent - de la case "F" de la partie "C - Personnes à charges" des déclarations - 2019 faites avec le formulaire n°2042. - </p> - <p class="class-sm mt-2 text-sm"> - <span class="font-bold" - >Cette base de donnée est-elle représentative de la population - française ?</span - ><br /> - Cette base est plutôt représentative de la population française, voici - quelques précisions : - </p> - <ul class="list-inside list-disc text-sm"> - <li> - certains individus n'effectuent pas de déclaration de revenus et - ne sont donc pas comptabilisés. Cette situation reste marginale - car la déclaration est obligatoire dans de nombreux cas explicités - à - <a - href="https://www.casd.eu/source/declarations-dimpot-sur-le-revenu-des-foyers-fiscaux-formulaire-2042-et-annexes/" - rel="noreferrer" - target="_blank" - class="lx-link-text">cette page du CASD</a - > ; - </li> - <li> - parmi les foyers fiscaux, certains déclarants sont de nationalité - étrangère ; - </li> - <li> - les individus de nationalité française résidant à l'étranger sont - faiblement représentés car, dans la majorité des cas, ils n'ont - pas cette déclaration de revenus à faire. - </li> - </ul> - </div> - {/if} - </div> - </div> - - <div class="flex justify-center"> - {#each [...iterGroupEntities(entityByKey)] as groupEntity} - {#if !hiddenEntitiesKeyPlural.includes(groupEntity.key_plural)} - <section class="m-2"> - <!-- {#if groupEntity.documentation !== undefined} - <p>{groupEntity.documentation}</p> - {/if} --> - {#if countGroups(situation, groupEntity) === 1} - <div class="mb-3"> - <label class="text-sm"> - <input - checked - class="checked rounded accent-gray-500" - on:click={() => addGroup(groupEntity.key)} - type="checkbox" + <span + class="font-normal underline decoration-dotted hover:text-black" + > + <iconify-icon + class="align-[-0.15rem] text-sm" + icon="ri-information-line" + /></span + > + <div + slot="tooltip" + class="overflow-hidden bg-le-gris-dispositif-light p-3 text-start text-sm font-normal rounded-lg shadow-md" + > + <p class="font-serif text-base"> + En 2020, + <span class="font-bold"> + {#if Object.hasOwn(taxableHouseholdCountByChildrenCount, childrenId.length)} + {taxableHouseholdCountByChildrenCount[childrenId.length]} + foyers fiscaux + {:else} + aucun foyer fiscal + {/if} + </span> + {#if !Object.hasOwn(taxableHouseholdCountByChildrenCount, childrenId.length)} + {@const maxChildrenCount = Number( + Object.keys(taxableHouseholdCountByChildrenCount).sort( + (a, b) => Number(b) - Number(a), + )[0], + )} + ne déclare plus de {maxChildrenCount} enfant{maxChildrenCount > + 1 + ? "s" + : ""} + {:else} + {#if childrenId.length === 0} + ne déclarent aucun + {:else} + déclarent {childrenId.length} + {/if} + enfant{childrenId.length > 1 ? "s" : ""} + {/if} + de moins de 18 ans*. + </p> + <p + class="my-2 ml-10 justify-self-end text-right font-serif text-sm text-gray-700" + > + LexImpact, <span class="italic" + >agrégat extrait de la <a + class="link" + href="https://www.casd.eu/source/declarations-dimpot-sur-le-revenu-des-foyers-fiscaux-formulaire-2042-et-annexes/" + rel="noreferrer" + target="_blank">base de donnée POTE</a + > + publiée par la DGFIP</span + >, Millésime 2019, extraction du 03 novembre 2022. <br /> + </p> + <button + class="inline-block font-sans text-sm" + on:click={() => (openMoreAggregatKids = !openMoreAggregatKids)} + > + <span>* En savoir plus sur l'agrégat et la source</span> + {#if !openMoreAggregatKids}<iconify-icon + class="inline-flex h-5 w-5" + icon="ri:arrow-down-s-line" /> - {groupEntity.is_single_label ?? - `${groupEntity.label ?? groupEntity.key} unique`} - </label> - </div> - {/if} - <ul class="mr-2"> - {#each [...iterGroups(situation, groupEntity)] as [groupId, group], index (`${groupEntity.key_plural}.${index}`)} - <li> - {#if countGroups(situation, groupEntity) > 1} - <div class="mt-2 flex items-center text-sm"> - <div> - {group.name ?? groupId} : - </div> - {#if index > 0} + {:else} + <iconify-icon + class="inline-flex h-5 w-5" + icon="ri:arrow-up-s-line" + /> + {/if} + </button> + + {#if openMoreAggregatKids} + <div + class="mt-2 rounded-b-sm bg-le-gris-dispositif-ultralight p-2 text-gray-700" + > + <p class="class-sm mt-2 text-sm"> + <span class="font-bold" + >Qu'est ce que représente la base de données POTE ?</span + ><br /> + La base POTE rassemble les informations recensées à l’occasion + de la déclaration 2020 sur les revenus 2019 grâce au + <a + class="link" + href="https://www.impots.gouv.fr/formulaire/2042/declaration-des-revenus" + rel="noreferrer" + target="_blank">formulaire n°2042 et ses annexes</a + >. + </p> + <p class="class-sm mt-2 text-sm"> + <span class="font-bold" + >Quel est le périmètre des enfants comptabilisés ?</span + ><br /> + Le périmètre englobe les enfants à charge, de moins de 18 ans au + 1er janvier de l'année de perception des revenus, ou nés durant + la même année, ou bien en situation de handicap quel que soit leur + âge. Les enfants mariés ou en résidence alternée ne sont pas comptés. + Ces agrégats proviennent de la case "F" de la partie "C - Personnes + à charges" des déclarations 2019 faites avec le formulaire n°2042. + </p> + <p class="class-sm mt-2 text-sm"> + <span class="font-bold" + >Cette base de donnée est-elle représentative de la + population française ?</span + ><br /> + Cette base est plutôt représentative de la population française, + voici quelques précisions : + </p> + <ul class="list-inside list-disc text-sm"> + <li> + certains individus n'effectuent pas de déclaration de + revenus et ne sont donc pas comptabilisés. Cette situation + reste marginale car la déclaration est obligatoire dans de + nombreux cas explicités à + <a + href="https://www.casd.eu/source/declarations-dimpot-sur-le-revenu-des-foyers-fiscaux-formulaire-2042-et-annexes/" + rel="noreferrer" + target="_blank" + class="link">cette page du CASD</a + > ; + </li> + <li> + parmi les foyers fiscaux, certains déclarants sont de + nationalité étrangère ; + </li> + <li> + les individus de nationalité française résidant à l'étranger + sont faiblement représentés car, dans la majorité des cas, + ils n'ont pas cette déclaration de revenus à faire. + </li> + </ul> + </div> + {/if} + </div> + </Tooltip> + </div> + </div> + + <div class="flex justify-center gap-2"> + {#each [...iterGroupEntities(entityByKey)] as groupEntity} + {#if !hiddenEntitiesKeyPlural.includes(groupEntity.key_plural)} + <section> + <!-- {#if groupEntity.documentation !== undefined} + <p>{groupEntity.documentation}</p> + {/if} --> + {#if countGroups(situation, groupEntity) === 1} + <div class="mb-3"> + <label class="text-sm"> + <input + checked + class="checked rounded accent-gray-500" + on:click={() => addGroup(groupEntity.key)} + type="checkbox" + /> + {groupEntity.is_single_label ?? + `${groupEntity.label ?? groupEntity.key} unique`} + </label> + </div> + {/if} + <ul class="mr-2"> + {#each [...iterGroups(situation, groupEntity)] as [groupId, group], index (`${groupEntity.key_plural}.${index}`)} + <li> + {#if countGroups(situation, groupEntity) > 1} + <div class="mt-2 flex items-center text-sm"> <div> - <button - class="ml-1 text-gray-500 enabled:hover:text-black" - disabled={!isGroupEmpty(groupEntity, group)} - on:click={() => - deleteGroup(groupEntity, groupId, group)} - title="Supprimer le groupe" - type="button" - > - <iconify-icon - class="h-5 w-5" - icon="ri-close-circle-fill" - /> - </button> + {group.name ?? groupId} : </div> - {/if} - </div> - {/if} + {#if index > 0} + <div> + <button + class="ml-1 text-gray-500 enabled:hover:text-black" + disabled={!isGroupEmpty(groupEntity, group)} + on:click={() => + deleteGroup(groupEntity, groupId, group)} + title="Supprimer le groupe" + type="button" + > + <iconify-icon + class="h-5 w-5" + icon="ri-close-circle-fill" + /> + </button> + </div> + {/if} + </div> + {/if} + + <dl class="ml-2 mt-2 border-l border-black pl-2"> + {#each groupEntity.roles as role} + <dt class="text-sm italic"> + {role.label ?? + (role.max === undefined || role.max > 1 + ? role.key_plural + : role.key)} : + </dt> + <dd> + <RolePersonsEdit + idPrefix="{groupEntity.key_plural}.{groupId}.{role.key}" + {personById} + personsId={getGroupPersonsId(group, role)} + on:change={({ detail }) => + changeRolePersonsId( + groupEntity.key_plural, + groupId, + getRolePersonsIdKey(role), + detail, + )} + type={groupEntity.key_plural} + /> + </dd> + {/each} + </dl> + </li> + {/each} + </ul> + {#if countGroups(situation, groupEntity) > 1} + <button + class="text-gray-700 hover:text-black" + on:click={() => addGroup(groupEntity.key)} + type="button" + > + <div class="mt-2 flex items-center text-sm"> + <svg + class="mr-1 inline fill-current" + xmlns="http://www.w3.org/2000/svg" + height="18px" + viewBox="0 0 24 24" + width="18px" + fill="#000000" + ><path d="M0 0h24v24H0z" fill="none" /><path + d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm5 11h-4v4h-2v-4H7v-2h4V7h2v4h4v2z" + /></svg + >Ajouter un groupe + </div></button + > + {/if} + </section> + {/if} + {/each} + </div> + </section> - <dl class="ml-2 mt-2 border-l border-black pl-2"> - {#each groupEntity.roles as role} - <dt class="text-sm italic"> - {role.label ?? - (role.max === undefined || role.max > 1 - ? role.key_plural - : role.key)} : - </dt> - <dd> - <RolePersonsEdit - idPrefix="{groupEntity.key_plural}.{groupId}.{role.key}" - {personById} - personsId={getGroupPersonsId(group, role)} - on:change={({ detail }) => - changeRolePersonsId( - groupEntity.key_plural, - groupId, - getRolePersonsIdKey(role), - detail, - )} - type={groupEntity.key_plural} - /> - </dd> - {/each} - </dl> - </li> - {/each} - </ul> - {#if countGroups(situation, groupEntity) > 1} - <button - class="text-gray-700 hover:text-black" - on:click={() => addGroup(groupEntity.key)} - type="button" - > - <div class="mt-2 flex items-center text-sm"> - <svg - class="mr-1 inline fill-current" - xmlns="http://www.w3.org/2000/svg" - height="18px" - viewBox="0 0 24 24" - width="18px" - fill="#000000" - ><path d="M0 0h24v24H0z" fill="none" /><path - d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm5 11h-4v4h-2v-4H7v-2h4V7h2v4h4v2z" - /></svg - >Ajouter un groupe - </div></button - > - {/if} - </section> - {/if} - {/each} - </div> -</section> - -<section class="ml-4 mr-4 mt-4 md:mr-0"> - <h3 class="mt-3 text-xl">Caractéristiques par an :</h3> - <p class="my-2 text-sm text-gray-700"> - 📌 Par défaut, la situation du cas type est considérée comme stable depuis 3 - ans. - </p> - <ul> - {#each [...variablesName] as variableName} - {#if (inputInstantsByVariableName[variableName] ?? new Set()).has(yearString)} - <li - class="my-4 flex-col items-baseline rounded bg-gray-200 p-1 py-1 text-base text-black md:p-2" - > - <VariableInput - {date} - bind:inputInstantsByVariableName - bind:situation - {situationIndex} - bind:valuesByCalculationNameByVariableName - variable={variableSummaryByName[variableName]} - {year} - /> - </li> - {/if} - {/each} - </ul> -</section> + <div class="hidden md:block my-9 self-stretch w-[1px] bg-neutral-300" /> + + <section class="flex-[4] md:overflow-y-scroll pb-20"> + <span + class="inline-block mb-1 md:mb-3 mt-3 md:mt-0 text-black text-base md:text-lg 2xl:text-xl leading-8" + > + Caractéristiques : + </span> + + <ul> + {#each [...sortVariableNames(variablesName, variablesNameSortOrder)] as variableName} + {#if (inputInstantsByVariableName[variableName] ?? new Set()).has(yearString)} + <li + class="mb-4 flex-col items-baseline rounded bg-neutral-100 p-1 py-1 text-base text-black md:p-2" + > + <VariableInput + {date} + bind:inputInstantsByVariableName + bind:situation + {situationIndex} + bind:valuesByCalculationNameByVariableName + variable={variableSummaryByName[variableName]} + {year} + /> + </li> + {/if} + {/each} + </ul> + + <TestCaseEditVariablesSearch + {date} + {inputInstantsByVariableName} + {situation} + {situationIndex} + {valuesByCalculationNameByVariableName} + {year} + /> + + <div class="flex gap-9 mb-3 mt-4"> + <hr class="mt-5 flex-1 border-dashed border-black" /> + <span class="text-lg font-light xl:text-2xl 2xl:text-3xl">ou</span> + <hr class="mt-5 flex-1 border-dashed border-black" /> + </div> + + {#if variableName === undefined} + <h2 class="mb-2 pt-3 text-xl font-bold"> + Ajouter d'autres caractéristiques : + </h2> + <p class="font-sm mb-10 hidden text-gray-700 md:block"> + ↖️ Cliquez sur le nom d'un dispositif affiché sur le cas type pour + modifier les variables entrant dans le calcul de ce dispositif. + </p> + <p class="font-sm mx-4 mb-10 block text-gray-700 md:hidden"> + Utilisez votre ordinateur ou élargissez l'écran pour obtenir plus de + fonctionnalités. + </p> + {:else} + <VariableReferredInputsPane + {date} + {inputInstantsByVariableName} + name={variableName} + on:changeInputInstantsByVariableName + on:changeSituation + {situation} + {situationIndex} + {valuesByCalculationNameByVariableName} + {year} + /> + {/if} + </section> + + <div + class="pointer-events-none absolute inset-x-0 bottom-0 h-4 bg-gradient-to-t from-gray-200 to-transparent" + /> +</div> diff --git a/src/lib/components/test_cases/TestCaseEditVariablesSearch.svelte b/src/lib/components/test_cases/TestCaseEditVariablesSearch.svelte index 2622aed7d3cdfb943a927843b50bf8aa0bee61d9..6166c0351b3b305a51ee09465905930d9f4a69f7 100644 --- a/src/lib/components/test_cases/TestCaseEditVariablesSearch.svelte +++ b/src/lib/components/test_cases/TestCaseEditVariablesSearch.svelte @@ -53,12 +53,12 @@ } </script> -<span class="block ml-4 mr-4 md:mr-0 text-lg"> +<span class="block text-lg"> Rechercher une variable parmi toutes celles disponibles : </span> <div - class="flex items-center gap-1.5 mt-2 ml-4 mr-4 md:mr-0 overflow-hidden rounded-t-md bg-white border-b-2 md:border-b-4 border-b-[#A6A00C]" + class="flex items-center gap-1.5 my-2 overflow-hidden rounded-t-md bg-gray-100 border-b-2 md:border-b-4 border-b-[#A6A00C]" > <iconify-icon class="ml-1 md:ml-3 self-center p-1 text-lg text-black" @@ -82,42 +82,40 @@ </div> {#if query.trim().length > 0} - <div class="ml-4 mr-4 md:mr-0"> - <div class="flex flex-wrap gap-x-1.5 mt-2"> - {#if filteredInputVariables.length > 0} - <span>Résultats de votre recherche :</span> - {:else} - <span>Aucune variable trouvée pour :</span> - {/if} - {#each query.split(" ").filter((term) => term.length > 0) as term} - <div class="px-1.5 rounded-full bg-white">{term}</div> - {/each} - </div> + <div class="flex flex-wrap gap-x-1.5 mt-2"> {#if filteredInputVariables.length > 0} - <ul> - {#each filteredInputVariables as variable} - <li - class="my-4 flex-col items-baseline rounded bg-gray-200 p-1 py-1 text-base text-black md:p-2" - > - <VariableInput - {date} - highlight={searchTerms} - bind:inputInstantsByVariableName - bind:situation - {situationIndex} - bind:valuesByCalculationNameByVariableName - {variable} - {year} - /> - </li> - {/each} - </ul> + <span>Résultats de votre recherche :</span> + {:else} + <span>Aucune variable trouvée pour :</span> {/if} + {#each query.split(" ").filter((term) => term.length > 0) as term} + <div class="px-1.5 rounded-full bg-white">{term}</div> + {/each} </div> + {#if filteredInputVariables.length > 0} + <ul> + {#each filteredInputVariables as variable} + <li + class="my-4 flex-col items-baseline rounded bg-gray-100 p-1 py-1 text-base text-black md:p-2" + > + <VariableInput + {date} + highlight={searchTerms} + bind:inputInstantsByVariableName + bind:situation + {situationIndex} + bind:valuesByCalculationNameByVariableName + {variable} + {year} + /> + </li> + {/each} + </ul> + {/if} {/if} {#if query.trim().length > 0} - <div class="flex items-center gap-2 ml-4 mt-2"> + <div class="flex items-center gap-2 mt-2"> <div class="flex-1 border-b border-b-gray-500" /> <span class="flex-shrink-0 text-sm text-gray-600" >Fin des résultats de la recherche</span diff --git a/src/lib/components/test_cases/TestCaseView.svelte b/src/lib/components/test_cases/TestCaseView.svelte index f9df4e91edb5c3db9ac8657642f8a21b751f2599..ee68196da3254e94a3395534f46d4f0c81bf4e51 100644 --- a/src/lib/components/test_cases/TestCaseView.svelte +++ b/src/lib/components/test_cases/TestCaseView.svelte @@ -73,41 +73,43 @@ class="mx-0 place-self-start overflow-hidden rounded-lg border border-gray-200 shadow-md md:mb-5" id="situation_{situationIndex}" > - <div class="bg-gray-100" id="situation_{situationIndex}_case_summary"> - <div class="px-4 pb-2" class:pt-4={situation.title === undefined}> - <TestCaseSummary - on:changeTestCasesIndex - on:changeTestCaseToEditIndex - on:changeSituation - {displayMode} - mode="view" - {situation} - {situationIndex} - {valuesByCalculationNameByVariableName} - {year} - /> - </div> - <div - class="flex w-full overflow-hidden bg-gray-100 shadow-inner" - id="situations_{situationIndex}_tabs" - > - {#each tabsConfig.tabs as tabData} - <TestCaseTab - href={newSimulationUrl({ - ...displayMode, - tab: tabData.tab, - })} - icon={tabData.icon} - picto={tabData.picto} - text={tabData.title} - isActive={displayMode.tab === tabData.tab || - (tabData.tab === tabsConfig.defaultTab && - displayMode.tab === undefined)} - isDisabled={tabData?.disabled ?? false} + {#if !displayMode.edit} + <div class="bg-gray-100" id="situation_{situationIndex}_case_summary"> + <div class="px-4 pb-2" class:pt-4={situation.title === undefined}> + <TestCaseSummary + on:changeTestCasesIndex + on:changeTestCaseToEditIndex + on:changeSituation + {displayMode} + mode="view" + {situation} + {situationIndex} + {valuesByCalculationNameByVariableName} + {year} /> - {/each} + </div> + <div + class="flex w-full overflow-hidden bg-gray-100 shadow-inner" + id="situations_{situationIndex}_tabs" + > + {#each tabsConfig.tabs as tabData} + <TestCaseTab + href={newSimulationUrl({ + ...displayMode, + tab: tabData.tab, + })} + icon={tabData.icon} + picto={tabData.picto} + text={tabData.title} + isActive={displayMode.tab === tabData.tab || + (tabData.tab === tabsConfig.defaultTab && + displayMode.tab === undefined)} + isDisabled={tabData?.disabled ?? false} + /> + {/each} + </div> </div> - </div> + {/if} {#if displayMode.tab === "dispositif" || (displayMode.tab === undefined && tabsConfig.defaultTab === "dispositif")} <div class="relative w-full bg-white px-8 py-5"> diff --git a/src/lib/components/variables/VariableInput.svelte b/src/lib/components/variables/VariableInput.svelte index a03e877138ccd4c3d045f13098702c5dc59eb48f..1abcb93a20d21fa1dc58a8dd1217ed632b71c55a 100644 --- a/src/lib/components/variables/VariableInput.svelte +++ b/src/lib/components/variables/VariableInput.svelte @@ -322,7 +322,7 @@ <section> <div> <div class="flex items-start justify-between"> - <h1 class="flex w-4/5 items-center text-base font-bold"> + <h1 class="flex items-center text-base font-bold"> <span> {@html highlightKeywords(variable.label ?? variable.name, highlight)} </span> @@ -353,46 +353,66 @@ --> </div> - <div class="mt-2 flex w-full flex-wrap gap-4"> - {#each Object.entries(entitySituation ?? {}).sort( ([populationId1], [populationId2]) => populationId1.localeCompare(populationId2), ) as [populationId, population]} - <div> - <p class="text-xs text-gray-600"> - {population.name ?? populationId} - </p> - {#if isInput} - {#if variable.possible_values !== undefined} - {#if ["activite"].includes(variable.name)} - {getSituationVariableValue( - situation, - variable, - populationId, - year, - )} - {:else} - <!-- Input menu sélection --> - - <select - class="my-1 w-full rounded border-none bg-gray-100 p-1 pr-10 focus:border-le-bleu focus:text-le-bleu" - on:blur={(event) => changeValue(event, populationId)} - on:change={(event) => changeValue(event, populationId)} - value={getSituationVariableValue( + <div class="flex flex-col md:flex-row gap-2"> + <div class="w-full flex flex-col flex-wrap gap-4 mt-2"> + {#each Object.entries(entitySituation ?? {}).sort( ([populationId1], [populationId2]) => populationId1.localeCompare(populationId2), ) as [populationId, population]} + <div> + <p class="text-xs text-gray-600"> + {population.name ?? populationId} + </p> + {#if isInput} + {#if variable.possible_values !== undefined} + {#if ["activite"].includes(variable.name)} + {getSituationVariableValue( situation, variable, populationId, year, )} - > - {#each Object.entries(variable.possible_values) as [symbol, label]} - <option value={symbol}>{label}</option> - {/each} - </select> - {/if} - {:else if variable.value_type === "bool"} - <!-- Input oui ou non --> - <label class="inline-flex text-sm"> + {:else} + <!-- Input menu sélection --> + + <select + class="my-1 w-full rounded border-none bg-white shadow-sm p-1 pr-10 focus:border-le-bleu focus:text-le-bleu" + on:blur={(event) => changeValue(event, populationId)} + on:change={(event) => changeValue(event, populationId)} + value={getSituationVariableValue( + situation, + variable, + populationId, + year, + )} + > + {#each Object.entries(variable.possible_values) as [symbol, label]} + <option value={symbol}>{label}</option> + {/each} + </select> + {/if} + {:else if variable.value_type === "bool"} + <!-- Input oui ou non --> + <label class="inline-flex text-sm"> + <input + class="b my-1 mr-1 rounded p-1" + checked={asBoolean( + getSituationVariableValue( + situation, + variable, + populationId, + year, + ), + )} + on:change={(event) => changeValue(event, populationId)} + type="checkbox" + /> + {#if asBoolean(getSituationVariableValue(situation, variable, populationId, year))}{variable.label}{/if} + </label> + {:else if variable.value_type === "date"} + <!-- Input date--> <input - class="b my-1 mr-1 rounded p-1" - checked={asBoolean( + class="my-1 w-full rounded border-none bg-white shadow-sm p-1 focus:border-le-bleu focus:text-le-bleu" + on:change={(event) => changeValue(event, populationId)} + type="date" + value={asString( getSituationVariableValue( situation, variable, @@ -400,34 +420,52 @@ year, ), )} - on:change={(event) => changeValue(event, populationId)} - type="checkbox" /> - {#if asBoolean(getSituationVariableValue(situation, variable, populationId, year))}{variable.label}{/if} - </label> - {:else if variable.value_type === "date"} - <!-- Input date--> - <input - class="my-1 w-full rounded border-none bg-gray-100 p-1 focus:border-le-bleu focus:text-le-bleu" - on:change={(event) => changeValue(event, populationId)} - type="date" - value={asString( - getSituationVariableValue( - situation, - variable, - populationId, - year, - ), - )} - /> - {:else if ["float", "int"].includes(variable.value_type)} - <!-- Input Montant --> - <div class="flex items-center"> + {:else if ["float", "int"].includes(variable.value_type)} + <!-- Input Montant --> + <div class="flex items-center"> + <input + class="my-1 w-full rounded border-none bg-white shadow-sm p-1" + on:change={(event) => changeValue(event, populationId)} + type="number" + value={asNumber( + getSituationVariableValue( + situation, + variable, + populationId, + year, + ), + )} + /> + <span class="px-1" + >{getUnitShortLabel(variable.unit, date, { + periodUnit: variable.definition_period, + })}</span + > + </div> + {:else if ["depcom", "depcom_foyer", "depcom_entreprise"].includes(variable.name)} + {#await postalDistributionFromInseeCode(asString(getSituationVariableValue(situation, variable, populationId, year))) then term} + <Autocomplete + divClass="grow" + forceSelectionOnEnter={false} + ignoreEnterKeyWhenMenuNotOpen + inputClass="border-0 input rounded p-1 my-1 bg-white shadow-sm" + items={communeSuggestions} + itemTermKey="autocompletion" + on:input={autocompleteCommune} + on:select={(event) => changeCommune(event, populationId)} + propagateEnter + placeholder="Code postal ou commune" + type="text" + {term} + /> + {/await} + {:else} <input - class="my-1 w-full rounded border-none bg-gray-100 p-1" + class="my-1 w-full rounded bg-white shadow-sm p-1 focus:border-le-bleu focus:text-le-bleu" on:change={(event) => changeValue(event, populationId)} - type="number" - value={asNumber( + type="text" + value={asString( getSituationVariableValue( situation, variable, @@ -436,267 +474,239 @@ ), )} /> - <span class="px-1" - >{getUnitShortLabel(variable.unit, date, { - periodUnit: variable.definition_period, - })}</span - > - </div> - {:else if ["depcom", "depcom_foyer", "depcom_entreprise"].includes(variable.name)} - {#await postalDistributionFromInseeCode(asString(getSituationVariableValue(situation, variable, populationId, year))) then term} - <Autocomplete - divClass="grow" - forceSelectionOnEnter={false} - ignoreEnterKeyWhenMenuNotOpen - inputClass="border-0 input rounded p-1 my-1 bg-gray-100 " - items={communeSuggestions} - itemTermKey="autocompletion" - on:input={autocompleteCommune} - on:select={(event) => changeCommune(event, populationId)} - propagateEnter - placeholder="Code postal ou commune" - type="text" - {term} - /> - {/await} + {/if} + {#if valueError !== null}<span class="text-red-500" + >{valueError}</span + >{/if} {:else} - <input - class="my-1 w-full rounded bg-gray-100 p-1 focus:border-le-bleu focus:text-le-bleu" - on:change={(event) => changeValue(event, populationId)} - type="text" - value={asString( - getSituationVariableValue( - situation, - variable, - populationId, - year, - ), + <ValueChange + unitName={null} + valueByCalculationName={getCalculatedVariableValueByCalculationName( + situation, + valuesByCalculationNameByVariableName, + variable, + populationId, )} /> {/if} - {#if valueError !== null}<span class="text-red-500" - >{valueError}</span - >{/if} - {:else} - <ValueChange - unitName={null} - valueByCalculationName={getCalculatedVariableValueByCalculationName( - situation, - valuesByCalculationNameByVariableName, - variable, - populationId, - )} - /> - {/if} - </div> - {/each} - </div> - <!--Partie dédiée aux agrégats--> - <div class="my-1 w-full"> - {#if variable.name === "activite"} - {#each Object.keys(entitySituation ?? {}).sort( (populationId1, populationId2) => populationId1.localeCompare(populationId2), ) as populationId} - {@const value = getSituationVariableValue( - situation, - variable, - populationId, - year, - )} - - {#if value === "chomeur" && isFirstEncounter("chomeur", populationId)} - <div class="rounded-md bg-le-gris-dispositif-light p-3"> - <p class="font-serif text-base"> - Au sens de Pôle emploi et de la DARES, il y a en moyenne <span - class="font-bold" - >3 028 500 demandeurs d'emploi sans emploi</span - > - (de catégorie A), en France hors Mayotte au 3ème trimestre 2023. - Le nombre de demandeurs d'emploi toute catégorie confondue est au - total de 6 115 200. - </p> - <p - class="ml-10 mt-2 justify-self-end text-right font-serif text-sm text-gray-700" - > - <a - class="lx-link-text" - href="https://dares.travail-emploi.gouv.fr/sites/default/files/2e053f96fcf33313b3b11c0c045d7e42/Dares_DI_DEFM_2023T3.pdf" - rel="noreferrer" - target="_blank" - >DARES, <span class="italic"> - « Indicateurs »</span - >, octobre 2023</a - >. - <span class="text-xs"><br />Consulté le 16 janvier 2024.</span> - </p> - - <p class="mt-4 font-serif text-base"> - Au sens du Bureau International du Travail (BIT) et de l'INSEE, - il y a en moyenne - <span class="mb-2 font-bold" - >2 285 000 personnes inactives</span - > - en France hors Mayotte au 3ème trimestre 2023. - </p> - <p - class="ml-10 mt-2 justify-self-end text-right font-serif text-sm text-gray-700" - > - <a - class="lx-link-text" - href="https://www.insee.fr/fr/statistiques/7713975" - rel="noreferrer" - target="_blank" - > - Insee, <span class="italic" - >« Statistiques et Études »</span - >, novembre 2023. - <span class="text-xs"><br />Consulté le 16 janvier 2024.</span - > - </a> - </p> - </div> - {/if} + </div> {/each} - {/if} - {#if variable.name === "statut_marital"} - {#each Object.entries(entitySituation ?? {}).sort( ([populationId1], [populationId2]) => populationId1.localeCompare(populationId2), ) as [populationId]} - {@const value = asString( - getSituationVariableValue(situation, variable, populationId, year), - )} - {#if ["celibataire", "divorce", "jeune_veuf", "marie", "pacse", "veuf", "non_renseigne"].includes(value) && isFirstEncounter(value, populationId)} - <div class="my-2 rounded-md bg-le-gris-dispositif-light p-3"> - <p class="mb-2 font-serif text-base"> - <span - >D'après la base de données des déclarations des revenus - 2019*,</span - > - {#if value === "celibataire"} - <span class="font-bold"> 41% des foyers fiscaux,</span> - <span class="italic">soit 16 184 400,</span> - sont « célibataires ». - <br /> - {:else if value === "divorce"} - <span class="font-bold"> - 15,7% des foyers fiscaux, - </span><span class="italic">soit 6 181 600,</span> - sont « divorcés ». - {:else if value === "jeune_veuf"} - <span class="font-bold"> 9,9% des foyers fiscaux, </span><span - class="italic">soit 3 890 800,</span - > - sont « veufs ». Le nombre de veufs dits « jeunes » - n'est pas connu. - {:else if value === "marie"} - <span class="font-bold"> - 29,41% des foyers fiscaux, - </span><span class="italic">soit 11 546 600,</span> - sont « mariés ». - {:else if value === "pacse"} - <span class="font-bold"> 3,7% des foyers fiscaux, </span><span - class="italic">soit 1 461 100,</span - > - sont « pacsés ». - {:else if value === "veuf"} - <span class="font-bold"> 9,9% des foyers fiscaux, </span><span - class="italic">soit 3 890 800,</span + </div> + <!--Partie dédiée aux agrégats--> + <div class="my-1 w-full"> + {#if variable.name === "activite"} + {#each Object.keys(entitySituation ?? {}).sort( (populationId1, populationId2) => populationId1.localeCompare(populationId2), ) as populationId} + {@const value = getSituationVariableValue( + situation, + variable, + populationId, + year, + )} + + {#if value === "chomeur" && isFirstEncounter("chomeur", populationId)} + <div class="rounded-md bg-le-gris-dispositif-light p-3"> + <p class="font-serif text-base"> + Au sens de Pôle emploi et de la DARES, il y a en moyenne <span + class="font-bold" + >3 028 500 demandeurs d'emploi sans emploi</span > - sont « veufs ». Parmi ces foyers, certains sont catégorisés - comme « jeunes veufs ». - {:else if value === "non_renseigne"} - <span class="font-bold" - >aucun agrégat est disponible pour cette catégorie.</span + (de catégorie A), en France hors Mayotte au 3ème trimestre 2023. + Le nombre de demandeurs d'emploi toute catégorie confondue est + au total de 6 115 200. + </p> + <p + class="ml-10 mt-2 justify-self-end text-right font-serif text-sm text-gray-700" + > + <a + class="lx-link-text" + href="https://dares.travail-emploi.gouv.fr/sites/default/files/2e053f96fcf33313b3b11c0c045d7e42/Dares_DI_DEFM_2023T3.pdf" + rel="noreferrer" + target="_blank" + >DARES, <span class="italic"> + « Indicateurs »</span + >, octobre 2023</a + >. + <span class="text-xs"><br />Consulté le 16 janvier 2024.</span > - {/if} - </p> + </p> - <p - class="ml-10 mt-2 justify-self-end text-right font-serif text-sm text-gray-700" - > - LexImpact, <span class="italic" - >agrégats extraits de la <a + <p class="mt-4 font-serif text-base"> + Au sens du Bureau International du Travail (BIT) et de + l'INSEE, il y a en moyenne + <span class="mb-2 font-bold" + >2 285 000 personnes inactives</span + > + en France hors Mayotte au 3ème trimestre 2023. + </p> + <p + class="ml-10 mt-2 justify-self-end text-right font-serif text-sm text-gray-700" + > + <a class="lx-link-text" - href="https://www.casd.eu/source/declarations-dimpot-sur-le-revenu-des-foyers-fiscaux-formulaire-2042-et-annexes/" + href="https://www.insee.fr/fr/statistiques/7713975" rel="noreferrer" - target="_blank">base de donnée POTE</a - > publiée par la DGFIP</span - >, Millésime 2019, extraction du 03/11/2022. - </p> - <button - class="inline-block font-sans text-sm" - on:click={() => - (openMoreAggregatMaritalStatus = - !openMoreAggregatMaritalStatus)} - > - <span>* En savoir plus sur l'agrégat et la source</span> - {#if !openMoreAggregatMaritalStatus}<iconify-icon - class="inline-flex h-5 w-5" - icon="ri:arrow-down-s-line" - /> - {:else} - <iconify-icon - class="inline-flex h-5 w-5" - icon="ri:arrow-up-s-line" - /> - {/if} - </button> + target="_blank" + > + Insee, <span class="italic" + >« Statistiques et Études »</span + >, novembre 2023. + <span class="text-xs" + ><br />Consulté le 16 janvier 2024.</span + > + </a> + </p> + </div> + {/if} + {/each} + {/if} + {#if variable.name === "statut_marital"} + {#each Object.entries(entitySituation ?? {}).sort( ([populationId1], [populationId2]) => populationId1.localeCompare(populationId2), ) as [populationId]} + {@const value = asString( + getSituationVariableValue( + situation, + variable, + populationId, + year, + ), + )} + {#if ["celibataire", "divorce", "jeune_veuf", "marie", "pacse", "veuf", "non_renseigne"].includes(value) && isFirstEncounter(value, populationId)} + <div class="my-2 rounded-md bg-le-gris-dispositif-light p-3"> + <p class="mb-2 font-serif text-base"> + <span + >D'après la base de données des déclarations des revenus + 2019*,</span + > + {#if value === "celibataire"} + <span class="font-bold"> 41% des foyers fiscaux,</span> + <span class="italic">soit 16 184 400,</span> + sont « célibataires ». + <br /> + {:else if value === "divorce"} + <span class="font-bold"> + 15,7% des foyers fiscaux, + </span><span class="italic">soit 6 181 600,</span> + sont « divorcés ». + {:else if value === "jeune_veuf"} + <span class="font-bold"> + 9,9% des foyers fiscaux, + </span><span class="italic">soit 3 890 800,</span> + sont « veufs ». Le nombre de veufs dits « jeunes » + n'est pas connu. + {:else if value === "marie"} + <span class="font-bold"> + 29,41% des foyers fiscaux, + </span><span class="italic">soit 11 546 600,</span + > + sont « mariés ». + {:else if value === "pacse"} + <span class="font-bold"> + 3,7% des foyers fiscaux, + </span><span class="italic">soit 1 461 100,</span> + sont « pacsés ». + {:else if value === "veuf"} + <span class="font-bold"> + 9,9% des foyers fiscaux, + </span><span class="italic">soit 3 890 800,</span> + sont « veufs ». Parmi ces foyers, certains sont catégorisés + comme « jeunes veufs ». + {:else if value === "non_renseigne"} + <span class="font-bold" + >aucun agrégat est disponible pour cette catégorie.</span + > + {/if} + </p> - {#if openMoreAggregatMaritalStatus === true} - <div - class="mt-2 rounded-b-sm bg-le-gris-dispositif-ultralight p-2 text-gray-700" + <p + class="ml-10 mt-2 justify-self-end text-right font-serif text-sm text-gray-700" > - <p class="class-sm mt-2 text-sm"> - <span class="font-bold" - >Qu'est ce que représente la base de données POTE ?</span - ><br /> - La base POTE rassemble les informations recensées à l’occasion - de la déclaration 2020 sur les revenus 2019 grâce au - <a + LexImpact, <span class="italic" + >agrégats extraits de la <a class="lx-link-text" - href="https://www.impots.gouv.fr/formulaire/2042/declaration-des-revenus" + href="https://www.casd.eu/source/declarations-dimpot-sur-le-revenu-des-foyers-fiscaux-formulaire-2042-et-annexes/" rel="noreferrer" - target="_blank">formulaire n°2042 et ses annexes</a - >. - </p> - <p class="class-sm mt-2 text-sm"> - <span class="font-bold" - >Cette base de donnée est-elle représentative de la - population française ?</span - ><br /> - Cette base est plutôt représentative de la population française, - voici quelques précisions : - </p> - <ul class="list-inside list-disc text-sm"> - <li> - certains individus n'effectuent pas de déclaration de - revenus et ne sont donc pas comptabilisés. Cette situation - reste marginale car la déclaration est obligatoire dans de - nombreux cas explicités à + target="_blank">base de donnée POTE</a + > publiée par la DGFIP</span + >, Millésime 2019, extraction du 03/11/2022. + </p> + <button + class="inline-block font-sans text-sm" + on:click={() => + (openMoreAggregatMaritalStatus = + !openMoreAggregatMaritalStatus)} + > + <span>* En savoir plus sur l'agrégat et la source</span> + {#if !openMoreAggregatMaritalStatus}<iconify-icon + class="inline-flex h-5 w-5" + icon="ri:arrow-down-s-line" + /> + {:else} + <iconify-icon + class="inline-flex h-5 w-5" + icon="ri:arrow-up-s-line" + /> + {/if} + </button> + + {#if openMoreAggregatMaritalStatus === true} + <div + class="mt-2 rounded-b-sm bg-le-gris-dispositif-ultralight p-2 text-gray-700" + > + <p class="class-sm mt-2 text-sm"> + <span class="font-bold" + >Qu'est ce que représente la base de données POTE ?</span + ><br /> + La base POTE rassemble les informations recensées à l’occasion + de la déclaration 2020 sur les revenus 2019 grâce au <a - href="https://www.casd.eu/source/declarations-dimpot-sur-le-revenu-des-foyers-fiscaux-formulaire-2042-et-annexes/" + class="lx-link-text" + href="https://www.impots.gouv.fr/formulaire/2042/declaration-des-revenus" rel="noreferrer" - target="_blank" - class="lx-link-text">cette page du CASD</a - > ; - </li> - <li> - parmi les foyers fiscaux, certains déclarants sont de - nationalité étrangère ; - </li> - <li> - les individus de nationalité française résidant à - l'étranger sont faiblement représentés car, dans la - majorité des cas, ils n'ont pas cette déclaration de - revenus à faire. - </li> - </ul> - </div> - {/if} - </div> - {/if} - {/each} - {/if} + target="_blank">formulaire n°2042 et ses annexes</a + >. + </p> + <p class="class-sm mt-2 text-sm"> + <span class="font-bold" + >Cette base de donnée est-elle représentative de la + population française ?</span + ><br /> + Cette base est plutôt représentative de la population française, + voici quelques précisions : + </p> + <ul class="list-inside list-disc text-sm"> + <li> + certains individus n'effectuent pas de déclaration de + revenus et ne sont donc pas comptabilisés. Cette + situation reste marginale car la déclaration est + obligatoire dans de nombreux cas explicités à + <a + href="https://www.casd.eu/source/declarations-dimpot-sur-le-revenu-des-foyers-fiscaux-formulaire-2042-et-annexes/" + rel="noreferrer" + target="_blank" + class="lx-link-text">cette page du CASD</a + > ; + </li> + <li> + parmi les foyers fiscaux, certains déclarants sont de + nationalité étrangère ; + </li> + <li> + les individus de nationalité française résidant à + l'étranger sont faiblement représentés car, dans la + majorité des cas, ils n'ont pas cette déclaration de + revenus à faire. + </li> + </ul> + </div> + {/if} + </div> + {/if} + {/each} + {/if} + </div> </div> {#if showLogementAlert} - <div class="bg-gray-100 text-sm text-yellow-900 p-2"> + <div class="bg-white text-sm text-yellow-900 p-2"> <iconify-icon icon="ri-information-fill" class="text-yellow-600 mr-1 align-[-0.2rem] text-base" diff --git a/src/lib/components/variables/VariableReferredInputs.svelte b/src/lib/components/variables/VariableReferredInputs.svelte index 8d63dd2260febb121a024f1fda2ee1fae7e753a6..0670463b4440e69266d5cbae6a8f5261df198523 100644 --- a/src/lib/components/variables/VariableReferredInputs.svelte +++ b/src/lib/components/variables/VariableReferredInputs.svelte @@ -50,12 +50,12 @@ } </script> -<h2 class="mb-2 ml-4 mr-4 pt-3 text-lg md:mr-0"> +<h2 class="mb-2 pt-3 text-lg md:mr-0"> Rechercher une variable ayant une influence sur : </h2> <div - class="ml-4 mr-4 flex items-center justify-between border-l-2 border-white pl-2 md:mr-0" + class="flex items-center justify-between border-l-2 border-white pl-2 md:mr-0" > <span class="font-serif text-lg font-bold" >{variable.short_label ?? variable.label ?? variable.name}</span @@ -140,7 +140,7 @@ {#if inputs.length > 0} <section class="mb-3 pb-3"> - <h2 class="my-4 ml-4 rounded-l bg-gray-400 text-white shadow-inner"> + <h2 class="my-4 rounded bg-gray-400 text-white shadow-inner"> <button class="flex w-full justify-between px-3 py-1" on:click={() => (openAllInputs = !openAllInputs)} @@ -155,7 +155,7 @@ {#if openAllInputs} <div - class="flex items-center gap-1.5 ml-4 mr-4 md:mr-0 overflow-hidden rounded-t-md bg-white border-b-2 md:border-b-4 border-b-[#A6A00C]" + class="flex items-center gap-1.5 overflow-hidden rounded-t-md bg-white border-b-2 md:border-b-4 border-b-[#A6A00C]" > <iconify-icon class="ml-1 md:ml-3 self-center p-1 text-lg text-black" @@ -178,26 +178,24 @@ {/if} </div> - <div class="ml-4 mr-4 md:mr-0"> - <ul> - {#each filter(inputs, allInputsSearchTerms) as input} - <li - class="my-4 flex-col items-baseline rounded bg-gray-200 p-1 py-1 text-base text-black md:p-2" - > - <VariableInput - {date} - highlight={allInputsSearchTerms} - bind:inputInstantsByVariableName - bind:situation - {situationIndex} - bind:valuesByCalculationNameByVariableName - variable={input} - {year} - /> - </li> - {/each} - </ul> - </div> + <ul> + {#each filter(inputs, allInputsSearchTerms) as input} + <li + class="my-4 flex-col items-baseline rounded bg-gray-100 p-1 py-1 text-base text-black md:p-2" + > + <VariableInput + {date} + highlight={allInputsSearchTerms} + bind:inputInstantsByVariableName + bind:situation + {situationIndex} + bind:valuesByCalculationNameByVariableName + variable={input} + {year} + /> + </li> + {/each} + </ul> {/if} </section> {/if} diff --git a/src/lib/components/variables/VariableView.svelte b/src/lib/components/variables/VariableView.svelte index 1afebe02ab4abeb86ac6973760440dc72fa90299..7669738b8aea54ae950da95a36525ec7c0990124 100644 --- a/src/lib/components/variables/VariableView.svelte +++ b/src/lib/components/variables/VariableView.svelte @@ -45,7 +45,7 @@ <div class="flex-col items-start pb-4"> <div class="flex-col items-start pb-4"> - <div class="px-10 pt-10"> + <div class="px-5 md:px-10 pt-10"> <!--Titre de la formule--> <p class="mb-2 uppercase">Formule de calcul</p> diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 92389eb362db6728219d0aa98634f16f0af9953c..d3b552eee26af9e42e1802f47c199434b156dd3b 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -39,7 +39,6 @@ import ModificationsPanel from "$lib/components/ModificationsPanel.svelte" import TestCaseCompareView from "$lib/components/test_cases/TestCaseCompareView.svelte" import TestCaseEdit from "$lib/components/test_cases/TestCaseEdit.svelte" - import TestCaseEditVariablesSearch from "$lib/components/test_cases/TestCaseEditVariablesSearch.svelte" import TestCasePictos from "$lib/components/test_cases/TestCasePictos.svelte" import TestCaseScreenshotLayout from "$lib/components/test_cases/TestCaseScreenshotLayout.svelte" import TestCaseSelectModal from "$lib/components/test_cases/TestCaseSelectModal.svelte" @@ -48,7 +47,6 @@ import TestCaseView from "$lib/components/test_cases/TestCaseView.svelte" import TestCaseSimulationSharingModal from "$lib/components/TestCaseSimulationSharingModal.svelte" import VariableDetail from "$lib/components/variables/VariableDetail.svelte" - import VariableReferredInputsPane from "$lib/components/variables/VariableReferredInputsPane.svelte" import VariableReferredParameters from "$lib/components/variables/VariableReferredParameters.svelte" import VariableValueChange from "$lib/components/variables/VariableValueChange.svelte" import WaterfallPlainView from "$lib/components/WaterfallPlainView.svelte" @@ -952,8 +950,9 @@ class:overflow-hidden={$searchActive} > <div - class="w-full flex flex-col {displayMode.edit !== undefined && - 'md:-translate-x-1/3'} overflow-x-clip md:overflow-hidden transition-transform duration-500 ease-out-quart" + class="w-full flex flex-col {displayMode.edit !== undefined + ? 'md:-translate-x-1/3' + : ''} overflow-x-clip md:overflow-hidden transition-transform duration-500 ease-out-quart" > <nav class="min-h-10 sticky top-0 z-20 flex h-10 shadow-md md:hidden"> <!--Onglets - MOBILE--> @@ -1023,8 +1022,8 @@ <div class="flex flex-1 overflow-x-hidden"> <!-- Panneau de gauche les zones éditables par l'utilisateur (amendement)--> <section - class="flex min-w-0 flex-[1_0_100%] overflow-y-scroll bg-white shadow-lg transition-transform duration-500 ease-out-quart md:z-10 md:!h-full md:flex-[1_0_33.3%] md:translate-x-0 md:overflow-visible md:!p-0" - class:translate-x-[-100%]={!mobileLawTab} + class="flex min-w-0 flex-[1_0_100%] overflow-y-scroll bg-white shadow-lg transition-transform duration-500 ease-out-quart md:z-10 md:!h-full md:flex-[0_0_33.3%] md:translate-x-0 md:overflow-visible md:!p-0" + class:-translate-x-full={!mobileLawTab} class:!h-[calc(100vh-96px)]={!mobileLawTab} class:pb-24={$testCasesIndex.length === 1 && displayMode.parametersVariableName === undefined} @@ -1226,16 +1225,19 @@ the height of the div enclosing the test cases below. --> <section - class="flex-[1_0_100%] h-fit flex flex-col overflow-y-hidden transition-transform duration-500 ease-out-quart md:!h-full md:flex-[1_0_66%] md:translate-x-0" - class:translate-x-[-100%]={!mobileLawTab} + class="flex-[1_0_100%] h-fit flex flex-col overflow-y-hidden transition-transform duration-500 ease-out-quart md:!h-full md:translate-x-0" + class:-translate-x-full={!mobileLawTab} class:!h-[calc(100vh-96px)]={mobileLawTab} + class:md:flex-[0_0_66.6%]={!displayMode.edit} + class:md:flex-[0_0_33.3%]={displayMode.edit} id="situation_right_part_impacts" > <!--en-tête--> - <header - class="relative w-screen md:w-full flex justify-center md:justify-normal px-5 xl:px-10 bg-yellow-50 border-b" - > - <!-- + {#if !displayMode.edit} + <header + class="relative w-screen md:w-full flex justify-center md:justify-normal px-5 xl:px-10 bg-yellow-50 border-b" + > + <!-- <select class="rounded border-1 text-xs" on:blur={changeBillName} @@ -1249,89 +1251,100 @@ </select> --> - <!--Affichage du titre "impacts" et onglets de choix cas types ou budget--> - <ul - class="flex items-center justify-between md:justify-start w-screen md:w-full" - > - <li class="w-1/2 md:w-auto flex justify-end"> - <a - class:hover:bg-le-vert-50={displayMode.budget} - class:hover:text-le-bleu={displayMode.budget} - data-sveltekit-noscroll - href={newSimulationUrl({ - ...displayMode, - budget: undefined, - })} - > - <h2 - class="h-12 md:h-14 2xl:h-16 flex items-center border-b-[3px] border-transparent px-3 pt-2 pb-1 text-xl text-black 2xl:border-b-4 2xl:text-2xl justify-center tracking-wide" - class:!border-black={!displayMode.budget} - class:text-black={!displayMode.budget} - class:font-bold={!displayMode.budget} - > - <span class="hidden sm:flex"> Impact cas type </span> - <span class="flex sm:hidden"> Cas type </span> - </h2> - </a> - </li> - <li - class="w-1/2 flex md:w-auto justify-start" - id="situation_budget" + <!--Affichage du titre "impacts" et onglets de choix cas types ou budget--> + <ul + class="flex items-center justify-between md:justify-start w-screen md:w-full" > - <a - class:hover:bg-le-vert-50={!displayMode.budget} - class:hover:text-le-bleu={!displayMode.budget} - data-sveltekit-noscroll - href={newSimulationUrl({ - ...displayMode, - budget: true, - })} + <li class="w-1/2 md:w-auto flex justify-end"> + <a + class:hover:bg-le-vert-50={displayMode.budget} + class:hover:text-le-bleu={displayMode.budget} + data-sveltekit-noscroll + href={newSimulationUrl({ + ...displayMode, + budget: undefined, + })} + > + <h2 + class="h-12 md:h-14 2xl:h-16 flex items-center border-b-[3px] border-transparent px-3 pt-2 pb-1 text-xl text-black 2xl:border-b-4 2xl:text-2xl justify-center tracking-wide" + class:!border-black={!displayMode.budget} + class:text-black={!displayMode.budget} + class:font-bold={!displayMode.budget} + > + <span class="hidden sm:flex"> Impact cas type </span> + <span class="flex sm:hidden"> Cas type </span> + </h2> + </a> + </li> + <li + class="w-1/2 flex md:w-auto justify-start" + id="situation_budget" > - <h2 - class="h-12 md:h-14 2xl:h-16 flex items-center border-b-[3px] border-transparent px-3 pt-2 pb-1 text-xl 2xl:border-b-4 2xl:text-2xl justify-center tracking-wide" - class:!border-black={displayMode.budget} - class:font-bold={displayMode.budget} + <a + class:hover:bg-le-vert-50={!displayMode.budget} + class:hover:text-le-bleu={!displayMode.budget} + data-sveltekit-noscroll + href={newSimulationUrl({ + ...displayMode, + budget: true, + })} > - <span class="hidden sm:flex"> Impact budgétaire </span> - <span class="flex sm:hidden"> Budget </span> - </h2> - </a> - </li> - </ul> - {#if (displayMode.parametersVariableName !== undefined || displayMode.testCasesIndex.length > 0) && (mobileLawTab || !displayMode.budget)} - <button - class="z-30 absolute -bottom-4 right-2 lg:right-5 xl:right-10 flex items-center gap-2 py-2 px-5 shadow-lg bg-white hover:bg-gray-100 active:bg-gray-200 rounded border border-le-bleu text-le-bleu text-sm font-bold tracking-[0.085em] uppercase transition-all duration-200 ease-out-back disabled:opacity-0 disabled:scale-90" - on:click={shareTestCaseSimulationLink} - id="situation_savebutton" - > - <span class="hidden lg:inline">Enregistrer / partager</span> - <iconify-icon class="text-lg" icon="ri:share-fill"></iconify-icon> - </button> + <h2 + class="h-12 md:h-14 2xl:h-16 flex items-center border-b-[3px] border-transparent px-3 pt-2 pb-1 text-xl 2xl:border-b-4 2xl:text-2xl justify-center tracking-wide" + class:!border-black={displayMode.budget} + class:font-bold={displayMode.budget} + > + <span class="hidden sm:flex"> Impact budgétaire </span> + <span class="flex sm:hidden"> Budget </span> + </h2> + </a> + </li> + </ul> + {#if (displayMode.parametersVariableName !== undefined || displayMode.testCasesIndex.length > 0) && (mobileLawTab || !displayMode.budget) && !displayMode.edit} + <button + class="z-30 absolute -bottom-4 right-2 lg:right-5 xl:right-10 flex items-center gap-2 py-2 px-5 shadow-lg bg-white hover:bg-gray-100 active:bg-gray-200 rounded border border-le-bleu text-le-bleu text-sm font-bold tracking-[0.085em] uppercase transition-all duration-200 ease-out-back disabled:opacity-0 disabled:scale-90" + on:click={shareTestCaseSimulationLink} + id="situation_savebutton" + > + <span class="hidden lg:inline">Enregistrer / partager</span> + <iconify-icon class="text-lg" icon="ri:share-fill" + ></iconify-icon> + </button> + + <TestCaseSimulationSharingModal + bind:config={testCaseSharingModal} + /> + {/if} - <TestCaseSimulationSharingModal - bind:config={testCaseSharingModal} + {#if !showBudgetBlurred && displayMode.budget && !mobileLawTab && (user !== undefined || !showPublicSimulationPanel)} + <button + class="z-30 absolute -bottom-4 right-2 lg:right-5 xl:right-10 flex items-center gap-2 py-2 px-5 shadow-lg bg-white hover:bg-gray-100 active:bg-gray-200 rounded border border-le-bleu text-le-bleu text-sm font-bold tracking-[0.085em] uppercase transition-all duration-200 ease-out-back disabled:opacity-0 disabled:scale-90" + on:click={() => (isBudgetSharingModalOpen = true)} + disabled={displayMode.parametersVariableName === undefined || + $budgetSimulation === undefined || + ($budgetSimulation.errors != null && + $budgetSimulation.errors.length > 0)} + > + <span class="hidden lg:inline">Enregistrer / partager</span> + <iconify-icon class="text-lg" icon="ri:share-fill" + ></iconify-icon> + </button> + {/if} + + <BudgetSimulationSharingModal + {displayMode} + bind:isOpen={isBudgetSharingModalOpen} /> - {/if} - - {#if !showBudgetBlurred && displayMode.budget && !mobileLawTab && (user !== undefined || !showPublicSimulationPanel)} - <button - class="z-30 absolute -bottom-4 right-2 lg:right-5 xl:right-10 flex items-center gap-2 py-2 px-5 shadow-lg bg-white hover:bg-gray-100 active:bg-gray-200 rounded border border-le-bleu text-le-bleu text-sm font-bold tracking-[0.085em] uppercase transition-all duration-200 ease-out-back disabled:opacity-0 disabled:scale-90" - on:click={() => (isBudgetSharingModalOpen = true)} - disabled={displayMode.parametersVariableName === undefined || - $budgetSimulation === undefined || - ($budgetSimulation.errors != null && - $budgetSimulation.errors.length > 0)} - > - <span class="hidden lg:inline">Enregistrer / partager</span> - <iconify-icon class="text-lg" icon="ri:share-fill"></iconify-icon> - </button> - {/if} - - <BudgetSimulationSharingModal - {displayMode} - bind:isOpen={isBudgetSharingModalOpen} - /> - </header> + </header> + {:else} + <header + class="w-screen md:w-full px-5 py-3 2xl:py-4 bg-yellow-50 border-b" + > + <h2 class="text-black font-bold text-xl 2xl:text-2xl leading-8"> + Impact du système socio-fiscal + </h2> + </header> + {/if} <!--Impacts--> {#key displayMode.budget} <div @@ -1811,7 +1824,9 @@ {:else} {#key $testCasesIndex.length || displayMode.parametersVariableName} <div - class="px-3 md:px-5 xl:px-10 py-4 xl:py-8" + class={!displayMode.edit + ? "px-3 md:px-5 xl:px-10 py-4 xl:py-8" + : "pl-4 pr-5 py-2"} out:fade={{ duration: 100 }} in:fade={{ delay: 150, duration: 150 }} > @@ -2147,61 +2162,67 @@ {/if} </div> {/if} - <div - class="flex-col flex md:flex-row items-stretch mt-10 justify-center gap-4" - > - {#if displayMode.parametersVariableName !== undefined} - {#if displayMode.testCasesIndex.length > 0} - {@const shortLabel = - $decompositionByName[ - displayMode.parametersVariableName - ]?.short_label ?? - variableSummaryByName[ - displayMode.parametersVariableName - ].short_label} - <a - class=" lx-card flex items-start gap-5 px-7 py-5" - href={newSimulationUrl({ - ...displayMode, - edit: undefined, - testCasesIndex: [], - variableName: undefined, - })} - > - <div class="flex flex-col"> - <span - class="text-start text-lg 2xl:text-xl font-bold" - > - Autres cas types «<span class="font-serif italic" - > {shortLabel} </span - >» - </span> - <span class="text-start text-sm"> - Sélectionner un ménage impacté par ce dispositif - </span> - </div> - </a> - {/if} - {/if} - <button - class="lx-card flex items-start gap-5 px-7 py-5" - on:click={() => (isTestCaseSelectModalOpen = true)} + {#if !displayMode.edit} + <div + class="flex-col flex items-stretch mt-10 justify-center gap-4" + class:md:flex-row={!displayMode.edit} > - <div class="flex flex-col"> - <span class="text-start text-lg 2xl:text-xl font-bold"> - {#if displayMode.parametersVariableName === undefined} - Choisir un cas type - {:else} - Bibliothèque des cas types - {/if} - </span> - <span class="max-w-48 text-start text-sm"> - Sélectionner un ménage parmi tous ceux disponibles - </span> - </div> - <TestCasesStackRepresentation /> - </button> - </div> + {#if displayMode.parametersVariableName !== undefined} + {#if displayMode.testCasesIndex.length > 0} + {@const shortLabel = + $decompositionByName[ + displayMode.parametersVariableName + ]?.short_label ?? + variableSummaryByName[ + displayMode.parametersVariableName + ].short_label} + <a + class="lx-card flex items-start gap-5 px-7 py-5" + href={newSimulationUrl({ + ...displayMode, + edit: undefined, + testCasesIndex: [], + variableName: undefined, + })} + > + <div class="flex flex-col"> + <span + class="text-start text-lg 2xl:text-xl font-bold" + > + Autres cas types «<span + class="font-serif italic" + > {shortLabel} </span + >» + </span> + <span class="text-start text-sm"> + Sélectionner un ménage impacté par ce dispositif + </span> + </div> + </a> + {/if} + {/if} + <button + class="lx-card flex justify-between items-start gap-5 px-7 py-5" + on:click={() => (isTestCaseSelectModalOpen = true)} + > + <div class="flex flex-col"> + <span + class="text-start text-lg 2xl:text-xl font-bold" + > + {#if displayMode.parametersVariableName === undefined} + Choisir un cas type + {:else} + Bibliothèque des cas types + {/if} + </span> + <span class="max-w-48 text-start text-sm"> + Sélectionner un ménage parmi tous ceux disponibles + </span> + </div> + <TestCasesStackRepresentation /> + </button> + </div> + {/if} </div> {/key} {/if} @@ -2214,28 +2235,63 @@ {#if displayMode.edit !== undefined} <!-- Panneau de droite pour l'édition de cas types --> <section - class="absolute z-40 top-0 md:bottom-0 right-0 w-full md:w-1/3 md:overflow-y-scroll flex flex-col bg-gray-300 shadow-lg" + class="absolute z-40 top-0 md:bottom-0 right-0 w-full md:w-2/3 md:overflow-y-scroll flex flex-col bg-gray-200 shadow-lg" transition:fly={{ duration: 520, - x: (windowInnerWidth ?? 0) / (windowInnerWidth >= 768 ? 3 : 1), + x: + ((windowInnerWidth >= 768 ? 2 : 1) * (windowInnerWidth ?? 0)) / + (windowInnerWidth >= 768 ? 3 : 1), opacity: 1, easing: quartOut, }} > - <div class="flex justify-between items-center mt-2"> - <h2 class="mx-5 mr-2 text-2xl font-bold text-black lg:text-3xl"> + <div + class="hidden md:flex justify-between items-center gap-2 md:gap-0 px-5 py-3 2xl:py-4 border-b border-gray-300" + > + <h2 + class="text-lg font-bold text-black md:text-xl 2xl:text-2xl leading-8" + > Modifier le cas type </h2> + <span class="text-sm text-gray-700"> + 📌 Par défaut, la situation du cas type est + considérée comme stable depuis 3 ans. + </span> <button on:click={() => changeTestCaseToEditIndex(undefined)} - class="ease flex text-sm uppercase text-gray-600 transition-opacity duration-500 hover:text-black mr-4" + class="flex items-center gap-1 p-1 md:py-1.5 md:pl-2 md:pr-3 hover:bg-gray-400 hover:bg-opacity-20 active:bg-gray-400 active:bg-opacity-40 rounded-lg text-neutral-600 hover:text-black text-sm tracking-wider uppercase transition-all duration-200 ease-out-back" title="Valider le cas type et fermer l'édition" > <iconify-icon class="align-[-0.2rem] text-xl" icon="ri-close-line" /> - <span class="hidden md:flex">Fermer</span> + <span>Fermer</span> </button> </div> - <div class="overflow-y-scroll pb-14"> + <div + class="flex md:hidden flex-col gap-1 px-3 py-2 2xl:py-4 border-b border-gray-300" + > + <div class="flex md:hidden justify-start items-center gap-2 md:gap-0"> + <button + on:click={() => changeTestCaseToEditIndex(undefined)} + class="flex items-center gap-1 p-1 md:py-1.5 md:pl-2 md:pr-3 hover:bg-gray-400 hover:bg-opacity-20 active:bg-gray-400 active:bg-opacity-40 rounded-lg text-neutral-600 hover:text-black text-sm tracking-wider uppercase transition-all duration-200 ease-out-back" + title="Valider le cas type et fermer l'édition" + > + <iconify-icon + class="align-[-0.2rem] text-xl" + icon="ri-close-line" + /> + </button> + <h2 + class="text-lg font-bold text-black md:text-xl 2xl:text-2xl leading-8" + > + Modifier le cas type + </h2> + </div> + <span class="text-xs text-gray-700 pl-1.5"> + 📌 Par défaut, la situation du cas type est + considérée comme stable depuis 3 ans. + </span> + </div> + <div class="overflow-y-scroll"> <TestCaseEdit date={$date} inputInstantsByVariableName={$inputInstantsByVariableNameArray[ @@ -2250,6 +2306,7 @@ valuesByCalculationNameByVariableName={$valuesByCalculationNameByVariableNameArray[ displayMode.edit ]} + variableName={displayMode.variableName} year={$year} /> <!-- #75 !253 : cache le panneau, on pourrait le mettre en tooltip/modale plus tard --> @@ -2279,60 +2336,6 @@ <!-- </p>--> <!-- </div>--> <!-- </div>--> - - <TestCaseEditVariablesSearch - date={$date} - inputInstantsByVariableName={$inputInstantsByVariableNameArray[ - displayMode.testCasesIndex[0] - ]} - situation={$testCases[displayMode.testCasesIndex[0]]} - situationIndex={displayMode.testCasesIndex[0]} - valuesByCalculationNameByVariableName={$valuesByCalculationNameByVariableNameArray[ - displayMode.testCasesIndex[0] - ]} - year={$year} - /> - - <div class="flex gap-9 ml-4 mr-4 md:mr-0 mb-3 mt-4"> - <hr class="mt-5 flex-1 border-dashed border-black" /> - <span class="text-lg font-light xl:text-2xl 2xl:text-3xl">ou</span> - <hr class="mt-5 flex-1 border-dashed border-black" /> - </div> - - {#if displayMode.variableName === undefined} - <h2 class="mb-2 ml-4 pt-3 text-xl font-bold"> - Ajouter d'autres caractéristiques : - </h2> - <p class="font-sm mx-4 mb-10 hidden text-gray-700 md:block"> - ↖️ Cliquez sur le nom d'un dispositif affiché sur le cas type pour - modifier les variables entrant dans le calcul de ce dispositif. - </p> - <p class="font-sm mx-4 mb-10 block text-gray-700 md:hidden"> - Utilisez votre ordinateur ou élargissez l'écran pour obtenir plus de - fonctionnalités. - </p> - {:else} - <VariableReferredInputsPane - date={$date} - inputInstantsByVariableName={$inputInstantsByVariableNameArray[ - displayMode.testCasesIndex[0] - ]} - name={displayMode.variableName} - on:changeInputInstantsByVariableName={({ detail }) => - changeInputInstantsByVariableName( - displayMode.testCasesIndex[0], - detail, - )} - on:changeSituation={({ detail }) => - changeSituation(displayMode.testCasesIndex[0], detail)} - situation={$testCases[displayMode.testCasesIndex[0]]} - situationIndex={displayMode.testCasesIndex[0]} - valuesByCalculationNameByVariableName={$valuesByCalculationNameByVariableNameArray[ - displayMode.testCasesIndex[0] - ]} - year={$year} - /> - {/if} </div> </section> {/if} @@ -2391,7 +2394,7 @@ <!--Bouton flottant mobile pour fermer volet édition du cas type--> <button on:click={() => changeTestCaseToEditIndex(undefined)} - class="fixed bottom-8 right-8 z-40 flex h-12 md:h-[52px] translate-y-[calc(100%+4rem)] rounded-full bg-le-bleu px-4 py-3 text-xl uppercase text-white shadow-lg transition-transform duration-500 ease-out-quart hover:bg-blue-900" + class="fixed bottom-8 right-8 z-40 flex items-center h-12 md:h-[52px] translate-y-[calc(100%+4rem)] rounded-full bg-le-bleu px-4 py-3 text-xl uppercase text-white shadow-lg transition-transform duration-500 ease-out-quart hover:bg-blue-900" class:!translate-y-0={displayMode.edit !== undefined} title="Valider le cas type et fermer l'édition" > diff --git a/src/routes/variables/[variable]/+page.svelte b/src/routes/variables/[variable]/+page.svelte index 074b20079ab863d125ba39e3b730b3e4e7250b2d..03240f4e34d02d04a653c8d1c83cda87f6778616 100644 --- a/src/routes/variables/[variable]/+page.svelte +++ b/src/routes/variables/[variable]/+page.svelte @@ -105,7 +105,7 @@ <main class="bg-polka-dots flex items-center justify-center"> <div class="max-w-screen-md bg-white"> <button - class="ml-10 mt-5 inline-flex cursor-pointer items-center rounded bg-gray-200 p-2 pr-3 text-sm text-black shadow-md hover:bg-gray-300 active:bg-gray-400" + class="ml-5 md:ml-10 mt-5 inline-flex cursor-pointer items-center rounded bg-gray-200 p-2 pr-3 text-sm text-black shadow-md hover:bg-gray-300 active:bg-gray-400" on:click={() => goto($displayMode === undefined ? "/" : newSimulationUrl($displayMode))} >