diff --git a/src/lib/components/BudgetConnexionModal.svelte b/src/lib/components/BudgetConnexionModal.svelte index f41b9775c18bc687ad378c0328b0549fb3ec1d79..d6e50f515f6d26a935b8e490a75f267f70357ee3 100644 --- a/src/lib/components/BudgetConnexionModal.svelte +++ b/src/lib/components/BudgetConnexionModal.svelte @@ -10,14 +10,20 @@ import { browser } from "$app/environment" import { goto } from "$app/navigation" import { page } from "$app/stores" + import type { DisplayMode } from "$lib/displays" import { trackBudgetPublicSimulation, trackBudgetSignInButton, } from "$lib/matomo" + import type { ParametricReform } from "$lib/reforms" import type { CachedSimulation } from "$lib/simulations" + export let displayMode: DisplayMode export let isOpen = false + const budgetSimulation = getContext("budgetSimulation") as Writable< + BudgetSimulation | undefined + > let cachedSimulations: CachedSimulation[] = [] $: if (browser && isOpen) { @@ -39,6 +45,36 @@ function onClose() { isOpen = false } + + async function sendSimulationRequest() { + const urlString = "/budget/demande" + const res = await fetch(urlString, { + body: JSON.stringify({ + displayMode, + parametricReform: Object.entries($parametricReform) + .sort(([keyA], [keyB]) => keyA.localeCompare(keyB)) + .reduce((filtered: ParametricReform, [parameterName, value]) => { + if (budgetEditableParametersName.has(parameterName)) { + filtered[parameterName] = value + } + return filtered + }, {}), + }), + headers: { + Accept: "application/json", + "Content-Type": "application/json; charset=utf-8", + }, + method: "POST", + }) + if (!res.ok) { + console.error( + `Error ${ + res.status + } while sending a simulation request at ${urlString}\n\n${await res.text()}`, + ) + return + } + } </script> <Transition appear show={isOpen}> @@ -154,8 +190,34 @@ <div class="flex md:flex-row flex-col w-full px-0 md:px-10 items-center gap-5" > +==== BASE ==== + Voir la simulation<iconify-icon + class="align-[-0.25rem] text-xl" + icon="ri-arrow-right-line" + /> + </a> + </div> + {/if} + + <div class="flex flex-col gap-10 bg-gray-100 p-5 border-t mt-10"> + {#if currentSimulationCache === undefined} + <div> + <h2 class="w-full text-left text-2xl font-bold"> + Demandez le calcul de votre réforme au service + LexImpact : + </h2> + <p class="w-full text-base font-normal leading-6 mt-1.5 mb-5"> + Après vérification par nos services, si elle est calculable + avec les données dont nous disposons et répond au secret + statistique, la simulation sera rendue publique. Vous serez + alors informé par e-mail : + </p> + <span class="font-bold text-sm py-2 pl-10" + >Votre adresse e-mail :</span + > <div - class="flex rounded-t-md border-b-2 border-b-black bg-white px-2 grow max-w-lg" + class="flex md:flex-row flex-col w-full px-0 md:px-10 items-center gap-5" +==== BASE ==== > <input autocomplete="off" @@ -178,8 +240,25 @@ class="ml-2 align-[-0.25rem] text-xl" icon="ri-send-plane-2-line" /> - </a> +==== BASE ==== + </div> + <div> + <a + class="flex items-center gap-2 py-2 px-5 shadow-lg bg-white hover:bg-gray-100 active:bg-gray-200 rounded-md border-2 border-le-bleu text-le-bleu text-sm font-bold tracking-[0.085em] uppercase" + data-sveltekit-reload + href={`/auth/login?redirect=${encodeURIComponent( + $page.url.toString(), + )}`} + title="Envoyer votre réforme budgétaire avec cet e-mail" + > + Demander le calcul <iconify-icon + class="ml-2 align-[-0.25rem] text-xl" + icon="ri-send-plane-2-line" + /> + </a> + </div> </div> +==== BASE ==== </div> </div> {#if cachedSimulations !== undefined && cachedSimulations.length > 0} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index ebd94e0feb44ebf11fc3faf04b2c96072a2399ab..1c1a4276f00e9345c9eaab186bfff8bfe613000f 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1762,7 +1762,10 @@ </div> </div> - <BudgetConnexionModal bind:isOpen={isBudgetConnexionModalOpen} /> + <BudgetConnexionModal + bind:isOpen={isBudgetConnexionModalOpen} + {displayMode} + /> {/if} <!--Bouton flottant "simulation publique" --> diff --git a/src/routes/budget/demande/+server.ts b/src/routes/budget/demande/+server.ts new file mode 100644 index 0000000000000000000000000000000000000000..7bf1f5c03c0ba22db6a59911f55ea8bdeae288c6 --- /dev/null +++ b/src/routes/budget/demande/+server.ts @@ -0,0 +1,103 @@ +import { auditRequire, cleanAudit, type Audit } from "@auditors/core" +import { error, json } from "@sveltejs/kit" +import fs from "fs-extra" +import path from "path" + +import { getParameter, rootParameter } from "$lib/parameters" +import type { ParametricReform } from "$lib/reforms" +import config from "$lib/server/config" +import type { CachedSimulation } from "$lib/simulations" + +import type { RequestHandler } from "./$types" +import { hashObject } from "$lib/hash" + +const { simulationsBudgetDir } = config + +function auditBody(audit: Audit, dataUnknown: unknown): [unknown, unknown] { + if (dataUnknown == null) { + return [dataUnknown, null] + } + if (typeof dataUnknown !== "object") { + return audit.unexpectedType(dataUnknown, "object") + } + + const data = { ...dataUnknown } + const errors: { [key: string]: unknown } = {} + const remainingKeys = new Set(Object.keys(data)) + + audit.attribute( + data, + "displayMode", + true, + errors, + remainingKeys, + // TODO + auditRequire, + ) + audit.attribute( + data, + "parametricReform", + true, + errors, + remainingKeys, + // TODO + auditRequire, + ) + + return audit.reduceRemaining(data, errors, remainingKeys) +} + +export const POST: RequestHandler = async ({ request, url }) => { + const [body, bodyError] = auditBody(cleanAudit, await request.json()) + if (bodyError !== null) { + console.error( + `Error in ${url.pathname} body:\n${JSON.stringify( + body, + null, + 2, + )}\n\nError:\n${JSON.stringify(bodyError, null, 2)}`, + ) + throw error(400, `Invalid body: ${JSON.stringify(bodyError, null, 2)}`) + } + + const modifiedParametersTitles = Object.keys( + (body as { parametricReform: ParametricReform }).parametricReform, + ).map((parameterName) => getParameter(rootParameter, parameterName)?.title) + + const modifiedParametersNames = Object.keys( + (body as { parametricReform: ParametricReform }).parametricReform, + ).map((parameterName) => getParameter(rootParameter, parameterName)?.name) + + const bodyJson = JSON.stringify(body, null, 2) + + const digest = hashObject( + (body as { parametricReform: ParametricReform }).parametricReform, + ) + + const simulationDir = path.join(simulationsBudgetDir, digest.substring(0, 2)) + const simulationFilePath = path.join(simulationDir, `${digest}.json`) + if (!(await fs.pathExists(simulationFilePath))) { + await fs.ensureDir(simulationDir) + await fs.writeFile(simulationFilePath, bodyJson) + } + + const indexDir = path.join(simulationsBudgetDir, "index.json") + const indexContents: CachedSimulation[] = (await fs.pathExists(indexDir)) + ? await fs.readJson(indexDir) + : [] + const contents: CachedSimulation[] = [ + ...indexContents, + { + date: new Intl.DateTimeFormat("fr-FR").format(new Date()), + hash: digest, + parameters: modifiedParametersNames, + title: modifiedParametersTitles.join(" | "), + } as CachedSimulation, + ].filter( + (value, index, self) => + index === self.findLastIndex((el) => el.hash === value.hash), + ) + await fs.writeFile(indexDir, JSON.stringify(contents)) + + return json({ token: digest }) +}