Skip to content

Commit

Permalink
feat(panorama-formations): ajout de la page de recherche d'un domaine…
Browse files Browse the repository at this point in the history
… de formation
  • Loading branch information
gBusato committed Oct 16, 2024
1 parent 181909a commit dca7a4b
Show file tree
Hide file tree
Showing 14 changed files with 424 additions and 1 deletion.
2 changes: 2 additions & 0 deletions server/src/modules/data/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { getDataForPanoramaRegionRoute } from "./usecases/getDataForPanoramaRegi
import { getDemandesRestitutionIntentionsRoute } from "./usecases/getDemandesRestitutionIntentions/getDemandesRestitutionIntentions.route";
import { getDepartementRoute } from "./usecases/getDepartement/getDepartement.route";
import { getDepartementsRoute } from "./usecases/getDepartements/getDepartements.route";
import { getDomainesDeFormationRoute } from "./usecases/getDomainesDeFormation/getDomainesDeFormation.route";
import { getEtablissementRoute } from "./usecases/getEtablissement/getEtablissement.route";
import { getFormationEtablissementsRoutes } from "./usecases/getFormationEtablissements/getFormationEtablissements.routes";
import { getFormationsRoute } from "./usecases/getFormations/getFormations.routes";
Expand Down Expand Up @@ -62,5 +63,6 @@ export const registerFormationModule = ({ server }: { server: Server }) => {
...searchFiliereRoute(server),
...searchCampusRoute(server),
...getRepartitionPilotageIntentionsRoute({ server }),
...getDomainesDeFormationRoute({ server }),
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { sql } from "kysely";

import { kdb } from "../../../../../db/db";
import { cleanNull } from "../../../../../utils/noNull";
import { getNormalizedSearchArray } from "../../../../utils/normalizeSearch";

export const getFormations = (search?: string) => {
let baseQuery = kdb
.selectFrom("formationScolaireView as formationView")
.leftJoin("nsf", "nsf.codeNsf", "formationView.codeNsf")
.select((sb) => [
sb.ref("nsf.libelleNsf").as("label"),
sb.ref("formationView.codeNsf").as("value"),
sb.ref("nsf.codeNsf").as("nsf"),
sb.val("nsf").as("type"),
])
.where("nsf.libelleNsf", "is not", null);

if (search) {
const searchArray = getNormalizedSearchArray(search);

baseQuery = baseQuery
.where((w) =>
w.and(
searchArray.map((search_word) =>
w(
sql`unaccent(${w.ref("nsf.libelleNsf")})`,
"ilike",
`%${search_word}%`
)
)
)
)
.union(
kdb
.selectFrom("formationScolaireView as formationView")
.leftJoin("nsf", "nsf.codeNsf", "formationView.codeNsf")
.where("nsf.libelleNsf", "is not", null)
.select((sb) => [
sb.ref("formationView.libelleFormation").as("label"),
sb.ref("formationView.cfd").as("value"),
sb.ref("nsf.codeNsf").as("nsf"),
sb.val("formation").as("type"),
])
.where((w) =>
w.and(
searchArray.map((search_word) =>
w(
sql`concat(
unaccent(${w.ref("formationView.libelleFormation")}),
' ',
unaccent(${w.ref("formationView.cfd")})
)`,
"ilike",
`%${search_word}%`
)
)
)
)
);
}

console.log(baseQuery.compile().sql);

return baseQuery.orderBy("label", "asc").distinct().execute().then(cleanNull);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { createRoute } from "@http-wizard/core";

import { Server } from "../../../../server";
import { getDomainesDeFormationSchema } from "./getDomainesDeFormation.schema";
import { getDomainesDeFormation } from "./getDomainesDeFormation.usecase";

export const getDomainesDeFormationRoute = ({ server }: { server: Server }) => {
return createRoute("/domaines-de-formation", {
method: "GET",
schema: getDomainesDeFormationSchema,
}).handle((props) =>
server.route({
...props,
handler: async (request, response) => {
const { search } = request.query;
const result = await getDomainesDeFormation(search);
response.status(200).send(result);
},
})
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { z } from "zod";

export const getDomainesDeFormationSchema = {
querystring: z.object({
search: z.string().trim().toLowerCase().optional(),
}),
response: {
200: z.array(
z.object({
label: z.string().optional(),
value: z.string(),
nsf: z.string().optional(),
type: z.enum(["nsf", "formation"]),
})
),
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { getFormations } from "./dependencies/getFormations.dep";

const getDomainesDeFormationFactory =
(deps = { getFormations }) =>
async (search?: string) =>
deps.getFormations(search);

export const getDomainesDeFormation = getDomainesDeFormationFactory();
8 changes: 8 additions & 0 deletions ui/app/(wrapped)/components/Nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,14 @@ export const Nav = () => {
Établissement
</NavMenuLink>
</MenuItem>
<MenuItem p="0">
<NavMenuLink
href="/panorama/formation"
segment="panorama/formation"
>
Domaine de formation
</NavMenuLink>
</MenuItem>
<MenuItem p="0">
<NavMenuLink
href="/panorama/lien-metier-formation"
Expand Down
9 changes: 9 additions & 0 deletions ui/app/(wrapped)/formation/[codeNsf]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
type Params = {
params: {
codeNsf: string;
};
};

export default async function Page({ params: { codeNsf } }: Params) {
return <div>page du code Nsf {codeNsf}</div>;
}
3 changes: 3 additions & 0 deletions ui/app/(wrapped)/formation/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { redirect } from "next/navigation";

export default () => redirect("/panorama/formation");
133 changes: 133 additions & 0 deletions ui/app/(wrapped)/panorama/formation/client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
"use client";

import {
AspectRatio,
Container,
Flex,
Img,
Text,
VisuallyHidden,
} from "@chakra-ui/react";
import { useRouter } from "next/navigation";
import { useState } from "react";
import Select, { CSSObjectWithLabel, StylesConfig } from "react-select";

import { client } from "../../../../api.client";
import { NsfOption, NsfOptions } from "./types";

const selectStyle: StylesConfig<NsfOption, false> = {
control: (styles: CSSObjectWithLabel) => ({
...styles,
width: "100%",
zIndex: "2",
}),
input: (styles) => ({ ...styles, width: "100%" }),
menu: (styles) => ({ ...styles, width: "100%" }),
container: (styles) => ({ ...styles, width: "100%" }),
groupHeading: (styles) => ({
...styles,
fontWeight: "bold",
color: "#161616",
fontSize: "14px",
}),
};

export const PanoramaFormationClient = ({
defaultNsf,
}: {
defaultNsf: NsfOptions;
}) => {
const router = useRouter();
const [search, setSearch] = useState<string>("");
const [selectedNsf, setSelectedNsf] = useState<NsfOption | null>(null);

const { data, isLoading, refetch } = client
.ref("[GET]/domaines-de-formation")
.useQuery(
{ query: { search } },
{ enabled: !!search, initialData: defaultNsf }
);

const onNsfSelected = (selected: NsfOption | null) => {
if (selected) {
setSelectedNsf(selected);
if (selected.type === "formation") {
router.push(`/formation/${selected.nsf}?cfd=${selected.value}`);
} else {
router.push(`/formation/${selected.value}`);
}
}
};

return (
<Container
px="48px"
as="section"
py="40px"
bg="bluefrance.975"
maxWidth={"container.xl"}
h={"100%"}
>
<Flex align="center" direction="row" justify="space-between">
<VisuallyHidden>
<Text as={"h1"}>
Rechercher un domaine de formation (NSF) ou par formation
</Text>
</VisuallyHidden>
<Flex flexDirection="column" gap="2" w="50%">
<label htmlFor="nsf-select" style={{ fontWeight: "bold" }}>
Rechercher un domaine de formation (NSF) ou par formation
</label>
<Flex width="100%">
<Select
id="nsf-select"
noOptionsMessage={({ inputValue }) =>
inputValue
? "Pas de formation correspondant à votre recherche"
: "Commencez à écrire..."
}
isLoading={isLoading}
inputValue={search}
value={selectedNsf}
options={[
{
label: `DOMAINES DE FORMATION (${
(data ?? []).filter((option) => option.type === "nsf")
.length
})`,
options: (data ?? []).filter(
(option) => option.type === "nsf"
),
},
{
label: `FORMATIONS (${
(data ?? []).filter((option) => option.type === "formation")
.length
})`,
options: (data ?? []).filter(
(option) => option.type === "formation"
),
},
]}
onChange={(selected) => onNsfSelected(selected as NsfOption)}
onInputChange={(value) => setSearch(value)}
isSearchable={true}
onMenuOpen={() => refetch()}
onMenuClose={() => setSearch("")}
placeholder="Saisir un nom de domaine, un libellé de formation, un code diplôme..."
styles={selectStyle}
isMulti={false}
/>
</Flex>
</Flex>
<AspectRatio width="100%" maxW="300px" ratio={2.7} mt="4">
<Img
src="/illustrations/team-at-work.svg"
objectFit="contain"
alt="Illustration équipe en collaboration"
/>
</AspectRatio>
</Flex>
</Container>
);
};
21 changes: 21 additions & 0 deletions ui/app/(wrapped)/panorama/formation/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { headers } from "next/headers";

import { client } from "../../../../api.client";
import { PanoramaFormationClient } from "./client";

const fetchDefaultNsf = async () => {
const headersList = Object.fromEntries(headers().entries());
try {
return await client
.ref("[GET]/domaines-de-formation")
.query({ query: { search: undefined } }, { headers: headersList });
} catch (e) {
return [];
}
};

export default async function Panorama() {
const defaultNsf = await fetchDefaultNsf();

return <PanoramaFormationClient defaultNsf={defaultNsf} />;
}
5 changes: 5 additions & 0 deletions ui/app/(wrapped)/panorama/formation/types.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { client } from "@/api.client";

export type NsfOptions = (typeof client.infer)["[GET]/domaines-de-formation"];
export type NsfOption =
(typeof client.infer)["[GET]/domaines-de-formation"][number];
21 changes: 20 additions & 1 deletion ui/app/(wrapped)/panorama/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ import { useSelectedLayoutSegment } from "next/navigation";
import { ReactNode } from "react";

import { Breadcrumb } from "../../../components/Breadcrumb";
import { feature } from "../../../utils/feature";

const getTabIndex = (segment: string | null) => {
if (segment === "region") return 0;
if (segment === "departement") return 1;
if (segment === "etablissement") return 2;
if (segment === "lien-metier-formation") return 3;
if (segment === "formation") return 3;
if (segment === "lien-metier-formation") return 4;
};

export default function PanoramaLayout({
Expand Down Expand Up @@ -53,6 +55,18 @@ export default function PanoramaLayout({
active: true,
},
];

if (segment === "formation") {
return [
{ title: "Accueil", to: "/" },
{
title: "Panorama domaine de formation",
to: "/panorama/formation",
active: true,
},
];
}

return [
{ title: "Accueil", to: "/" },
{
Expand Down Expand Up @@ -85,6 +99,11 @@ export default function PanoramaLayout({
<Tab as={Link} href="/panorama/etablissement">
Établissement
</Tab>
{feature.panoramaFormation && (
<Tab as={Link} href="/panorama/formation">
Domaine de formation
</Tab>
)}
<Tab as={Link} href="/panorama/lien-metier-formation">
Lien métier formation
</Tab>
Expand Down
Loading

0 comments on commit dca7a4b

Please sign in to comment.