diff --git a/reverse_proxy/app/nginx/templates/conf.d/gzip.conf.template b/reverse_proxy/app/nginx/templates/conf.d/gzip.conf.template index 441e5fb04..bb8e1caf2 100644 --- a/reverse_proxy/app/nginx/templates/conf.d/gzip.conf.template +++ b/reverse_proxy/app/nginx/templates/conf.d/gzip.conf.template @@ -1 +1,2 @@ gzip on; +gzip_types *; diff --git a/reverse_proxy/dev/nginx/templates/conf.d/default.conf.template b/reverse_proxy/dev/nginx/templates/conf.d/default.conf.template index 3909cf803..07d814845 100644 --- a/reverse_proxy/dev/nginx/templates/conf.d/default.conf.template +++ b/reverse_proxy/dev/nginx/templates/conf.d/default.conf.template @@ -24,7 +24,7 @@ server { proxy_set_header X-Forwarded-Proto $scheme; proxy_http_version 1.1; - proxy_buffering off; + proxy_buffering on; proxy_cache off; proxy_connect_timeout 60s; proxy_read_timeout 36000s; diff --git a/server/src/cli.ts b/server/src/cli.ts index ed79e175c..557e9851c 100644 --- a/server/src/cli.ts +++ b/server/src/cli.ts @@ -32,7 +32,7 @@ cli.command("create-migration").action(() => `import { Kysely } from "kysely"; export const up = async (db: Kysely) => {}; - + export const down = async (db: Kysely) => {}; ` ) @@ -90,6 +90,25 @@ cli } }); +cli + .command("create-user") + .requiredOption("--email ") + .requiredOption("--firstname ") + .requiredOption("--lastname ") + .requiredOption("--role ") + .option("--codeRegion ") + .action( + async (options: { + email: string; + firstname: string; + lastname: string; + role: string; + codeRegion?: string; + }) => { + await createUser(options); + } + ); + cli .command("importFiles") .argument("[filename]") diff --git a/server/src/modules/data/queries/utils/tauxDevenirFavorable.ts b/server/src/modules/data/queries/utils/tauxDevenirFavorable.ts index dada7c7b1..f1b7100ad 100644 --- a/server/src/modules/data/queries/utils/tauxDevenirFavorable.ts +++ b/server/src/modules/data/queries/utils/tauxDevenirFavorable.ts @@ -49,22 +49,32 @@ export const selectTauxDevenirFavorableAgg = ( end `; -export function withDevenirFavorableReg({ +type EbRef> = Parameters[0]; + +export function withTauxDevenirFavorableReg< + EB extends ExpressionBuilder, +>({ eb, millesimeSortie, + cfdRef, + dispositifIdRef, + codeRegionRef, }: { - eb: ExpressionBuilder; + eb: EB; millesimeSortie: string; + cfdRef: EbRef; + dispositifIdRef: EbRef; + codeRegionRef: EbRef; }) { return eb .selectFrom("indicateurRegionSortie as subIRS") - .whereRef("subIRS.cfd", "=", "formationEtablissement.cfd") - .whereRef("subIRS.dispositifId", "=", "formationEtablissement.dispositifId") + .whereRef("subIRS.cfd", "=", cfdRef) + .whereRef("subIRS.dispositifId", "=", dispositifIdRef) .where("subIRS.millesimeSortie", "=", millesimeSortie) .whereRef( "subIRS.codeRegion", "=", - sql`ANY(array_agg(${eb.ref("etablissement.codeRegion")}))` + sql`ANY(array_agg(${eb.ref(codeRegionRef)}))` ) .select([selectTauxDevenirFavorableAgg("subIRS").as("sa")]) .groupBy(["subIRS.cfd", "subIRS.dispositifId"]); diff --git a/server/src/modules/data/routes/pilotageTransformation.routes.ts b/server/src/modules/data/routes/pilotageTransformation.routes.ts index ece000a0f..fe480fa1d 100644 --- a/server/src/modules/data/routes/pilotageTransformation.routes.ts +++ b/server/src/modules/data/routes/pilotageTransformation.routes.ts @@ -17,10 +17,11 @@ export const pilotageTransformationRoutes = ({ preHandler: hasPermissionHandler("pilotage-intentions/lecture"), }, async (request, response) => { - const { ...filters } = request.query; + const { order, orderBy, ...filters } = request.query; const stats = await getTransformationStats({ ...filters, + orderBy: order && orderBy ? { order, column: orderBy } : undefined, }); response.status(200).send(stats); } @@ -33,7 +34,11 @@ export const pilotageTransformationRoutes = ({ preHandler: hasPermissionHandler("pilotage-intentions/lecture"), }, async (request, response) => { - const stats = await getFormationsTransformationStats(request.query); + const { order, orderBy, ...filters } = request.query; + const stats = await getFormationsTransformationStats({ + ...filters, + orderBy: order && orderBy ? { order, column: orderBy } : undefined, + }); response.status(200).send(stats); } ); diff --git a/server/src/modules/data/usecases/getDataForPanorama/dependencies.ts b/server/src/modules/data/usecases/getDataForPanorama/dependencies.ts index 91a45e93b..7a291714c 100644 --- a/server/src/modules/data/usecases/getDataForPanorama/dependencies.ts +++ b/server/src/modules/data/usecases/getDataForPanorama/dependencies.ts @@ -4,7 +4,7 @@ import { kdb } from "../../../../db/db"; import { cleanNull } from "../../../../utils/noNull"; import { effectifAnnee } from "../../queries/utils/effectifAnnee"; import { hasContinuum } from "../../queries/utils/hasContinuum"; -import { withDevenirFavorableReg } from "../../queries/utils/tauxDevenirFavorable"; +import { withTauxDevenirFavorableReg } from "../../queries/utils/tauxDevenirFavorable"; import { withInsertionReg } from "../../queries/utils/tauxInsertion6mois"; import { withPoursuiteReg } from "../../queries/utils/tauxPoursuite"; import { selectTauxPressionAgg } from "../../queries/utils/tauxPression"; @@ -122,9 +122,13 @@ export const queryFormations = async ({ codeRegionRef: "etablissement.codeRegion", }).as("continuum"), (eb) => - withDevenirFavorableReg({ eb, millesimeSortie }).as( - "tauxDevenirFavorable" - ), + withTauxDevenirFavorableReg({ + eb, + millesimeSortie, + cfdRef: "formationEtablissement.cfd", + dispositifIdRef: "formationEtablissement.dispositifId", + codeRegionRef: "etablissement.codeRegion", + }).as("tauxDevenirFavorable"), ]) .$narrowType<{ tauxInsertion6mois: number; diff --git a/server/src/modules/data/usecases/getFormationsTransformationStats/getFormationsStatsQuery.dep.ts b/server/src/modules/data/usecases/getFormationsTransformationStats/getFormationsStatsQuery.dep.ts index 62bc035b1..0f0e7fba5 100644 --- a/server/src/modules/data/usecases/getFormationsTransformationStats/getFormationsStatsQuery.dep.ts +++ b/server/src/modules/data/usecases/getFormationsTransformationStats/getFormationsStatsQuery.dep.ts @@ -4,6 +4,7 @@ import { kdb } from "../../../../db/db"; import { DB } from "../../../../db/schema"; import { cleanNull } from "../../../../utils/noNull"; import { hasContinuum } from "../../queries/utils/hasContinuum"; +import { withTauxDevenirFavorableReg } from "../../queries/utils/tauxDevenirFavorable"; import { withInsertionReg } from "../../queries/utils/tauxInsertion6mois"; import { withPoursuiteReg } from "../../queries/utils/tauxPoursuite"; import { withTauxPressionReg } from "../../queries/utils/tauxPression"; @@ -30,17 +31,25 @@ const selectDifferencePlaces = ( - ${eb.ref("capaciteApprentissageActuelle")})`; }; +const selectPlacesTransformees = (eb: ExpressionBuilder) => + sql`GREATEST(${eb.ref("capaciteScolaire")} + - ${eb.ref("capaciteScolaireActuelle")}, 0) + + GREATEST(${eb.ref("capaciteApprentissage")} + - ${eb.ref("capaciteApprentissageActuelle")}, 0) + + GREATEST(${eb.ref("capaciteScolaireActuelle")} + - ${eb.ref("capaciteScolaire")}, 0)`; + const selectPlacesOuvertes = (eb: ExpressionBuilder) => sql`GREATEST(${eb.ref("capaciteScolaire")} - - ${eb.ref("capaciteScolaireActuelle")}, 0) - + GREATEST(${eb.ref("capaciteApprentissage")} - - ${eb.ref("capaciteApprentissageActuelle")}, 0)`; + - ${eb.ref("capaciteScolaireActuelle")}, 0) + + GREATEST(${eb.ref("capaciteApprentissage")} + - ${eb.ref("capaciteApprentissageActuelle")}, 0)`; const selectPlacesFermees = (eb: ExpressionBuilder) => sql`GREATEST(${eb.ref("capaciteScolaireActuelle")} - - ${eb.ref("capaciteScolaire")}, 0) - + GREATEST(${eb.ref("capaciteApprentissageActuelle")} - - ${eb.ref("capaciteApprentissage")}, 0)`; + - ${eb.ref("capaciteScolaire")}, 0) + + GREATEST(${eb.ref("capaciteApprentissageActuelle")} + - ${eb.ref("capaciteApprentissage")}, 0)`; const selectNbDemandes = (eb: ExpressionBuilder) => eb.fn.count("demande.id").distinct(); @@ -59,6 +68,7 @@ export const getFormationsTransformationStatsQuery = ({ tauxPression, codeNiveauDiplome, filiere, + orderBy, }: { status?: "draft" | "submitted"; type?: "fermeture" | "ouverture"; @@ -69,6 +79,7 @@ export const getFormationsTransformationStatsQuery = ({ tauxPression?: "eleve" | "faible"; codeNiveauDiplome?: string[]; filiere?: string[]; + orderBy?: { column: string; order: "asc" | "desc" }; }) => { const partition = (() => { if (codeDepartement) return ["dataEtablissement.codeDepartement"] as const; @@ -119,6 +130,13 @@ export const getFormationsTransformationStatsQuery = ({ dispositifIdRef: "demande.dispositifId", codeRegionRef: "dataEtablissement.codeRegion", }).as("tauxPression"), + withTauxDevenirFavorableReg({ + eb, + millesimeSortie: "2020_2021", + cfdRef: "demande.cfd", + dispositifIdRef: "demande.dispositifId", + codeRegionRef: "dataEtablissement.codeRegion", + }).as("tauxDevenirFavorable"), selectNbDemandes(eb).as("nbDemandes"), selectNbEtablissements(eb).as("nbEtablissements"), sql`ABS(${eb.fn.sum(selectDifferencePlaces(eb, type))})`.as( @@ -126,6 +144,7 @@ export const getFormationsTransformationStatsQuery = ({ ), eb.fn.sum(selectPlacesOuvertes(eb)).as("placesOuvertes"), eb.fn.sum(selectPlacesFermees(eb)).as("placesFermees"), + eb.fn.sum(selectPlacesTransformees(eb)).as("placesTransformees"), hasContinuum({ eb, millesimeSortie: "2020_2021", @@ -222,6 +241,14 @@ export const getFormationsTransformationStatsQuery = ({ if (!filiere?.length) return q; return q.where("dataFormation.libelleFiliere", "in", filiere); }) + .$call((q) => { + if (!orderBy) return q; + return q.orderBy( + sql.ref(orderBy.column), + sql`${sql.raw(orderBy.order)} NULLS LAST` + ); + }) + .orderBy("tauxDevenirFavorable", "desc") .execute() .then(cleanNull); }; diff --git a/server/src/modules/data/usecases/getFormationsTransformationStats/getFormationsTransformationStats.usecase.ts b/server/src/modules/data/usecases/getFormationsTransformationStats/getFormationsTransformationStats.usecase.ts index ff56de3be..a5bc38d4c 100644 --- a/server/src/modules/data/usecases/getFormationsTransformationStats/getFormationsTransformationStats.usecase.ts +++ b/server/src/modules/data/usecases/getFormationsTransformationStats/getFormationsTransformationStats.usecase.ts @@ -16,6 +16,7 @@ export const [getFormationsTransformationStats] = inject( tauxPression?: "eleve" | "faible"; codeNiveauDiplome?: string[]; filiere?: string[]; + orderBy?: { column: string; order: "asc" | "desc" }; }) => { const [stats, formations] = await Promise.all([ deps.getRegionStats({ diff --git a/server/src/modules/data/usecases/getTransformationStats/getTransformationStats.usecase.ts b/server/src/modules/data/usecases/getTransformationStats/getTransformationStats.usecase.ts index 0593149ca..405a15507 100644 --- a/server/src/modules/data/usecases/getTransformationStats/getTransformationStats.usecase.ts +++ b/server/src/modules/data/usecases/getTransformationStats/getTransformationStats.usecase.ts @@ -6,8 +6,48 @@ import { getTransformationStatsQuery, } from "./getTransformationStatsQuery.dep"; +type DataTerritoire = Awaited< + ReturnType +>[0]["region" | "academie" | "departement"]; + +const formatTerritoire = (item: DataTerritoire) => ({ + ...item, + countDemande: item.countDemande || 0, + placesOuvertesScolaire: item.placesOuvertesScolaire || 0, + placesOuvertesApprentissage: item.placesOuvertesApprentissage || 0, + placesOuvertes: + item.placesOuvertesScolaire + item.placesOuvertesApprentissage || 0, + placesFermeesScolaire: item.placesFermeesScolaire || 0, + placesFermeesApprentissage: item.placesFermeesApprentissage || 0, + placesFermees: + item.placesFermeesScolaire + item.placesFermeesApprentissage || 0, + ratioOuverture: + Math.round( + ((item.placesOuvertesScolaire + item.placesOuvertesApprentissage) / + (item.placesOuvertesScolaire + + item.placesOuvertesApprentissage + + item.placesFermeesScolaire + + item.placesFermeesApprentissage) || 0) * 10000 + ) / 100, + ratioFermeture: + Math.round( + ((item.placesFermeesScolaire + item.placesFermeesApprentissage) / + (item.placesOuvertesScolaire + + item.placesOuvertesApprentissage + + item.placesFermeesScolaire + + item.placesFermeesApprentissage) || 0) * 10000 + ) / 100, + differenceCapaciteScolaire: item.differenceCapaciteScolaire || 0, + differenceCapaciteApprentissage: item.differenceCapaciteApprentissage || 0, + placesTransformees: + item.placesOuvertesScolaire + + item.placesOuvertesApprentissage + + item.placesFermeesScolaire || 0, +}); + const formatResult = ( - result: Awaited> + result: Awaited>, + orderBy?: { column: string; order: "asc" | "desc" } ) => { return { national: { @@ -16,13 +56,40 @@ const formatResult = ( placesOuvertesScolaire: result[0]?.national.placesOuvertesScolaire || 0, placesOuvertesApprentissage: result[0]?.national.placesOuvertesApprentissage || 0, + placesOuvertes: + result[0]?.national.placesOuvertesScolaire + + result[0]?.national.placesOuvertesApprentissage || 0, placesFermeesScolaire: result[0]?.national.placesFermeesScolaire || 0, placesFermeesApprentissage: result[0]?.national.placesFermeesApprentissage || 0, + placesFermees: + result[0]?.national.placesFermeesScolaire + + result[0]?.national.placesFermeesApprentissage || 0, + ratioFermeture: + Math.round( + ((result[0]?.national.placesFermeesScolaire + + result[0]?.national.placesFermeesApprentissage) / + (result[0]?.national.placesOuvertesScolaire + + result[0]?.national.placesOuvertesApprentissage + + result[0]?.national.placesFermeesScolaire + + result[0]?.national.placesFermeesApprentissage) || 0) * 10000 + ) / 100, + ratioOuverture: + Math.round( + ((result[0]?.national.placesOuvertesScolaire + + result[0]?.national.placesOuvertesApprentissage) / + (result[0]?.national.placesOuvertesScolaire + + result[0]?.national.placesOuvertesApprentissage + + result[0]?.national.placesFermeesScolaire + + result[0]?.national.placesFermeesApprentissage) || 0) * 10000 + ) / 100, differenceCapaciteScolaire: result[0]?.national.differenceCapaciteScolaire || 0, differenceCapaciteApprentissage: result[0]?.national.differenceCapaciteApprentissage || 0, + placesTransformees: + result[0]?.national.differenceCapaciteScolaire + + result[0]?.national.differenceCapaciteApprentissage || 0, tauxTransformation: Math.round( (result[0]?.national.transformes / effectifNational || 0) * 10000 @@ -31,40 +98,29 @@ const formatResult = ( regions: _.chain(result) .groupBy((item) => item.region.codeRegion) .mapValues((items) => ({ - ...items[0].region, - countDemande: items[0].region.countDemande || 0, - placesOuvertesScolaire: items[0].region.placesOuvertesScolaire || 0, - placesOuvertesApprentissage: - items[0].region.placesOuvertesApprentissage || 0, - placesFermeesScolaire: items[0].region.placesFermeesScolaire || 0, - placesFermeesApprentissage: - items[0].region.placesFermeesApprentissage || 0, - differenceCapaciteScolaire: - items[0].region.differenceCapaciteScolaire || 0, - differenceCapaciteApprentissage: - items[0].region.differenceCapaciteApprentissage || 0, + ...formatTerritoire(items[0].region), + code: `_${items[0].region.codeRegion}`, tauxTransformation: Math.round( (items[0].region.transforme / effectifsRegions[items[0].region.codeRegion ?? ""] || 0) * 10000 ) / 100, })) + .orderBy( + (item) => { + if (orderBy && orderBy.column) + return item[orderBy.column as keyof typeof item]; + return item.libelle; + }, + orderBy?.order ?? "asc" + ) + .keyBy("code") .value(), academies: _.chain(result) .groupBy((item) => item.academie.codeAcademie) .mapValues((items) => ({ - ...items[0].academie, - countDemande: items[0].academie.countDemande || 0, - placesOuvertesScolaire: items[0].academie.placesOuvertesScolaire || 0, - placesOuvertesApprentissage: - items[0].academie.placesOuvertesApprentissage || 0, - placesFermeesScolaire: items[0].academie.placesFermeesScolaire || 0, - placesFermeesApprentissage: - items[0].academie.placesFermeesApprentissage || 0, - differenceCapaciteScolaire: - items[0].academie.differenceCapaciteScolaire || 0, - differenceCapaciteApprentissage: - items[0].academie.differenceCapaciteApprentissage || 0, + ...formatTerritoire(items[0].academie), + code: `_${items[0].academie.codeAcademie}`, tauxTransformation: Math.round( (items[0].academie.transforme / @@ -72,23 +128,21 @@ const formatResult = ( 10000 ) / 100, })) + .orderBy( + (item) => { + if (orderBy && orderBy.column) + return item[orderBy.column as keyof typeof item]; + return item.libelle; + }, + orderBy?.order ?? "asc" + ) + .keyBy("code") .value(), departements: _.chain(result) .groupBy((item) => item.departement.codeDepartement) .mapValues((items) => ({ - ...items[0].departement, - countDemande: items[0].departement.countDemande || 0, - placesOuvertesScolaire: - items[0].departement.placesOuvertesScolaire || 0, - placesOuvertesApprentissage: - items[0].departement.placesOuvertesApprentissage || 0, - placesFermeesScolaire: items[0].departement.placesFermeesScolaire || 0, - placesFermeesApprentissage: - items[0].departement.placesFermeesApprentissage || 0, - differenceCapaciteScolaire: - items[0].departement.differenceCapaciteScolaire || 0, - differenceCapaciteApprentissage: - items[0].departement.differenceCapaciteApprentissage || 0, + ...formatTerritoire(items[0].departement), + code: `_${items[0].departement.codeDepartement}`, tauxTransformation: Math.round( (items[0].departement.transforme / @@ -97,6 +151,15 @@ const formatResult = ( ] || 0) * 10000 ) / 100, })) + .orderBy( + (item) => { + if (orderBy && orderBy.column) + return item[orderBy.column as keyof typeof item]; + return item.libelle; + }, + orderBy?.order ?? "asc" + ) + .keyBy("code") .value(), }; }; @@ -108,24 +171,25 @@ export const [getTransformationStats] = inject( rentreeScolaire?: string; codeNiveauDiplome?: string[]; filiere?: string[]; + orderBy?: { column: string; order: "asc" | "desc" }; }) => { const resultDraft = await deps .getTransformationStatsQuery({ ...activeFilters, status: "draft", }) - .then(formatResult); + .then((result) => formatResult(result, activeFilters.orderBy)); const resultSubmitted = await deps .getTransformationStatsQuery({ ...activeFilters, status: "submitted", }) - .then(formatResult); + .then((result) => formatResult(result, activeFilters.orderBy)); const resultAll = await deps .getTransformationStatsQuery({ ...activeFilters, }) - .then(formatResult); + .then((result) => formatResult(result, activeFilters.orderBy)); const filters = await deps.getFiltersQuery(activeFilters); diff --git a/server/src/modules/intentions/usecases/getStatsDemandes/dependencies.ts b/server/src/modules/intentions/usecases/getStatsDemandes/dependencies.ts index f7a22150e..ea71ae1d7 100644 --- a/server/src/modules/intentions/usecases/getStatsDemandes/dependencies.ts +++ b/server/src/modules/intentions/usecases/getStatsDemandes/dependencies.ts @@ -77,6 +77,11 @@ const findStatsDemandesInDB = async ({ "dataEtablissement.codeDepartement" ) .leftJoin("region", "region.codeRegion", "dataEtablissement.codeRegion") + .leftJoin( + "academie", + "academie.codeAcademie", + "dataEtablissement.codeAcademie" + ) .leftJoin("familleMetier", "familleMetier.cfdSpecialite", "demande.cfd") .leftJoin( "niveauDiplome", @@ -105,6 +110,8 @@ const findStatsDemandesInDB = async ({ "region.libelleRegion as libelleRegion", "departement.libelle as libelleDepartement", "departement.codeDepartement as codeDepartement", + "academie.libelle as libelleAcademie", + "academie.codeAcademie as codeAcademie", countDifferenceCapaciteScolaire(eb).as("differenceCapaciteScolaire"), countDifferenceCapaciteApprentissage(eb).as( "differenceCapaciteApprentissage" @@ -731,7 +738,15 @@ const findFiltersInDb = async ({ .execute(); const formationsFilters = await filtersBase - .select(["dataFormation.libelle as label", "dataFormation.cfd as value"]) + .select((eb) => [ + sql`CONCAT( + ${eb.ref("dataFormation.libelle")}, + ' (', + ${eb.ref("niveauDiplome.libelleNiveauDiplome")}, + ')' + )`.as("label"), + "dataFormation.cfd as value", + ]) .where("dataFormation.cfd", "is not", null) .where((eb) => { return eb.or([ diff --git a/shared/client/intentions/intentions.schema.ts b/shared/client/intentions/intentions.schema.ts index 8955538aa..dceb32e48 100644 --- a/shared/client/intentions/intentions.schema.ts +++ b/shared/client/intentions/intentions.schema.ts @@ -83,6 +83,24 @@ const DemandesItem = Type.Object({ compensationRentreeScolaire: Type.Optional(Type.Number()), idCompensation: Type.Optional(Type.String()), typeCompensation: Type.Optional(Type.String()), + dispositifId: Type.Optional(Type.String()), + rentreeScolaire: Type.Optional(Type.Number()), + motif: Type.Optional(Type.Array(Type.String())), + autreMotif: Type.Optional(Type.String()), + libelleColoration: Type.Optional(Type.String()), + coloration: Type.Optional(Type.Boolean()), + amiCma: Type.Optional(Type.Boolean()), + poursuitePedagogique: Type.Optional(Type.Boolean()), + commentaire: Type.Optional(Type.String()), + mixte: Type.Optional(Type.Boolean()), + capaciteScolaireActuelle: Type.Optional(Type.Number()), + capaciteScolaire: Type.Optional(Type.Number()), + capaciteScolaireColoree: Type.Optional(Type.Number()), + capaciteApprentissageActuelle: Type.Optional(Type.Number()), + capaciteApprentissage: Type.Optional(Type.Number()), + capaciteApprentissageColoree: Type.Optional(Type.Number()), + codeRegion: Type.String(), + codeAcademie: Type.Optional(Type.String()), }); const OptionSchema = Type.Object({ @@ -106,6 +124,7 @@ const StatsDemandesItem = Type.Object({ libelleEtablissement: Type.Optional(Type.String()), commune: Type.Optional(Type.String()), libelleDispositif: Type.Optional(Type.String()), + dispositifId: Type.Optional(Type.String()), libelleFCIL: Type.Optional(Type.String()), libelleColoration: Type.Optional(Type.String()), uai: Type.Optional(Type.String()), @@ -113,9 +132,13 @@ const StatsDemandesItem = Type.Object({ updatedAt: Type.String(), createurId: Type.String(), status: Type.String(), + rentreeScolaire: Type.Optional(Type.Number()), typeDemande: Type.Optional(Type.String()), motif: Type.Optional(Type.Array(Type.String())), autreMotif: Type.Optional(Type.String()), + poursuitePedagogique: Type.Optional(Type.Boolean()), + coloration: Type.Optional(Type.Boolean()), + amiCma: Type.Optional(Type.Boolean()), compensationCfd: Type.Optional(Type.String()), compensationDispositifId: Type.Optional(Type.String()), compensationUai: Type.Optional(Type.String()), @@ -124,14 +147,17 @@ const StatsDemandesItem = Type.Object({ typeCompensation: Type.Optional(Type.String()), codeRegion: Type.Optional(Type.String()), libelleRegion: Type.Optional(Type.String()), + codeAcademie: Type.Optional(Type.String()), codeDepartement: Type.Optional(Type.String()), libelleDepartement: Type.Optional(Type.String()), libelleFiliere: Type.Optional(Type.String()), capaciteScolaireActuelle: Type.Optional(Type.Number()), capaciteScolaire: Type.Optional(Type.Number()), differenceCapaciteScolaire: Type.Optional(Type.Number()), + capaciteScolaireColoree: Type.Optional(Type.Number()), capaciteApprentissageActuelle: Type.Optional(Type.Number()), capaciteApprentissage: Type.Optional(Type.Number()), + capaciteApprentissageColoree: Type.Optional(Type.Number()), differenceCapaciteApprentissage: Type.Optional(Type.Number()), insertion: Type.Optional(Type.Number()), poursuite: Type.Optional(Type.Number()), diff --git a/shared/client/pilotageTransfo/pilotageTransfo.schema.ts b/shared/client/pilotageTransfo/pilotageTransfo.schema.ts index 0a0331121..6dfac182a 100644 --- a/shared/client/pilotageTransfo/pilotageTransfo.schema.ts +++ b/shared/client/pilotageTransfo/pilotageTransfo.schema.ts @@ -12,10 +12,15 @@ const ScopedStatsTransfoSchema = Type.Object({ countDemande: Type.Number(), differenceCapaciteScolaire: Type.Number(), differenceCapaciteApprentissage: Type.Number(), + placesTransformees: Type.Number(), placesOuvertesScolaire: Type.Number(), - placesFermeesScolaire: Type.Number(), placesOuvertesApprentissage: Type.Number(), + placesOuvertes: Type.Number(), + placesFermeesScolaire: Type.Number(), placesFermeesApprentissage: Type.Number(), + placesFermees: Type.Number(), + ratioOuverture: Type.Number(), + ratioFermeture: Type.Number(), tauxTransformation: Type.Number(), }); @@ -50,9 +55,47 @@ const StatsFiltersSchema = Type.Object({ filiere: Type.Optional(Type.Array(Type.String())), }); +const FormationTransformationStatsSchema = Type.Object({ + libelleDiplome: Type.Optional(Type.String()), + libelleDispositif: Type.Optional(Type.String()), + tauxInsertion: Type.Number(), + tauxPoursuite: Type.Number(), + tauxPression: Type.Optional(Type.Number()), + dispositifId: Type.Optional(Type.String()), + cfd: Type.String(), + nbDemandes: Type.Number(), + nbEtablissements: Type.Number(), + differencePlaces: Type.Number(), + placesOuvertes: Type.Number(), + placesFermees: Type.Number(), + placesTransformees: Type.Number(), + continuum: Type.Optional( + Type.Object({ + cfd: Type.String(), + libelle: Type.Optional(Type.String()), + }) + ), +}); + +const FormationsTransformationStatsFiltersSchema = Type.Object({ + rentreeScolaire: Type.Optional(Type.String()), + codeNiveauDiplome: Type.Optional(Type.Array(Type.String())), + filiere: Type.Optional(Type.Array(Type.String())), +}); + export const pilotageTransformationSchemas = { getTransformationStats: { - querystring: Type.Intersect([StatsFiltersSchema]), + querystring: Type.Intersect([ + StatsFiltersSchema, + Type.Object({ + order: Type.Optional( + Type.Union([Type.Literal("asc"), Type.Literal("desc")]) + ), + orderBy: Type.Optional( + Type.KeyOf(Type.Omit(ScopedStatsTransfoSchema, [])) + ), + }), + ]), response: { 200: Type.Object({ submitted: StatsTransfoSchema, @@ -71,7 +114,7 @@ export const pilotageTransformationSchemas = { }, getFormationsTransformationStats: { querystring: Type.Intersect([ - StatsFiltersSchema, + FormationsTransformationStatsFiltersSchema, Type.Object({ status: Type.Optional( Type.Union([Type.Literal("draft"), Type.Literal("submitted")]) @@ -86,6 +129,14 @@ export const pilotageTransformationSchemas = { Type.Union([Type.Literal("eleve"), Type.Literal("faible")]) ), }), + Type.Object({ + order: Type.Optional( + Type.Union([Type.Literal("asc"), Type.Literal("desc")]) + ), + orderBy: Type.Optional( + Type.KeyOf(Type.Omit(FormationTransformationStatsSchema, [])) + ), + }), ]), response: { 200: Type.Object({ @@ -93,28 +144,7 @@ export const pilotageTransformationSchemas = { tauxInsertion: Type.Number(), tauxPoursuite: Type.Number(), }), - formations: Type.Array( - Type.Object({ - libelleDiplome: Type.Optional(Type.String()), - libelleDispositif: Type.Optional(Type.String()), - tauxInsertion: Type.Number(), - tauxPoursuite: Type.Number(), - tauxPression: Type.Optional(Type.Number()), - dispositifId: Type.Optional(Type.String()), - cfd: Type.String(), - nbDemandes: Type.Number(), - nbEtablissements: Type.Number(), - differencePlaces: Type.Number(), - placesOuvertes: Type.Number(), - placesFermees: Type.Number(), - continuum: Type.Optional( - Type.Object({ - cfd: Type.String(), - libelle: Type.Optional(Type.String()), - }) - ), - }) - ), + formations: Type.Array(FormationTransformationStatsSchema), }), }, }, diff --git a/ui/app/(wrapped)/console/etablissements/page.tsx b/ui/app/(wrapped)/console/etablissements/page.tsx index c3ba6de87..0e8016a67 100644 --- a/ui/app/(wrapped)/console/etablissements/page.tsx +++ b/ui/app/(wrapped)/console/etablissements/page.tsx @@ -19,11 +19,13 @@ import { usePlausible } from "next-plausible"; import qs from "qs"; import { Fragment, useContext, useEffect, useState } from "react"; import { unstable_batchedUpdates } from "react-dom"; +import { ApiType } from "shared"; import { api } from "@/api.client"; import { OrderIcon } from "@/components/OrderIcon"; import { TableFooter } from "@/components/TableFooter"; import { createParametrizedUrl } from "@/utils/createParametrizedUrl"; +import { ExportColumns } from "@/utils/downloadCsv"; import { Multiselect } from "../../../../components/Multiselect"; import { TooltipIcon } from "../../../../components/TooltipIcon"; @@ -55,7 +57,6 @@ const ETABLISSEMENTS_COLUMNS = { tauxInsertion6mois: "Tx d'emploi 6 mois régional", tauxPoursuiteEtudes: "Tx de poursuite d'études régional", valeurAjoutee: "Valeur ajoutée", - decrochage: "Décrochage", secteur: "Secteur", UAI: "UAI", libelleDispositif: "Dispositif", @@ -67,7 +68,9 @@ const ETABLISSEMENTS_COLUMNS = { libelleFiliere: "Secteur d’activité", "continuum.libelle": "Diplôme historique", "continuum.cfd": "Code diplôme historique", -} as const; +} satisfies ExportColumns< + ApiType["etablissements"][number] +>; type Query = Parameters[0]["query"]; diff --git a/ui/app/(wrapped)/console/formations/page.tsx b/ui/app/(wrapped)/console/formations/page.tsx index 41b2ed721..ec4f893ed 100644 --- a/ui/app/(wrapped)/console/formations/page.tsx +++ b/ui/app/(wrapped)/console/formations/page.tsx @@ -18,6 +18,7 @@ import { useRouter, useSearchParams } from "next/navigation"; import { usePlausible } from "next-plausible"; import qs from "qs"; import { Fragment, useContext, useEffect, useState } from "react"; +import { ApiType } from "shared"; import { api } from "@/api.client"; import { TauxPressionScale } from "@/app/(wrapped)/components/TauxPressionScale"; @@ -27,7 +28,7 @@ import { TooltipIcon } from "@/components/TooltipIcon"; import { Multiselect } from "../../../../components/Multiselect"; import { OrderIcon } from "../../../../components/OrderIcon"; import { createParametrizedUrl } from "../../../../utils/createParametrizedUrl"; -import { downloadCsv } from "../../../../utils/downloadCsv"; +import { downloadCsv, ExportColumns } from "../../../../utils/downloadCsv"; import { CodeRegionFilterContext } from "../../../layoutClient"; import { FormationLineContent, @@ -59,7 +60,9 @@ const FORMATIONS_COLUMNS = { libelleFiliere: "Secteur d’activité", "continuum.libelle": "Diplôme historique", "continuum.cfd": "Code diplôme historique", -} as const; +} satisfies ExportColumns< + ApiType["formations"][number] +>; const fetchFormations = async (query: Query) => api.getFormations({ query }).call(); diff --git a/ui/app/(wrapped)/intentions/pilotage/components/CadranSection.tsx b/ui/app/(wrapped)/intentions/pilotage/components/CadranSection.tsx index 00a42d956..81bd35ce1 100644 --- a/ui/app/(wrapped)/intentions/pilotage/components/CadranSection.tsx +++ b/ui/app/(wrapped)/intentions/pilotage/components/CadranSection.tsx @@ -1,4 +1,4 @@ -import { DownloadIcon } from "@chakra-ui/icons"; +import { DownloadIcon, ViewIcon } from "@chakra-ui/icons"; import { AspectRatio, Box, @@ -15,21 +15,31 @@ import { Select, Skeleton, Stack, + Table, + TableContainer, + Tbody, + Td, Text, + Th, + Thead, + Tr, } from "@chakra-ui/react"; import { useQuery } from "@tanstack/react-query"; import NextLink from "next/link"; +import { usePlausible } from "next-plausible"; import { useMemo, useState } from "react"; +import { ApiType } from "shared"; import { GraphWrapper } from "@/components/GraphWrapper"; import { InfoBlock } from "@/components/InfoBlock"; import { api } from "../../../../../api.client"; import { Cadran } from "../../../../../components/Cadran"; +import { OrderIcon } from "../../../../../components/OrderIcon"; import { createParametrizedUrl } from "../../../../../utils/createParametrizedUrl"; import { downloadCsv } from "../../../../../utils/downloadCsv"; import { useStateParams } from "../../../../../utils/useFilters"; -import { Filters, Scope } from "../types"; +import { Filters, OrderFormationsTransformationStats, Scope } from "../types"; export const CadranSection = ({ scope, @@ -42,24 +52,38 @@ export const CadranSection = ({ rentreeScolaire?: string; parentFilters: Partial; }) => { + const trackEvent = usePlausible(); + const [typeVue, setTypeVue] = useState<"cadran" | "tableau">("cadran"); + + const toggleTypeVue = () => { + if (typeVue === "cadran") setTypeVue("tableau"); + else setTypeVue("cadran"); + }; + const [filters, setFilters] = useStateParams({ prefix: "quadrant", defaultValues: { tauxPression: undefined, status: undefined, - type: "ouverture", + type: undefined, + order: undefined, } as { tauxPression?: "eleve" | "faible"; status?: "submitted" | "draft"; - type: "ouverture" | "fermeture"; + type?: "ouverture" | "fermeture"; + order?: Partial; }, }); + const order = filters.order; + const [currentCfd, setFormationId] = useState(); const mergedFilters = { - ...filters, ...parentFilters, + tauxPression: filters.tauxPression, + status: filters.status, + type: filters.type, codeRegion: scope?.type === "regions" ? scope.value : undefined, codeAcademie: scope?.type === "academies" ? scope.value : undefined, codeDepartement: scope?.type === "departements" ? scope.value : undefined, @@ -68,9 +92,13 @@ export const CadranSection = ({ const { data: { formations, stats } = {} } = useQuery({ keepPreviousData: true, staleTime: 10000000, - queryKey: ["getformationsTransformationStats", mergedFilters], - queryFn: api.getFormationsTransformationStats({ query: mergedFilters }) - .call, + queryKey: ["getformationsTransformationStats", mergedFilters, order], + queryFn: api.getFormationsTransformationStats({ + query: { + ...mergedFilters, + ...order, + }, + }).call, }); const formation = useMemo( @@ -78,6 +106,58 @@ export const CadranSection = ({ [currentCfd, formations] ); + const getTdColor = ( + formation: ApiType< + typeof api.getFormationsTransformationStats + >["formations"][0] + ) => { + if (formation.cfd === currentCfd) return "white !important"; + return ""; + }; + + const getTrBgColor = ( + formation: ApiType< + typeof api.getFormationsTransformationStats + >["formations"][0] + ) => { + if (formation.cfd === currentCfd) return "blue.main !important"; + if (stats?.tauxInsertion && stats?.tauxPoursuite) { + if ( + formation.tauxInsertion >= stats?.tauxInsertion && + formation.tauxPoursuite >= stats?.tauxPoursuite + ) + return "#C8F6D6"; + if ( + formation.tauxInsertion < stats?.tauxInsertion && + formation.tauxPoursuite < stats?.tauxPoursuite + ) + return "#ffe2e1"; + } + return "inherit"; + }; + + const handleOrder = ( + column: OrderFormationsTransformationStats["orderBy"] + ) => { + trackEvent("tableau-cadran-intentions:ordre", { + props: { colonne: column }, + }); + if (order?.orderBy !== column) { + setFilters({ + ...filters, + order: { order: "desc", orderBy: column }, + }); + return; + } + setFilters({ + ...filters, + order: { + order: order?.order === "asc" ? "desc" : "asc", + orderBy: column, + }, + }); + }; + return ( <> @@ -89,7 +169,12 @@ export const CadranSection = ({ RÉPARTITION DES OFFRES DE FORMATIONS TRANSFORMÉES +