diff --git a/src/lib/components/DevNavBar.svelte b/src/lib/components/DevNavBar.svelte new file mode 100644 index 0000000000000000000000000000000000000000..00e3f5cb3c115e5d3edda7bcd45d2758961d16ed --- /dev/null +++ b/src/lib/components/DevNavBar.svelte @@ -0,0 +1,199 @@ +<script lang="ts"> + import { page, session } from "$app/stores" + + const menuItems = [ + { href: "/", label: "Accueil" }, + { href: "/about", label: "À propos" }, + { href: "/calculations", label: "Calculs" }, + { href: "/entities", label: "Entités" }, + { href: "/parameters", label: "Paramètres" }, + { href: "/storybook", label: "Storybook" }, + { href: "/variables", label: "Variables" }, + ] + let open = false + let openUserMenu = false + + $: pageUrlPath = $page.path.replace(/\/+$/, "") || "/" + + $: title = $session.title + + function activeMenuItem(href: string) { + return pageUrlPath === href || pageUrlPath.startsWith(href + "/") + } +</script> + +<nav class="bg-gray-800"> + <div class="max-w-7xl mx-auto px-2 sm:px-6 lg:px-8"> + <div class="relative flex items-center justify-between h-16"> + <div class="absolute inset-y-0 left-0 flex items-center sm:hidden"> + <!-- Mobile menu button--> + <button + aria-controls="mobile-menu" + aria-expanded={open} + class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white" + on:click={() => (open = !open)} + type="button" + > + <span class="sr-only">Open main menu</span> + {#if open} + <!-- Heroicon name: outline/x --> + <svg + class="block h-6 stroke-current w-6" + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + aria-hidden="true" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + stroke-width="2" + d="M6 18L18 6M6 6l12 12" + /> + </svg> + {:else} + <!-- Heroicon name: outline/menu --> + <svg + class="block h-6 stroke-current w-6" + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + aria-hidden="true" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + stroke-width="2" + d="M4 6h16M4 12h16M4 18h16" + /> + </svg> + {/if} + </button> + </div> + <div + class="flex-1 flex items-center justify-center sm:items-stretch sm:justify-start" + > + <div class="flex-shrink-0 flex items-center"> + <img + alt={title} + class="block lg:hidden h-8 w-auto" + src="/logo_100x100.png" + /> + <img + alt={title} + class="hidden lg:block h-8 w-auto" + src="/logo_100x100.png" + /> + </div> + <div class="hidden sm:block sm:ml-6"> + <div class="flex space-x-4"> + {#each menuItems as { label, href }} + <a + aria-current="page" + class="{activeMenuItem(href) + ? 'bg-gray-900 text-white' + : 'text-gray-300 hover:bg-gray-700 hover:text-white'} px-3 py-2 rounded-md text-sm font-medium" + {href}>{label}</a + > + {/each} + </div> + </div> + </div> + <div + class="absolute inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:ml-6 sm:pr-0" + > + <button + class="bg-gray-800 p-1 rounded-full text-gray-400 hover:text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white" + > + <span class="sr-only">View notifications</span> + <!-- Heroicon name: outline/bell --> + <svg + class="h-6 stroke-current w-6" + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + aria-hidden="true" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + stroke-width="2" + d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" + /> + </svg> + </button> + + <!-- Profile dropdown --> + <div class="ml-3 relative"> + <div> + <button + aria-expanded={openUserMenu} + aria-haspopup="true" + class="bg-gray-800 flex text-sm rounded-full focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white" + id="user-menu" + on:click={() => (openUserMenu = !openUserMenu)} + type="button" + > + <span class="sr-only">Open user menu</span> + <img + class="h-8 w-8 rounded-full" + src="/logo_100x100.png" + alt="" + /> + </button> + </div> + {#if openUserMenu} + <!-- + Dropdown menu + + Entering: "transition ease-out duration-100" + From: "transform opacity-0 scale-95" + To: "transform opacity-100 scale-100" + Leaving: "transition ease-in duration-75" + From: "transform opacity-100 scale-100" + To: "transform opacity-0 scale-95" + --> + <div + class="origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none" + role="menu" + aria-orientation="vertical" + aria-labelledby="user-menu" + > + <a + href="profile" + class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" + role="menuitem">Your Profile</a + > + <a + href="settings" + class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" + role="menuitem">Settings</a + > + <a + href="sign_out" + class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" + role="menuitem">Sign out</a + > + </div> + {/if} + </div> + </div> + </div> + </div> + + {#if open} + <div class="sm:hidden" id="mobile-menu"> + <div class="px-2 pt-2 pb-3 space-y-1"> + {#each menuItems as { label, href }} + <a + aria-current="page" + class="{activeMenuItem(href) + ? 'bg-gray-900 text-white' + : 'text-gray-300 hover:bg-gray-700 hover:text-white'} block px-3 py-2 rounded-md text-base font-medium" + {href}>{label}</a + > + {/each} + </div> + </div> + {/if} +</nav> diff --git a/src/lib/components/NavBar.svelte b/src/lib/components/NavBar.svelte index 00e3f5cb3c115e5d3edda7bcd45d2758961d16ed..8d7bcb68225f5ee8d2f0cba7742c1ff7937bc430 100644 --- a/src/lib/components/NavBar.svelte +++ b/src/lib/components/NavBar.svelte @@ -2,12 +2,9 @@ import { page, session } from "$app/stores" const menuItems = [ - { href: "/", label: "Accueil" }, + { href: "/", label: "Simulation" }, { href: "/about", label: "À propos" }, - { href: "/calculations", label: "Calculs" }, - { href: "/entities", label: "Entités" }, { href: "/parameters", label: "Paramètres" }, - { href: "/storybook", label: "Storybook" }, { href: "/variables", label: "Variables" }, ] let open = false diff --git a/src/routes/dev.svelte b/src/routes/dev.svelte new file mode 100644 index 0000000000000000000000000000000000000000..2eadf70625ec4e31d05028d36e9aa6f5de7b19e3 --- /dev/null +++ b/src/routes/dev.svelte @@ -0,0 +1,31 @@ +<script lang="ts"> + import { session } from "$app/stores" + + $: title = $session.title +</script> + +<svelte:head> + <title>{title}</title> +</svelte:head> + +<main> + <h1>{title}</h1> + + <h2>Tests de l'API OpenFisca</h2> + <ul> + <li><a class="link" href="/calculations">Calculs</a></li> + <li><a class="link" href="/entities">Entités</a></li> + <li><a class="link" href="/parameters">Paramètres</a></li> + <li><a class="link" href="/variables">Variables</a></li> + </ul> + + <h2>Documentation</h2> + <ul> + <li><a class="link" href="/spec">Documentation de l'API</a></li> + <li> + <a class="link" href="/storybook" + >Storybook (design des éléments visuels)</a + > + </li> + </ul> +</main> diff --git a/src/routes/dev/__layout.reset.svelte b/src/routes/dev/__layout.reset.svelte new file mode 100644 index 0000000000000000000000000000000000000000..87600dc80a0f83dc7581957723ceb4d1422d7261 --- /dev/null +++ b/src/routes/dev/__layout.reset.svelte @@ -0,0 +1,27 @@ +<script lang="ts"> + import "@fontsource/lato/index.css" + import "@fontsource/lora/index.css" + + import "../../global.css" + + import { setContext } from "svelte" + import { writable } from "svelte/store" + + import DevNavBar from "$lib/components/DevNavBar.svelte" + + const reform = writable({}) + setContext("reform", reform) + + const simulationRequested = writable(false) + setContext("simulationRequested", simulationRequested) + + const situationComplement = writable({}) + setContext("situationComplement", situationComplement) + + const situationCore = writable({}) + setContext("situationCore", situationCore) +</script> + +<DevNavBar /> + +<slot /> diff --git a/src/routes/calculations/index.svelte b/src/routes/dev/calculations.svelte similarity index 100% rename from src/routes/calculations/index.svelte rename to src/routes/dev/calculations.svelte diff --git a/src/routes/entities/index.svelte b/src/routes/dev/entities.svelte similarity index 100% rename from src/routes/entities/index.svelte rename to src/routes/dev/entities.svelte diff --git a/src/routes/dev/index.svelte b/src/routes/dev/index.svelte new file mode 100644 index 0000000000000000000000000000000000000000..b6378b8e0e9ffe302bdcb92a67b1467dc11ada70 --- /dev/null +++ b/src/routes/dev/index.svelte @@ -0,0 +1,46 @@ +<script lang="ts"> + import { session } from "$app/stores" + + $: title = $session.title +</script> + +<svelte:head> + <title>{title}</title> +</svelte:head> + +<main> + <h1 class="text-2xl">{title}</h1> + + <section class="mt-8"> + <h1 class="mb-4 text-xl">Pages de "<em>production</em>"</h1> + + <ul> + <li><a class="link" href="/">Page d'accueil (simulation)</a></li> + </ul> + </section> + + <section class="mt-8"> + <h1 class="mb-4 text-xl">Pages de développement et de test</h1> + + <ul> + <li><a class="link" href="/dev">Cette page d'accueil</a></li> + <li><a class="link" href="/calculations">Calculs</a></li> + <li><a class="link" href="/entities">Entités</a></li> + <li><a class="link" href="/parameters">Paramètres</a></li> + <li><a class="link" href="/variables">Variables</a></li> + </ul> + </section> + + <section class="mt-8"> + <h1 class="mb-4 text-xl">Documentation</h1> + + <ul> + <li><a class="link" href="/spec">Documentation de l'API</a></li> + <li> + <a class="link" href="/storybook" + >Storybook (design des éléments visuels)</a + > + </li> + </ul> + </section> +</main> diff --git a/src/routes/dev/parameters/[parameter].svelte b/src/routes/dev/parameters/[parameter].svelte new file mode 100644 index 0000000000000000000000000000000000000000..1878b303c0bb5ef9c938ff4d0b769b9bf6961d9b --- /dev/null +++ b/src/routes/dev/parameters/[parameter].svelte @@ -0,0 +1,48 @@ +<script context="module" lang="ts"> + import type { LoadInput, LoadOutput } from "@sveltejs/kit/types/page" + + import { improveParameterWithAncestors } from "$lib/parameters" + + export async function load({ + fetch, + page, + session, + }: LoadInput): Promise<LoadOutput> { + const { parameter: name } = page.params + const url = new URL(`parameters/${name}`, session.apiBaseUrl).toString() + const res = await fetch(url) + if (!res.ok) { + return { + status: res.status, + error: new Error(`Could not load ${url}`), + } + } + const parameterWithAncestors = await res.json() + return { + props: { + parameter: improveParameterWithAncestors(parameterWithAncestors), + }, + } + } +</script> + +<script lang="ts"> + import { setContext } from "svelte" + + import { session } from "$app/stores" + import ParameterView from "$lib/components/parameters/ParameterView.svelte" + import type { AnyParameter } from "$lib/parameters" + import { newSelfTargetAProps } from "$lib/urls" + + export let parameter: AnyParameter + + setContext("newSelfTargetAProps", newSelfTargetAProps) +</script> + +<svelte:head> + <title>{parameter.name} | Paramètres | {$session.title}</title> +</svelte:head> + +<main> + <ParameterView editable={true} {parameter} /> +</main> diff --git a/src/routes/dev/parameters/index.svelte b/src/routes/dev/parameters/index.svelte new file mode 100644 index 0000000000000000000000000000000000000000..1784427605a87e8395f1a339576dc2b485f6c408 --- /dev/null +++ b/src/routes/dev/parameters/index.svelte @@ -0,0 +1,87 @@ +<script context="module" lang="ts"> + import type { LoadInput, LoadOutput } from "@sveltejs/kit/types/page" + + import type { AnyParameter, ParameterNode } from "$lib/parameters" + import { improveParameter } from "$lib/parameters" + + export async function load({ + fetch, + session, + }: LoadInput): Promise<LoadOutput> { + const url = new URL("parameters/", session.apiBaseUrl).toString() + const res = await fetch(url) + if (!res.ok) { + return { + status: res.status, + error: new Error(`Could not load ${url}`), + } + } + const rootParameter = await res.json() + improveParameter(null, rootParameter) + return { + props: { + rootParameter, + }, + } + } +</script> + +<script lang="ts"> + import { goto } from "$app/navigation" + import { page, session } from "$app/stores" + import ParametersSearch from "$lib/components/parameters/ParametersSearch.svelte" + + export let rootParameter: ParameterNode + + let initialTerm: string | undefined = undefined + + $: term = $page.query.get("q") ?? "" + + function searchTermChanged({ detail }: { detail: string }) { + if (initialTerm === undefined) { + initialTerm = term + } + term = detail + history.replaceState( + null, + "", + `${$page.path}${term ? `?q=${encodeURIComponent(term)}` : ""}`, + ) + } + + async function parameterClicked({ + detail: parameter, + }: { + detail: AnyParameter + }) { + if (initialTerm !== undefined) { + // Restore the initial term in browser history. + await goto( + `${$page.path}${ + initialTerm ? `?q=${encodeURIComponent(initialTerm)}` : "" + }`, + { replaceState: true }, + ) + // Push the current term. + await goto(`${$page.path}${term ? `?q=${encodeURIComponent(term)}` : ""}`) + } + // Go to parameter page. + await goto(`/parameters/${parameter.name}`) + } +</script> + +<svelte:head> + <title>Paramètres{term ? ` « ${term} »` : " "} | {$session.title}</title> +</svelte:head> + +<main> + <h1>Paramètres</h1> + + <ParametersSearch + dispatchItemClick={true} + on:change={searchTermChanged} + on:itemClick={parameterClicked} + {rootParameter} + {term} + /> +</main> diff --git a/src/routes/dev/variables/[variable]/index.svelte b/src/routes/dev/variables/[variable]/index.svelte new file mode 100644 index 0000000000000000000000000000000000000000..a161cb81412992ca50033e75a31f6a5d0dc0b534 --- /dev/null +++ b/src/routes/dev/variables/[variable]/index.svelte @@ -0,0 +1,45 @@ +<script context="module" lang="ts"> + import type { LoadInput, LoadOutput } from "@sveltejs/kit/types/page" + + export async function load({ + fetch, + page, + session, + }: LoadInput): Promise<LoadOutput> { + const { variable: name } = page.params + const url = new URL(`variables/${name}`, session.apiBaseUrl).toString() + const res = await fetch(url) + if (!res.ok) { + return { + status: res.status, + error: new Error(`Could not load ${url}`), + } + } + return { + props: { + variable: await res.json(), + }, + } + } +</script> + +<script lang="ts"> + import { setContext } from "svelte" + + import { session } from "$app/stores" + import VariableView from "$lib/components/variables/VariableView.svelte" + import { newSelfTargetAProps } from "$lib/urls" + import type { Variable } from "$lib/variables" + + export let variable: Variable + + setContext("newSelfTargetAProps", newSelfTargetAProps) +</script> + +<svelte:head> + <title>{variable.name} | Variables | {$session.title}</title> +</svelte:head> + +<main> + <VariableView editable={true} {variable} /> +</main> diff --git a/src/routes/dev/variables/[variable]/inputs/[date].svelte b/src/routes/dev/variables/[variable]/inputs/[date].svelte new file mode 100644 index 0000000000000000000000000000000000000000..5aabc155a082778b78f3d87b94f5876a1c130cec --- /dev/null +++ b/src/routes/dev/variables/[variable]/inputs/[date].svelte @@ -0,0 +1,89 @@ +<script context="module" lang="ts"> + import type { LoadInput, LoadOutput } from "@sveltejs/kit/types/page" + + export async function load({ + fetch, + page, + session, + }: LoadInput): Promise<LoadOutput> { + const { date, variable: name } = page.params + const results = await Promise.all([ + (async () => { + const url = new URL(`variables/${name}`, session.apiBaseUrl).toString() + const res = await fetch(url) + if (!res.ok) { + return { + status: res.status, + error: new Error(`Could not load ${url}`), + } + } + return { + props: { + variable: await res.json(), + }, + } + })(), + (async () => { + const url = new URL( + `variables/${name}/inputs/${date}`, + session.apiBaseUrl, + ).toString() + const res = await fetch(url) + if (!res.ok) { + return { + status: res.status, + error: new Error(`Could not load ${url}`), + } + } + return { + props: { + inputs: await res.json(), + }, + } + })(), + ]) + const firstResultWithError = results.find( + ({ error }) => error !== undefined, + ) + if (firstResultWithError !== undefined) { + return firstResultWithError + } + return { + props: { + ...Object.fromEntries( + [].concat(...results.map(({ props }) => Object.entries(props))), + ), + }, + } + } +</script> + +<script lang="ts"> + import { setContext } from "svelte" + + import { page, session } from "$app/stores" + import VariableReferredInputs from "$lib/components/variables/VariableReferredInputs.svelte" + import { newSelfTargetAProps } from "$lib/urls" + import type { Variable } from "$lib/variables" + + export let variable: Variable + export let inputs: Variable[] + + setContext("newSelfTargetAProps", newSelfTargetAProps) + + $: params = $page.params + + $: date = params.date + + $: name = params.variable +</script> + +<svelte:head> + <title + >Variables d'entrée au {date} | {name} | Variables | {$session.title}</title + > +</svelte:head> + +<main> + <VariableReferredInputs {inputs} {variable} /> +</main> diff --git a/src/routes/dev/variables/[variable]/parameters/[date].svelte b/src/routes/dev/variables/[variable]/parameters/[date].svelte new file mode 100644 index 0000000000000000000000000000000000000000..0736fc0b9d4936926385d71354506a5fc6a8291b --- /dev/null +++ b/src/routes/dev/variables/[variable]/parameters/[date].svelte @@ -0,0 +1,95 @@ +<script context="module" lang="ts"> + import type { LoadInput, LoadOutput } from "@sveltejs/kit/types/page" + + import { improveParameterWithAncestors } from "$lib/parameters" + + export async function load({ + fetch, + page, + session, + }: LoadInput): Promise<LoadOutput> { + const { date, variable: name } = page.params + const results = await Promise.all([ + (async () => { + const url = new URL(`variables/${name}`, session.apiBaseUrl).toString() + const res = await fetch(url) + if (!res.ok) { + return { + status: res.status, + error: new Error(`Could not load ${url}`), + } + } + return { + props: { + variable: await res.json(), + }, + } + })(), + (async () => { + const url = new URL( + `variables/${name}/parameters/${date}`, + session.apiBaseUrl, + ).toString() + const res = await fetch(url) + if (!res.ok) { + return { + status: res.status, + error: new Error(`Could not load ${url}`), + } + } + const parametersWithAncestors = await res.json() + return { + props: { + parameters: parametersWithAncestors.map( + improveParameterWithAncestors, + ), + }, + } + })(), + ]) + const firstResultWithError = results.find( + ({ error }) => error !== undefined, + ) + if (firstResultWithError !== undefined) { + return firstResultWithError + } + return { + props: { + ...Object.fromEntries( + [].concat(...results.map(({ props }) => Object.entries(props))), + ), + }, + } + } +</script> + +<script lang="ts"> + import { setContext } from "svelte" + + import { page, session } from "$app/stores" + import VariableReferredParameters from "$lib/components/variables/VariableReferredParameters.svelte" + import type { AnyParameter } from "$lib/parameters" + import { newSelfTargetAProps } from "$lib/urls" + import type { Variable } from "$lib/variables" + + export let parameters: AnyParameter[] + export let variable: Variable + + setContext("newSelfTargetAProps", newSelfTargetAProps) + + $: params = $page.params + + $: date = params.date + + $: name = params.variable +</script> + +<svelte:head> + <title + >Paramètres influant au {date} | {name} | Variables | {$session.title}</title + > +</svelte:head> + +<main> + <VariableReferredParameters {date} {parameters} {variable} /> +</main> diff --git a/src/routes/dev/variables/index.svelte b/src/routes/dev/variables/index.svelte new file mode 100644 index 0000000000000000000000000000000000000000..d60d4df8aded9f64dde3c7b6d0a3d6605f5880dc --- /dev/null +++ b/src/routes/dev/variables/index.svelte @@ -0,0 +1,50 @@ +<script context="module" lang="ts"> + import type { LoadInput, LoadOutput } from "@sveltejs/kit/types/page" + + export async function load({ + fetch, + session, + }: LoadInput): Promise<LoadOutput> { + const url = new URL("variables/", session.apiBaseUrl).toString() + const res = await fetch(url) + if (!res.ok) { + return { + status: res.status, + error: new Error(`Could not load ${url}`), + } + } + return { + props: { + variableByName: await res.json(), + }, + } + } +</script> + +<script lang="ts"> + import { session } from "$app/stores" + import type { VariableSummaryByName } from "$lib/variables" + + export let variableByName: VariableSummaryByName +</script> + +<svelte:head> + <title>Variables | {$session.title}</title> +</svelte:head> + +<main> + <h1>Variables</h1> + + <ul class="list-disc list-inside"> + {#each Object.entries(variableByName) as [name, variable]} + <li> + <a class="link" href="variables/{name}"> + <var>{name}</var> + {#if variable.label !== undefined} + : {variable.label} + {/if} + </a> + </li> + {/each} + </ul> +</main> diff --git a/src/routes/index.svelte b/src/routes/index.svelte index 2eadf70625ec4e31d05028d36e9aa6f5de7b19e3..f5f590a67e7ac9618ae4cafa9c0f0ba7415e5b48 100644 --- a/src/routes/index.svelte +++ b/src/routes/index.svelte @@ -1,31 +1,368 @@ <script lang="ts"> - import { session } from "$app/stores" + import { getContext, setContext } from "svelte" + import type { Writable } from "svelte/store" + import { writable } from "svelte/store" + import Sockette from "sockette" - $: title = $session.title + import { browser } from "$app/env" + import { page, session } from "$app/stores" + import { validateCalculationQuery } from "$lib/auditors/queries" + import type { ValidCalculationQuery } from "$lib/calculations" + import CalculationPane from "$lib/components/calculations/CalculationPane.svelte" + import type { Decomposition } from "$lib/decompositions" + import { decomposition as decompositionWithoutValue } from "$lib/decompositions" + import type { ReformChange } from "$lib/reforms" + import type { Axis, Situation, SituationComplement } from "$lib/situations" + + let axes: Axis[][] = [] + let deltaByCode: { [code: string]: number[] } = {} + let decomposition = writable( + updateDecompositionValues( + decompositionWithoutValue as Decomposition, + deltaByCode, + 1, // vectorLength + ), + ) + setContext("decomposition", decomposition) + const reform = getContext("reform") as Writable<ReformChange> + let showNulls = false + const simulationRequested = getContext( + "simulationRequested", + ) as Writable<boolean> + let situation: Situation | undefined = undefined + let situationComplement = getContext( + "situationComplement", + ) as Writable<SituationComplement> + let situationCore = getContext("situationCore") as Writable<Situation> + let vectorIndex = 0 + let vectorLength = 1 + let webSocket: Sockette | undefined = undefined + let webSocketOpen = false + let year = 2021 + + $: query = ensureValidQuery($page.query) + + $: if (browser) { + openWebSocket() + } + + $: if ($simulationRequested) { + $simulationRequested = false + submit() + } + + function changeAxes({ detail }) { + axes = detail + + vectorLength = 1 + for (const parallelAxes of axes) { + // All the parallel axes have the same count. + const axis = parallelAxes[0] + vectorLength *= axis.count + } + + $decomposition = updateDecompositionValues( + decompositionWithoutValue as Decomposition, + deltaByCode, + vectorLength, + ) + if (webSocketOpen) { + submit() + } + } + + function changeSituation({ detail }) { + situation = detail + $situationCore = situation + if (webSocketOpen) { + submit() + } + } + + function changeVectorIndex({ detail }) { + vectorIndex = detail + } + + function ensureValidQuery(query: URLSearchParams): ValidCalculationQuery { + const [validQuery, queryError] = validateCalculationQuery(query) + if (queryError !== null) { + console.warn( + `Query error at ${$page.path}: ${JSON.stringify( + queryError, + null, + 2, + )}\n\n${JSON.stringify(validQuery, null, 2)}`, + ) + return {} + } + return validQuery + } + + function openWebSocket() { + webSocket = new Sockette( + new URL("simulations/calculate", $session.apiWebSocketBaseUrl).toString(), + { + // maxAttempts: 10, + onmessage: (event) => { + const result = JSON.parse(event.data) + if (result.error !== undefined) { + console.error("Error:", result) + } else { + deltaByCode = { + ...deltaByCode, + [result.code]: result.value, + } + $decomposition = updateDecompositionValues( + decompositionWithoutValue as Decomposition, + deltaByCode, + vectorLength, + ) + } + }, + // onopen: (event) => console.log("[WebSocket] Connected!", event), + onopen: () => { + webSocketOpen = true + submit() + }, + // onreconnect: (event) => + // console.log("[WebSocket] Reconnecting...", event), + // onmaximum: (event) => + // console.log("[WebSocket] Stop Attempting!", event), + // onclose: (event) => console.log("[WebSocket] Closed!", event), + // onerror: (event) => console.log("[WebSocket] Error:", event), + // timeout: 5e3, + }, + ) + } + + function submit() { + if (situation === undefined) { + return + } + + let situationWithAxes = { ...situation } + for (const [entityPlural, entitySituationComplement] of Object.entries( + $situationComplement, + )) { + const entitySituation = (situationWithAxes[entityPlural] = { + ...situationWithAxes[entityPlural], + }) + for (const [itemName, itemSituationComplement] of Object.entries( + entitySituationComplement, + )) { + const itemSituation = (entitySituation[itemName] = { + ...entitySituation[itemName], + }) + for (const [variableName, variableValue] of Object.entries( + itemSituationComplement, + )) { + itemSituation[variableName] = { [year]: variableValue } + } + } + } + + if (axes.length > 0) { + // Remove variables used as axes from situation (otherwise OpenFisca Core fails). + situationWithAxes = { + axes, + ...situationWithAxes, + } + for (const parallelAxes of axes) { + nextAxis: for (const axis of parallelAxes) { + const { name: code, index } = axis + + let individuIndex = 0 + for (let [name, individu] of Object.entries( + situationWithAxes.individus, + )) { + if (index === individuIndex && individu[code] !== undefined) { + individu = { ...individu } + delete individu[code] + situationWithAxes.individus = { ...situationWithAxes.individus } + situationWithAxes.individus[name] = individu // Preserve order of individu in individus. + continue nextAxis + } + } + + let familleIndex = 0 + for (let [name, famille] of Object.entries( + situationWithAxes.familles, + )) { + if (index === familleIndex && famille[code] !== undefined) { + famille = { ...famille } + delete famille[code] + situationWithAxes.familles = { ...situationWithAxes.familles } + situationWithAxes.familles[name] = famille // Preserve order of famille in familles. + continue nextAxis + } + } + + let foyerFiscalIndex = 0 + for (let [name, foyerFiscal] of Object.entries( + situationWithAxes.foyers_fiscaux, + )) { + if (index === foyerFiscalIndex && foyerFiscal[code] !== undefined) { + foyerFiscal = { ...foyerFiscal } + delete foyerFiscal[code] + situationWithAxes.foyers_fiscaux = { + ...situationWithAxes.foyers_fiscaux, + } + situationWithAxes.foyers_fiscaux[name] = foyerFiscal // Preserve order of foyerFiscal in foyersFiscaux. + continue nextAxis + } + } + + let menageIndex = 0 + for (let [name, menage] of Object.entries( + situationWithAxes.menages, + )) { + if (index === menageIndex && menage[code] !== undefined) { + menage = { ...menage } + delete menage[code] + situationWithAxes.menages = { ...situationWithAxes.menages } + situationWithAxes.menages[name] = menage // Preserve order of menage in menages. + continue nextAxis + } + } + } + } + } + + webSocket.send( + JSON.stringify({ + decomposition: $decomposition, + reform: $reform, + situation: situationWithAxes, + period: year.toString(), + }), + ) + webSocket.send( + JSON.stringify({ + calculate: true, + }), + ) + } + + function updateDecompositionValues( + node: Decomposition, + deltaByCode: { [code: string]: number[] }, + vectorLength: number, + valuePrevious = undefined, + ): Decomposition { + if (valuePrevious === undefined) { + valuePrevious = new Array(vectorLength).fill(0) + } + let children = node.children + if (children !== undefined) { + children = [] + let childValuePrevious = valuePrevious + for (let child of node.children) { + child = updateDecompositionValues( + child, + deltaByCode, + vectorLength, + childValuePrevious, + ) + children.push(child) + childValuePrevious = child.values.map((itemValue) => itemValue[1]) + } + } + let delta = deltaByCode[node.code] + if (delta === undefined) { + if (children === undefined) { + delta = new Array(vectorLength).fill(0) + } else { + const firstChildValues = children[0].values + const lastChildValues = children[children.length - 1].values + delta = lastChildValues.map( + (lastChildValue, index) => + lastChildValue[1] - firstChildValues[index][0], + ) + } + } + return { + ...node, + children, + delta, + values: valuePrevious.map((previousItemValue, index) => [ + previousItemValue, + previousItemValue + delta[index], + ]), + } + } </script> <svelte:head> - <title>{title}</title> + <title>Calculs | {$session.title}</title> </svelte:head> <main> - <h1>{title}</h1> - - <h2>Tests de l'API OpenFisca</h2> - <ul> - <li><a class="link" href="/calculations">Calculs</a></li> - <li><a class="link" href="/entities">Entités</a></li> - <li><a class="link" href="/parameters">Paramètres</a></li> - <li><a class="link" href="/variables">Variables</a></li> - </ul> - - <h2>Documentation</h2> - <ul> - <li><a class="link" href="/spec">Documentation de l'API</a></li> - <li> - <a class="link" href="/storybook" - >Storybook (design des éléments visuels)</a - > - </li> - </ul> + <label class="block"> + Année + <input max={2021} min={2013} step="1" type="number" bind:value={year} /> + </label> + + <div> + <button class="border rounded p-1" on:click={submit}>Simuler</button> + </div> + + <div class="flex w-full"> + <section class="overflow-auto relative w-1/3"> + <CalculationPane + actions={query.pane1} + on:changeAxes={changeAxes} + on:changeSituation={changeSituation} + on:changeVectorIndex={changeVectorIndex} + pane="pane1" + {query} + {showNulls} + {vectorIndex} + {year} + /> + </section> + <section class="overflow-auto relative w-2/3"> + <CalculationPane + actions={query.pane2} + on:changeAxes={changeAxes} + on:changeSituation={changeSituation} + on:changeVectorIndex={changeVectorIndex} + pane="pane2" + {query} + {showNulls} + {vectorIndex} + {year} + /> + </section> + </div> + <div class="flex w-full"> + <section class="overflow-auto relative w-1/3"> + <CalculationPane + actions={query.pane3} + on:changeAxes={changeAxes} + on:changeSituation={changeSituation} + on:changeVectorIndex={changeVectorIndex} + pane="pane3" + {query} + {showNulls} + {vectorIndex} + {year} + /> + </section> + <section class="overflow-auto relative w-2/3"> + <CalculationPane + actions={query.pane4} + on:changeAxes={changeAxes} + on:changeSituation={changeSituation} + on:changeVectorIndex={changeVectorIndex} + pane="pane4" + {query} + {showNulls} + {vectorIndex} + {year} + /> + </section> + </div> + <label + ><input bind:checked={showNulls} type="checkbox" /> Montrer les montants nuls</label + > </main>