diff --git a/server/src/modules/data/index.ts b/server/src/modules/data/index.ts index 4a5eb3510..cdfd75fe9 100644 --- a/server/src/modules/data/index.ts +++ b/server/src/modules/data/index.ts @@ -5,6 +5,7 @@ import { cadranRoutes } from "./routes/panorama.routes"; import { pilotageReformeRoutes } from "./routes/pilotageReforme.routes"; import { pilotageTransformationRoutes } from "./routes/pilotageTransformation.routes"; import { regionsRoutes } from "./routes/regions.routes"; +import { restitutionIntentionsRoutes } from "./routes/restitutionIntentions.routes"; export const registerFormationModule = ({ server }: { server: Server }) => { formationsRoutes({ server }); @@ -13,4 +14,5 @@ export const registerFormationModule = ({ server }: { server: Server }) => { regionsRoutes({ server }); pilotageReformeRoutes({ server }); pilotageTransformationRoutes({ server }); + restitutionIntentionsRoutes({ server }); }; diff --git a/server/src/modules/data/queries/getEtablissement/getEtablissement.query.ts b/server/src/modules/data/queries/getEtablissement/getEtablissement.query.ts index 67472f7e6..380ec4d9f 100644 --- a/server/src/modules/data/queries/getEtablissement/getEtablissement.query.ts +++ b/server/src/modules/data/queries/getEtablissement/getEtablissement.query.ts @@ -5,6 +5,7 @@ import { kdb } from "../../../../db/db"; import { cleanNull } from "../../../../utils/noNull"; import { hasContinuum } from "../utils/hasContinuum"; import { notHistorique } from "../utils/notHistorique"; +import { withPositionCadran } from "../utils/positionCadran"; import { withInsertionReg } from "../utils/tauxInsertion6mois"; import { withPoursuiteReg } from "../utils/tauxPoursuite"; import { selectTauxPression } from "../utils/tauxPression"; @@ -106,6 +107,13 @@ export const getEtablissement = async ({ dispositifIdRef: "formationEtablissement.dispositifId", codeRegionRef: "etablissement.codeRegion", }).as("tauxPoursuiteEtudes"), + withPositionCadran({ + eb, + millesimeSortie, + cfdRef: "formationEtablissement.cfd", + dispositifIdRef: "formationEtablissement.dispositifId", + codeRegionRef: "etablissement.codeRegion", + }).as("positionCadran"), ]) .where(notHistorique) .whereRef("formationEtablissement.UAI", "=", "etablissement.UAI") diff --git a/server/src/modules/data/queries/getFormationsTransformationRegionStats/getFormationsTransformationRegionStats.query.ts b/server/src/modules/data/queries/getFormationsTransformationRegionStats/getFormationsTransformationRegionStats.query.ts new file mode 100644 index 000000000..0464771ee --- /dev/null +++ b/server/src/modules/data/queries/getFormationsTransformationRegionStats/getFormationsTransformationRegionStats.query.ts @@ -0,0 +1,62 @@ +import { kdb } from "../../../../db/db"; +import { notHistoriqueIndicateurRegionSortie } from "../../queries/utils/notHistorique"; +import { selectTauxInsertion6moisAgg } from "../../queries/utils/tauxInsertion6mois"; +import { selectTauxPoursuiteAgg } from "../../queries/utils/tauxPoursuite"; + +export const getFormationsTransformationRegionStats = async ({ + codeRegion, + codeAcademie, + codeDepartement, + codeNiveauDiplome, + millesimeSortie = "2020_2021", +}: { + codeRegion?: string; + codeAcademie?: string; + codeDepartement?: string; + millesimeSortie?: string; + codeNiveauDiplome?: string[]; +}) => { + const statsSortie = await kdb + .selectFrom("indicateurRegionSortie") + .innerJoin( + "formation", + "formation.codeFormationDiplome", + "indicateurRegionSortie.cfd" + ) + .where((w) => { + if (!codeRegion) return w.val(true); + return w("indicateurRegionSortie.codeRegion", "=", codeRegion); + }) + .$call((q) => { + if (!codeDepartement && !codeAcademie) { + return q; + } + return q + .innerJoin( + "departement", + "departement.codeRegion", + "indicateurRegionSortie.codeRegion" + ) + .where((w) => { + if (!codeAcademie) return w.val(true); + return w("departement.codeAcademie", "=", codeAcademie); + }) + .where((w) => { + if (!codeDepartement) return w.val(true); + return w("departement.codeDepartement", "=", codeDepartement); + }); + }) + .$call((q) => { + if (!codeNiveauDiplome?.length) return q; + return q.where("formation.codeNiveauDiplome", "in", codeNiveauDiplome); + }) + .where("indicateurRegionSortie.millesimeSortie", "=", millesimeSortie) + .where(notHistoriqueIndicateurRegionSortie) + .select([ + selectTauxInsertion6moisAgg("indicateurRegionSortie").as("tauxInsertion"), + selectTauxPoursuiteAgg("indicateurRegionSortie").as("tauxPoursuite"), + ]) + .executeTakeFirstOrThrow(); + + return statsSortie; +}; diff --git a/server/src/modules/data/queries/utils/positionCadran.ts b/server/src/modules/data/queries/utils/positionCadran.ts new file mode 100644 index 000000000..bd5ee3c6e --- /dev/null +++ b/server/src/modules/data/queries/utils/positionCadran.ts @@ -0,0 +1,134 @@ +import { ExpressionBuilder, sql } from "kysely"; + +import { DB } from "../../../../db/schema"; +import { notHistoriqueIndicateurRegionSortie } from "./notHistorique"; +import { + selectTauxInsertion6mois, + selectTauxInsertion6moisAgg, + withInsertionReg, +} from "./tauxInsertion6mois"; +import { + selectTauxPoursuite, + selectTauxPoursuiteAgg, + withPoursuiteReg, +} from "./tauxPoursuite"; + +export const getPositionCadran = ( + indicateurRegionSortieAlias: string, + indicateurRegionSortieRegAlias: string +) => { + const tauxPoursuite = sql`${selectTauxPoursuite( + indicateurRegionSortieAlias + )}`; + const tauxInsertion = sql`${selectTauxInsertion6mois( + indicateurRegionSortieAlias + )}`; + const tauxPoursuiteReg = sql`${selectTauxPoursuiteAgg( + indicateurRegionSortieRegAlias + )}`; + const tauxInsertionReg = sql`${selectTauxInsertion6moisAgg( + indicateurRegionSortieRegAlias + )}`; + + return sql` + CASE + WHEN (${tauxInsertion} >= ${tauxInsertionReg} AND ${tauxPoursuite} > ${tauxPoursuiteReg}) THEN 'Q1' + WHEN (${tauxInsertion} >= ${tauxInsertionReg} AND ${tauxPoursuite} < ${tauxPoursuiteReg}) THEN 'Q2' + WHEN (${tauxInsertion} < ${tauxInsertionReg} AND ${tauxPoursuite} >= ${tauxPoursuiteReg}) THEN 'Q3' + WHEN (${tauxInsertion} < ${tauxInsertionReg} AND ${tauxPoursuite} < ${tauxPoursuiteReg}) THEN 'Q4' + ELSE 'Hors cadran' + END + `; +}; + +type EbRef> = Parameters[0]; + +export function withPositionCadran>({ + eb, + cfdRef, + dispositifIdRef, + codeRegionRef, + millesimeSortie, + codeNiveauDiplomeRef, +}: { + eb: EB; + cfdRef: EbRef; + dispositifIdRef: EbRef; + codeRegionRef: EbRef; + millesimeSortie: string; + codeNiveauDiplomeRef?: EbRef; +}) { + const tauxInsertionReg = withInsertionReg({ + eb, + cfdRef, + dispositifIdRef, + codeRegionRef, + millesimeSortie, + }); + + const tauxPoursuiteReg = withPoursuiteReg({ + eb, + cfdRef, + dispositifIdRef, + codeRegionRef, + millesimeSortie, + }); + + return eb + .selectFrom("indicateurRegionSortie") + .whereRef( + "indicateurRegionSortie.codeRegion", + "=", + sql`ANY(array_agg(${eb.ref(codeRegionRef)}))` + ) + .where("indicateurRegionSortie.millesimeSortie", "=", millesimeSortie) + .where(notHistoriqueIndicateurRegionSortie) + .innerJoin( + "formation", + "formation.codeFormationDiplome", + "indicateurRegionSortie.cfd" + ) + .$call((eb) => { + if (codeNiveauDiplomeRef) + return eb.whereRef( + "formation.codeNiveauDiplome", + "=", + codeNiveauDiplomeRef + ); + return eb; + }) + .select([ + sql` + CASE + WHEN (${tauxInsertionReg} >= ${selectTauxInsertion6moisAgg( + "indicateurRegionSortie" + )} + AND ${tauxPoursuiteReg} > ${selectTauxPoursuiteAgg( + "indicateurRegionSortie" + )}) + THEN 'Q1' + WHEN (${tauxInsertionReg} >= ${selectTauxInsertion6moisAgg( + "indicateurRegionSortie" + )} + AND ${tauxPoursuiteReg} < ${selectTauxPoursuiteAgg( + "indicateurRegionSortie" + )}) + THEN 'Q2' + WHEN (${tauxInsertionReg} < ${selectTauxInsertion6moisAgg( + "indicateurRegionSortie" + )} + AND ${tauxPoursuiteReg} >= ${selectTauxPoursuiteAgg( + "indicateurRegionSortie" + )}) + THEN 'Q3' + WHEN (${tauxInsertionReg} < ${selectTauxInsertion6moisAgg( + "indicateurRegionSortie" + )} + AND ${tauxPoursuiteReg} < ${selectTauxPoursuiteAgg( + "indicateurRegionSortie" + )}) + THEN 'Q4' + ELSE 'Hors cadran' + END`.as("positionCadran"), + ]); +} diff --git a/server/src/modules/data/queries/utils/tauxInsertion6mois.ts b/server/src/modules/data/queries/utils/tauxInsertion6mois.ts index 98cff24f9..42f33cc54 100644 --- a/server/src/modules/data/queries/utils/tauxInsertion6mois.ts +++ b/server/src/modules/data/queries/utils/tauxInsertion6mois.ts @@ -8,18 +8,18 @@ export const selectDenominateurInsertion6moisAgg = ( indicateurSortieAlias: string ) => sql` SUM( - case when - ${sql.table(indicateurSortieAlias)}."nbInsertion6mois" is not null - then ${sql.table(indicateurSortieAlias)}."nbSortants" + case when + ${sql.table(indicateurSortieAlias)}."nbInsertion6mois" is not null + then ${sql.table(indicateurSortieAlias)}."nbSortants" end )`; export const selectTauxInsertion6moisAgg = ( indicateurSortieAlias: string ) => sql` - case when + case when ${selectDenominateurInsertion6moisAgg(indicateurSortieAlias)} >= ${seuil} - then (100 * SUM(${sql.table(indicateurSortieAlias)}."nbInsertion6mois") + then (100 * SUM(${sql.table(indicateurSortieAlias)}."nbInsertion6mois") / ${selectDenominateurInsertion6moisAgg(indicateurSortieAlias)}) end `; @@ -27,17 +27,17 @@ export const selectTauxInsertion6moisAgg = ( export const selectDenominateurInsertion6mois = ( indicateurSortieAlias: string ) => sql` - case when - ${sql.table(indicateurSortieAlias)}."nbInsertion6mois" is not null - then ${sql.table(indicateurSortieAlias)}."nbSortants" + case when + ${sql.table(indicateurSortieAlias)}."nbInsertion6mois" is not null + then ${sql.table(indicateurSortieAlias)}."nbSortants" end`; export const selectTauxInsertion6mois = ( indicateurSortieAlias: string ) => sql` - case when + case when ${selectDenominateurInsertion6mois(indicateurSortieAlias)} >= ${seuil} - then (100 * ${sql.table(indicateurSortieAlias)}."nbInsertion6mois" + then (100 * ${sql.table(indicateurSortieAlias)}."nbInsertion6mois" / ${selectDenominateurInsertion6mois(indicateurSortieAlias)}) end `; diff --git a/server/src/modules/data/routes/restitutionIntentions.routes.ts b/server/src/modules/data/routes/restitutionIntentions.routes.ts new file mode 100644 index 000000000..5034431df --- /dev/null +++ b/server/src/modules/data/routes/restitutionIntentions.routes.ts @@ -0,0 +1,46 @@ +import Boom from "@hapi/boom"; +import { ROUTES_CONFIG } from "shared"; + +import { Server } from "../../../server"; +import { hasPermissionHandler } from "../../core"; +import { getCountRestitutionIntentionsStats } from "../usecases/getCountRestitutionIntentionsStats/getCountRestitutionIntentionsStats"; +import { getRestitutionIntentionsStats } from "../usecases/getRestitutionIntentionsStats/getRestitutionIntentionsStats.usecase"; + +export const restitutionIntentionsRoutes = ({ server }: { server: Server }) => { + server.get( + "/intentions/stats", + { + schema: ROUTES_CONFIG.getRestitutionIntentionsStats, + preHandler: hasPermissionHandler("intentions/lecture"), + }, + async (request, response) => { + const { order, orderBy, ...rest } = request.query; + if (!request.user) throw Boom.forbidden(); + + const result = await getRestitutionIntentionsStats({ + ...rest, + orderBy: order && orderBy ? { order, column: orderBy } : undefined, + user: request.user, + }); + response.status(200).send(result); + } + ); + + server.get( + "/intentions/stats/count", + { + schema: ROUTES_CONFIG.countRestitutionIntentionsStats, + preHandler: hasPermissionHandler("intentions/lecture"), + }, + async (request, response) => { + const { ...filters } = request.query; + if (!request.user) throw Boom.forbidden(); + + const result = await getCountRestitutionIntentionsStats({ + ...filters, + user: request.user, + }); + response.status(200).send(result); + } + ); +}; diff --git a/server/src/modules/intentions/usecases/getCountStatsDemandes/dependencies.ts b/server/src/modules/data/usecases/getCountRestitutionIntentionsStats/dependencies.ts similarity index 96% rename from server/src/modules/intentions/usecases/getCountStatsDemandes/dependencies.ts rename to server/src/modules/data/usecases/getCountRestitutionIntentionsStats/dependencies.ts index 97d1fd860..4300e8cd0 100644 --- a/server/src/modules/intentions/usecases/getCountStatsDemandes/dependencies.ts +++ b/server/src/modules/data/usecases/getCountRestitutionIntentionsStats/dependencies.ts @@ -10,10 +10,10 @@ import { countOuvertures, countOuverturesApprentissage, countOuverturesSco, -} from "../../utils/countCapacite"; -import { isStatsDemandeVisible } from "../../utils/isStatsDemandesVisible"; +} from "../../../utils/countCapacite"; +import { isIntentionVisible } from "../../../utils/isIntentionVisible"; -const countStatsDemandesInDB = async ({ +const countRestitutionIntentionsStatsInDB = async ({ status, codeRegion, rentreeScolaire, @@ -249,12 +249,12 @@ const countStatsDemandesInDB = async ({ if (secteur) return eb.where("dataEtablissement.secteur", "=", secteur); return eb; }) - .where(isStatsDemandeVisible({ user })) + .where(isIntentionVisible({ user })) .executeTakeFirstOrThrow(); return countDemandes; }; export const dependencies = { - countStatsDemandesInDB, + countRestitutionIntentionsStatsInDB, }; diff --git a/server/src/modules/intentions/usecases/getCountStatsDemandes/getCountStatsDemandes.usecase.ts b/server/src/modules/data/usecases/getCountRestitutionIntentionsStats/getCountRestitutionIntentionsStats.ts similarity index 67% rename from server/src/modules/intentions/usecases/getCountStatsDemandes/getCountStatsDemandes.usecase.ts rename to server/src/modules/data/usecases/getCountRestitutionIntentionsStats/getCountRestitutionIntentionsStats.ts index 91cc8067e..626a1203c 100644 --- a/server/src/modules/intentions/usecases/getCountStatsDemandes/getCountStatsDemandes.usecase.ts +++ b/server/src/modules/data/usecases/getCountRestitutionIntentionsStats/getCountRestitutionIntentionsStats.ts @@ -1,8 +1,10 @@ import { RequestUser } from "../../../core/model/User"; import { dependencies } from "./dependencies"; -const getCountStatsDemandesFactory = - ({ countStatsDemandesInDB = dependencies.countStatsDemandesInDB }) => +const getCountRestitutionIntentionsStatsFactory = + ({ + countRestitutionIntentionsStatsInDB = dependencies.countRestitutionIntentionsStatsInDB, + }) => async (activeFilters: { status?: "draft" | "submitted"; codeRegion?: string[]; @@ -24,9 +26,11 @@ const getCountStatsDemandesFactory = compensation?: string; user: Pick; }) => { - const countStatsDemandesPromise = countStatsDemandesInDB(activeFilters); + const countStatsDemandesPromise = + countRestitutionIntentionsStatsInDB(activeFilters); return await countStatsDemandesPromise; }; -export const getCountStatsDemandes = getCountStatsDemandesFactory({}); +export const getCountRestitutionIntentionsStats = + getCountRestitutionIntentionsStatsFactory({}); diff --git a/server/src/modules/data/usecases/getDataForPanorama/dependencies.ts b/server/src/modules/data/usecases/getDataForPanorama/dependencies.ts index 7a291714c..042a30351 100644 --- a/server/src/modules/data/usecases/getDataForPanorama/dependencies.ts +++ b/server/src/modules/data/usecases/getDataForPanorama/dependencies.ts @@ -4,6 +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 { withPositionCadran } from "../../queries/utils/positionCadran"; import { withTauxDevenirFavorableReg } from "../../queries/utils/tauxDevenirFavorable"; import { withInsertionReg } from "../../queries/utils/tauxInsertion6mois"; import { withPoursuiteReg } from "../../queries/utils/tauxPoursuite"; @@ -129,6 +130,14 @@ export const queryFormations = async ({ dispositifIdRef: "formationEtablissement.dispositifId", codeRegionRef: "etablissement.codeRegion", }).as("tauxDevenirFavorable"), + (eb) => + withPositionCadran({ + eb, + millesimeSortie, + cfdRef: "formationEtablissement.cfd", + dispositifIdRef: "formationEtablissement.dispositifId", + codeRegionRef: "etablissement.codeRegion", + }).as("positionCadran"), ]) .$narrowType<{ tauxInsertion6mois: number; diff --git a/server/src/modules/data/usecases/getEtablissements/dependencies.ts b/server/src/modules/data/usecases/getEtablissements/dependencies.ts index 66976f7a8..8c73f7791 100644 --- a/server/src/modules/data/usecases/getEtablissements/dependencies.ts +++ b/server/src/modules/data/usecases/getEtablissements/dependencies.ts @@ -7,6 +7,8 @@ import { capaciteAnnee } from "../../queries/utils/capaciteAnnee"; import { effectifAnnee } from "../../queries/utils/effectifAnnee"; import { hasContinuum } from "../../queries/utils/hasContinuum"; import { notHistorique } from "../../queries/utils/notHistorique"; +import { withPositionCadran } from "../../queries/utils/positionCadran"; +import { withTauxDevenirFavorableReg } from "../../queries/utils/tauxDevenirFavorable"; import { withInsertionReg } from "../../queries/utils/tauxInsertion6mois"; import { withPoursuiteReg } from "../../queries/utils/tauxPoursuite"; import { selectTauxPression } from "../../queries/utils/tauxPression"; @@ -160,6 +162,22 @@ const findEtablissementsInDb = async ({ dispositifIdRef: "formationEtablissement.dispositifId", codeRegionRef: "etablissement.codeRegion", }).as("tauxInsertion6mois"), + (eb) => + withTauxDevenirFavorableReg({ + eb, + millesimeSortie, + cfdRef: "formationEtablissement.cfd", + dispositifIdRef: "formationEtablissement.dispositifId", + codeRegionRef: "etablissement.codeRegion", + }).as("tauxDevenirFavorable"), + (eb) => + withPositionCadran({ + eb, + millesimeSortie, + cfdRef: "formationEtablissement.cfd", + dispositifIdRef: "formationEtablissement.dispositifId", + codeRegionRef: "etablissement.codeRegion", + }).as("positionCadran"), ]) .$call((q) => { if (!codeRegion) return q; diff --git a/server/src/modules/data/usecases/getFormations/dependencies.ts b/server/src/modules/data/usecases/getFormations/dependencies.ts index 758fa3145..032fa9486 100644 --- a/server/src/modules/data/usecases/getFormations/dependencies.ts +++ b/server/src/modules/data/usecases/getFormations/dependencies.ts @@ -6,6 +6,8 @@ import { cleanNull } from "../../../../utils/noNull"; import { capaciteAnnee } from "../../queries/utils/capaciteAnnee"; import { effectifAnnee } from "../../queries/utils/effectifAnnee"; import { hasContinuum } from "../../queries/utils/hasContinuum"; +import { withPositionCadran } from "../../queries/utils/positionCadran"; +import { withTauxDevenirFavorableReg } from "../../queries/utils/tauxDevenirFavorable"; import { withInsertionReg } from "../../queries/utils/tauxInsertion6mois"; import { withPoursuiteReg } from "../../queries/utils/tauxPoursuite"; import { selectTauxPressionAgg } from "../../queries/utils/tauxPression"; @@ -30,6 +32,7 @@ const findFormationsInDb = async ({ CPCSecteur, CPCSousSecteur, libelleFiliere, + positionCadran, }: { offset?: number; limit?: number; @@ -49,6 +52,7 @@ const findFormationsInDb = async ({ CPCSecteur?: string[]; CPCSousSecteur?: string[]; libelleFiliere?: string[]; + positionCadran?: string; } = {}) => { const query = kdb .selectFrom("formation") @@ -159,6 +163,21 @@ const findFormationsInDb = async ({ dispositifIdRef: "formationEtablissement.dispositifId", codeRegionRef: "etablissement.codeRegion", }).as("tauxInsertion6mois"), + withTauxDevenirFavorableReg({ + eb, + millesimeSortie, + cfdRef: "formationEtablissement.cfd", + dispositifIdRef: "formationEtablissement.dispositifId", + codeRegionRef: "etablissement.codeRegion", + }).as("tauxDevenirFavorable"), + withPositionCadran({ + eb, + millesimeSortie, + cfdRef: "formationEtablissement.cfd", + dispositifIdRef: "formationEtablissement.dispositifId", + codeRegionRef: "etablissement.codeRegion", + codeNiveauDiplomeRef: "formation.codeNiveauDiplome", + }).as("positionCadran"), ]) .where( "codeFormationDiplome", @@ -170,24 +189,24 @@ const findFormationsInDb = async ({ cmpr("indicateurEntree.rentreeScolaire", "is not", null), withEmptyFormations ? not( - exists( - selectFrom("formationEtablissement as fe") - .select("cfd") - .distinct() - .innerJoin( - "indicateurEntree", - "id", - "formationEtablissementId" - ) - .where("rentreeScolaire", "in", rentreeScolaire) - .whereRef( - "fe.dispositifId", - "=", - "formationEtablissement.dispositifId" - ) - .whereRef("fe.cfd", "=", "formationEtablissement.cfd") - ) + exists( + selectFrom("formationEtablissement as fe") + .select("cfd") + .distinct() + .innerJoin( + "indicateurEntree", + "id", + "formationEtablissementId" + ) + .where("rentreeScolaire", "in", rentreeScolaire) + .whereRef( + "fe.dispositifId", + "=", + "formationEtablissement.dispositifId" + ) + .whereRef("fe.cfd", "=", "formationEtablissement.cfd") ) + ) : sql`false`, ]) ) @@ -201,6 +220,21 @@ const findFormationsInDb = async ({ "familleMetier.libelleOfficielFamille", "niveauDiplome.libelleNiveauDiplome", ]) + .having((h) => { + if (!positionCadran) return h.val(true); + return h( + (eb) => + withPositionCadran({ + eb, + millesimeSortie, + cfdRef: "formationEtablissement.cfd", + dispositifIdRef: "formationEtablissement.dispositifId", + codeRegionRef: "etablissement.codeRegion", + }), + "=", + positionCadran + ); + }) .$call((q) => { if (!codeRegion) return q; return q.where("etablissement.codeRegion", "in", codeRegion); diff --git a/server/src/modules/data/usecases/getFormations/getFormations.usecase.ts b/server/src/modules/data/usecases/getFormations/getFormations.usecase.ts index 9ebe889b7..96e113b30 100644 --- a/server/src/modules/data/usecases/getFormations/getFormations.usecase.ts +++ b/server/src/modules/data/usecases/getFormations/getFormations.usecase.ts @@ -25,6 +25,7 @@ const getFormationsFactory = libelleFiliere?: string[]; orderBy?: { order: "asc" | "desc"; column: string }; withEmptyFormations?: boolean; + positionCadran?: string; }) => { const [{ formations, count }, filters] = await Promise.all([ deps.findFormationsInDb(activeFilters), diff --git a/server/src/modules/data/usecases/getFormationsTransformationStats/getFormationsStatsQuery.dep.ts b/server/src/modules/data/usecases/getFormationsTransformationStats/getFormationsStatsQuery.dep.ts index 0f0e7fba5..ee2f7f63f 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 { withPositionCadran } from "../../queries/utils/positionCadran"; import { withTauxDevenirFavorableReg } from "../../queries/utils/tauxDevenirFavorable"; import { withInsertionReg } from "../../queries/utils/tauxInsertion6mois"; import { withPoursuiteReg } from "../../queries/utils/tauxPoursuite"; @@ -137,6 +138,14 @@ export const getFormationsTransformationStatsQuery = ({ dispositifIdRef: "demande.dispositifId", codeRegionRef: "dataEtablissement.codeRegion", }).as("tauxDevenirFavorable"), + withPositionCadran({ + eb, + millesimeSortie: "2020_2021", + cfdRef: "demande.cfd", + dispositifIdRef: "demande.dispositifId", + codeRegionRef: "dataEtablissement.codeRegion", + codeNiveauDiplomeRef: codeNiveauDiplome ? "dataFormation.codeNiveauDiplome" : undefined, + }).as("positionCadran"), selectNbDemandes(eb).as("nbDemandes"), selectNbEtablissements(eb).as("nbEtablissements"), sql`ABS(${eb.fn.sum(selectDifferencePlaces(eb, type))})`.as( diff --git a/server/src/modules/data/usecases/getFormationsTransformationStats/getRegionStats.dep.ts b/server/src/modules/data/usecases/getFormationsTransformationStats/getRegionStats.dep.ts index 9547b05a0..7b70a4e21 100644 --- a/server/src/modules/data/usecases/getFormationsTransformationStats/getRegionStats.dep.ts +++ b/server/src/modules/data/usecases/getFormationsTransformationStats/getRegionStats.dep.ts @@ -23,7 +23,6 @@ export const getRegionStats = async ({ "formation.codeFormationDiplome", "indicateurRegionSortie.cfd" ) - .where((w) => { if (!codeRegion) return w.val(true); return w("indicateurRegionSortie.codeRegion", "=", codeRegion); diff --git a/server/src/modules/intentions/usecases/getStatsDemandes/dependencies.ts b/server/src/modules/data/usecases/getRestitutionIntentionsStats/dependencies.ts similarity index 92% rename from server/src/modules/intentions/usecases/getStatsDemandes/dependencies.ts rename to server/src/modules/data/usecases/getRestitutionIntentionsStats/dependencies.ts index ea71ae1d7..16b499fdd 100644 --- a/server/src/modules/intentions/usecases/getStatsDemandes/dependencies.ts +++ b/server/src/modules/data/usecases/getRestitutionIntentionsStats/dependencies.ts @@ -7,19 +7,27 @@ import { cleanNull } from "../../../../utils/noNull"; import { RequestUser } from "../../../core/model/User"; import { nbEtablissementFormationRegion } from "../../../data/queries/utils/nbEtablissementFormationRegion"; import { selectTauxDevenirFavorable } from "../../../data/queries/utils/tauxDevenirFavorable"; -import { selectTauxInsertion6mois } from "../../../data/queries/utils/tauxInsertion6mois"; -import { selectTauxPoursuite } from "../../../data/queries/utils/tauxPoursuite"; +import { + selectTauxInsertion6mois, + selectTauxInsertion6moisAgg, +} from "../../../data/queries/utils/tauxInsertion6mois"; +import { + selectTauxPoursuite, + selectTauxPoursuiteAgg, +} from "../../../data/queries/utils/tauxPoursuite"; import { selectTauxPressionParFormationEtParRegionDemande } from "../../../data/queries/utils/tauxPression"; import { countDifferenceCapaciteApprentissage, countDifferenceCapaciteScolaire, -} from "../../utils/countCapacite"; +} from "../../../utils/countCapacite"; import { + isIntentionVisible, isRegionVisible, - isStatsDemandeVisible, -} from "../../utils/isStatsDemandesVisible"; +} from "../../../utils/isIntentionVisible"; +import { notHistoriqueIndicateurRegionSortie } from "../../queries/utils/notHistorique"; +import { getPositionCadran } from "../../queries/utils/positionCadran"; -const findStatsDemandesInDB = async ({ +const findRestitutionIntentionsStatsInDB = async ({ status, codeRegion, rentreeScolaire, @@ -39,6 +47,7 @@ const findStatsDemandesInDB = async ({ uai, compensation, user, + millesimeSortie = "2020_2021", offset = 0, limit = 20, orderBy = { order: "desc", column: "createdAt" }, @@ -62,6 +71,7 @@ const findStatsDemandesInDB = async ({ uai?: string[]; compensation?: string; user: Pick; + millesimeSortie: string; offset?: number; limit?: number; orderBy?: { order: "asc" | "desc"; column: string }; @@ -97,7 +107,7 @@ const findStatsDemandesInDB = async ({ "=", "demande.dispositifId" ) - .on("indicateurRegionSortie.millesimeSortie", "=", "2020_2021") + .on("indicateurRegionSortie.millesimeSortie", "=", millesimeSortie) ) .selectAll("demande") .select((eb) => [ @@ -142,6 +152,31 @@ const findStatsDemandesInDB = async ({ nbEtablissementFormationRegion({ eb, rentreeScolaire: "2022" }).as( "nbEtablissement" ), + jsonObjectFrom( + eb + .selectFrom("indicateurRegionSortie as subIRS") + .innerJoin( + "formation", + "formation.codeFormationDiplome", + "subIRS.cfd" + ) + .whereRef("subIRS.codeRegion", "=", "demande.codeRegion") + .whereRef("subIRS.dispositifId", "=", "demande.dispositifId") + .whereRef( + "formation.codeNiveauDiplome", + "=", + "dataFormation.codeNiveauDiplome" + ) + .where("subIRS.millesimeSortie", "=", millesimeSortie) + .where(notHistoriqueIndicateurRegionSortie) + .select([ + selectTauxInsertion6moisAgg("subIRS").as("tauxInsertion"), + selectTauxPoursuiteAgg("subIRS").as("tauxPoursuite"), + getPositionCadran("indicateurRegionSortie", "subIRS").as( + "positionCadran" + ), + ]) + ).as("statsSortieMoyennes"), ]) .$call((eb) => { if (status && status != undefined) @@ -263,7 +298,7 @@ const findStatsDemandesInDB = async ({ sql`${sql.raw(orderBy.order)} NULLS LAST` ); }) - .where(isStatsDemandeVisible({ user })) + .where(isIntentionVisible({ user })) .offset(offset) .limit(limit) .execute(); @@ -276,6 +311,12 @@ const findStatsDemandesInDB = async ({ updatedAt: demande.updatedAt?.toISOString(), idCompensation: demande.demandeCompensee?.id, typeCompensation: demande.demandeCompensee?.typeDemande ?? undefined, + tauxInsertionMoyen: + demande.statsSortieMoyennes?.tauxInsertion ?? undefined, + tauxPoursuiteMoyen: + demande.statsSortieMoyennes?.tauxPoursuite ?? undefined, + positionCadran: + demande.statsSortieMoyennes?.positionCadran ?? "Hors cadran", }) ), count: parseInt(demandes[0]?.count) || 0, @@ -924,6 +965,6 @@ const findFiltersInDb = async ({ }; export const dependencies = { - findStatsDemandesInDB, + findRestitutionIntentionsStatsInDB, findFiltersInDb, }; diff --git a/server/src/modules/intentions/usecases/getStatsDemandes/getStatsDemandes.usecase.ts b/server/src/modules/data/usecases/getRestitutionIntentionsStats/getRestitutionIntentionsStats.usecase.ts similarity index 75% rename from server/src/modules/intentions/usecases/getStatsDemandes/getStatsDemandes.usecase.ts rename to server/src/modules/data/usecases/getRestitutionIntentionsStats/getRestitutionIntentionsStats.usecase.ts index b4e593754..5e13b55ee 100644 --- a/server/src/modules/intentions/usecases/getStatsDemandes/getStatsDemandes.usecase.ts +++ b/server/src/modules/data/usecases/getRestitutionIntentionsStats/getRestitutionIntentionsStats.usecase.ts @@ -1,9 +1,9 @@ import { RequestUser } from "../../../core/model/User"; import { dependencies } from "./dependencies"; -const getStatsDemandesFactory = +const getRestitutionIntentionsStatsFactory = ({ - findStatsDemandesInDB = dependencies.findStatsDemandesInDB, + findRestitutionIntentionsStatsInDB = dependencies.findRestitutionIntentionsStatsInDB, findFiltersInDb = dependencies.findFiltersInDb, }) => async (activeFilters: { @@ -34,7 +34,10 @@ const getStatsDemandesFactory = }; }) => { const [{ count, demandes }, filters] = await Promise.all([ - findStatsDemandesInDB(activeFilters), + findRestitutionIntentionsStatsInDB({ + ...activeFilters, + millesimeSortie: "2020_2021", + }), findFiltersInDb(activeFilters), ]); @@ -45,4 +48,5 @@ const getStatsDemandesFactory = }; }; -export const getStatsDemandes = getStatsDemandesFactory({}); +export const getRestitutionIntentionsStats = + getRestitutionIntentionsStatsFactory({}); diff --git a/server/src/modules/intentions/queries/countDemandes.query.ts b/server/src/modules/intentions/queries/countDemandes.query.ts index aca3afe84..faf19272b 100644 --- a/server/src/modules/intentions/queries/countDemandes.query.ts +++ b/server/src/modules/intentions/queries/countDemandes.query.ts @@ -2,7 +2,7 @@ import { sql } from "kysely"; import { kdb } from "../../../db/db"; import { RequestUser } from "../../core/model/User"; -import { isDemandeSelectable } from "../utils/isDemandeSelectable"; +import { isDemandeSelectable } from "../../utils/isDemandeSelectable"; export const countDemandes = async ({ user, diff --git a/server/src/modules/intentions/queries/findDemande.query.ts b/server/src/modules/intentions/queries/findDemande.query.ts index c8d6eb9d0..f7148679b 100644 --- a/server/src/modules/intentions/queries/findDemande.query.ts +++ b/server/src/modules/intentions/queries/findDemande.query.ts @@ -8,7 +8,7 @@ import { import { kdb } from "../../../db/db"; import { cleanNull } from "../../../utils/noNull"; import { RequestUser } from "../../core/model/User"; -import { isDemandeSelectable } from "../utils/isDemandeSelectable"; +import { isDemandeSelectable } from "../../utils/isDemandeSelectable"; export const findDemande = async ({ id, diff --git a/server/src/modules/intentions/queries/findDemandes.query.ts b/server/src/modules/intentions/queries/findDemandes.query.ts index 88c6a158c..75d949b28 100644 --- a/server/src/modules/intentions/queries/findDemandes.query.ts +++ b/server/src/modules/intentions/queries/findDemandes.query.ts @@ -4,7 +4,7 @@ import { jsonObjectFrom } from "kysely/helpers/postgres"; import { kdb } from "../../../db/db"; import { cleanNull } from "../../../utils/noNull"; import { RequestUser } from "../../core/model/User"; -import { isDemandeSelectable } from "../utils/isDemandeSelectable"; +import { isDemandeSelectable } from "../../utils/isDemandeSelectable"; export const findDemandes = async ({ status, diff --git a/server/src/modules/intentions/routes/demande.routes.ts b/server/src/modules/intentions/routes/demande.routes.ts index 1ec7e8557..c45fcc6f2 100644 --- a/server/src/modules/intentions/routes/demande.routes.ts +++ b/server/src/modules/intentions/routes/demande.routes.ts @@ -7,8 +7,6 @@ import { countDemandes } from "../queries/countDemandes.query"; import { findDemande } from "../queries/findDemande.query"; import { findDemandes } from "../queries/findDemandes.query"; import { deleteDemande } from "../usecases/deleteDemande/deleteDemande.usecase"; -import { getCountStatsDemandes } from "../usecases/getCountStatsDemandes/getCountStatsDemandes.usecase"; -import { getStatsDemandes } from "../usecases/getStatsDemandes/getStatsDemandes.usecase"; import { submitDemande } from "../usecases/submitDemande/submitDemande.usecase"; import { submitDraftDemande } from "../usecases/submitDraftDemande/submitDraftDemande.usecase"; @@ -120,41 +118,4 @@ export const demandeRoutes = ({ server }: { server: Server }) => { response.status(200).send(result); } ); - - server.get( - "/demandes/stats", - { - schema: ROUTES_CONFIG.getStatsDemandes, - preHandler: hasPermissionHandler("intentions/lecture"), - }, - async (request, response) => { - const { order, orderBy, ...rest } = request.query; - if (!request.user) throw Boom.forbidden(); - - const result = await getStatsDemandes({ - ...rest, - orderBy: order && orderBy ? { order, column: orderBy } : undefined, - user: request.user, - }); - response.status(200).send(result); - } - ); - - server.get( - "/demandes/stats/count", - { - schema: ROUTES_CONFIG.countStatsDemandes, - preHandler: hasPermissionHandler("intentions/lecture"), - }, - async (request, response) => { - const { ...filters } = request.query; - if (!request.user) throw Boom.forbidden(); - - const result = await getCountStatsDemandes({ - ...filters, - user: request.user, - }); - response.status(200).send(result); - } - ); }; diff --git a/server/src/modules/intentions/usecases/submitDemande/submitDemande.usecase.ts b/server/src/modules/intentions/usecases/submitDemande/submitDemande.usecase.ts index e90c7448d..0fdfe2914 100644 --- a/server/src/modules/intentions/usecases/submitDemande/submitDemande.usecase.ts +++ b/server/src/modules/intentions/usecases/submitDemande/submitDemande.usecase.ts @@ -5,11 +5,11 @@ import { demandeValidators, getPermissionScope, guardScope } from "shared"; import { logger } from "../../../../logger"; import { cleanNull } from "../../../../utils/noNull"; import { RequestUser } from "../../../core/model/User"; +import { generateId } from "../../../utils/generateId"; import { findOneDataEtablissement } from "../../repositories/findOneDataEtablissement.query"; import { findOneDataFormation } from "../../repositories/findOneDataFormation.query"; import { findOneDemande } from "../../repositories/findOneDemande.query"; import { findOneSimilarDemande } from "../../repositories/findOneSimilarDemande.query"; -import { generateId } from "../../utils/generateId"; import { createDemandeQuery } from "./createDemandeQuery.dep"; type Demande = { diff --git a/server/src/modules/intentions/usecases/submitDraftDemande/submitDraftDemande.usecase.ts b/server/src/modules/intentions/usecases/submitDraftDemande/submitDraftDemande.usecase.ts index 75b7b9eed..4c6c966d9 100644 --- a/server/src/modules/intentions/usecases/submitDraftDemande/submitDraftDemande.usecase.ts +++ b/server/src/modules/intentions/usecases/submitDraftDemande/submitDraftDemande.usecase.ts @@ -5,11 +5,11 @@ import { demandeValidators, getPermissionScope, guardScope } from "shared"; import { logger } from "../../../../logger"; import { cleanNull } from "../../../../utils/noNull"; import { RequestUser } from "../../../core/model/User"; +import { generateId } from "../../../utils/generateId"; import { findOneDataEtablissement } from "../../repositories/findOneDataEtablissement.query"; import { findOneDataFormation } from "../../repositories/findOneDataFormation.query"; import { findOneDemande } from "../../repositories/findOneDemande.query"; import { findOneSimilarDemande } from "../../repositories/findOneSimilarDemande.query"; -import { generateId } from "../../utils/generateId"; import { createDemandeQuery } from "./createDemandeQuery.dep"; type Demande = { diff --git a/server/src/modules/intentions/utils/countCapacite.ts b/server/src/modules/utils/countCapacite.ts similarity index 98% rename from server/src/modules/intentions/utils/countCapacite.ts rename to server/src/modules/utils/countCapacite.ts index 6d5c8ebba..bf37f9117 100644 --- a/server/src/modules/intentions/utils/countCapacite.ts +++ b/server/src/modules/utils/countCapacite.ts @@ -1,6 +1,6 @@ import { ExpressionBuilder, sql } from "kysely"; -import { DB } from "../../../db/schema"; +import { DB } from "../../db/schema"; export const countOuvertures = ({ eb, diff --git a/server/src/modules/intentions/utils/generateId.ts b/server/src/modules/utils/generateId.ts similarity index 100% rename from server/src/modules/intentions/utils/generateId.ts rename to server/src/modules/utils/generateId.ts diff --git a/server/src/modules/intentions/utils/isDemandeSelectable.ts b/server/src/modules/utils/isDemandeSelectable.ts similarity index 93% rename from server/src/modules/intentions/utils/isDemandeSelectable.ts rename to server/src/modules/utils/isDemandeSelectable.ts index 2f3204784..9877984b4 100644 --- a/server/src/modules/intentions/utils/isDemandeSelectable.ts +++ b/server/src/modules/utils/isDemandeSelectable.ts @@ -2,8 +2,8 @@ import Boom from "@hapi/boom"; import { ExpressionBuilder, sql } from "kysely"; import { getPermissionScope } from "shared"; -import { DB } from "../../../db/schema"; -import { RequestUser } from "../../core/model/User"; +import { DB } from "../../db/schema"; +import { RequestUser } from "../core/model/User"; export const isDemandeSelectable = ({ user }: { user: Pick }) => diff --git a/server/src/modules/intentions/utils/isStatsDemandesVisible.ts b/server/src/modules/utils/isIntentionVisible.ts similarity index 78% rename from server/src/modules/intentions/utils/isStatsDemandesVisible.ts rename to server/src/modules/utils/isIntentionVisible.ts index b6af823da..48f820734 100644 --- a/server/src/modules/intentions/utils/isStatsDemandesVisible.ts +++ b/server/src/modules/utils/isIntentionVisible.ts @@ -2,13 +2,13 @@ import Boom from "@hapi/boom"; import { ExpressionBuilder, sql } from "kysely"; import { getPermissionScope } from "shared"; -import { DB } from "../../../db/schema"; -import { RequestUser } from "../../core/model/User"; +import { DB } from "../../db/schema"; +import { RequestUser } from "../core/model/User"; -export const isStatsDemandeVisible = +export const isIntentionVisible = ({ user }: { user: Pick }) => (eb: ExpressionBuilder) => { - const filter = getStatsDemandesVisibleFilters(user); + const filter = getIntentionsVisiblesFilters(user); return eb.and([ filter.codeRegion ? eb("demande.codeRegion", "=", filter.codeRegion) @@ -19,7 +19,7 @@ export const isStatsDemandeVisible = export const isRegionVisible = ({ user }: { user: Pick }) => (eb: ExpressionBuilder) => { - const filter = getStatsDemandesVisibleFilters(user); + const filter = getIntentionsVisiblesFilters(user); return eb.and([ filter.codeRegion ? eb("region.codeRegion", "=", filter.codeRegion) @@ -27,7 +27,7 @@ export const isRegionVisible = ]); }; -const getStatsDemandesVisibleFilters = ( +const getIntentionsVisiblesFilters = ( user?: Pick ) => { if (!user) throw new Error("missing variable user"); diff --git a/shared/client/ROUTES_CONFIG.ts b/shared/client/ROUTES_CONFIG.ts index 58193636a..bbe23e91b 100644 --- a/shared/client/ROUTES_CONFIG.ts +++ b/shared/client/ROUTES_CONFIG.ts @@ -4,6 +4,7 @@ import { formationSchemas } from "./formations/formation.schema"; import { intentionsSchemas } from "./intentions/intentions.schema"; import { pilotageReformeSchemas } from "./pilotageReforme/pilotageReforme.schema"; import { pilotageTransformationSchemas } from "./pilotageTransfo/pilotageTransfo.schema"; +import { restitutionIntentionsSchemas } from "./restitutionIntentions/restitutionIntentions.schema"; export const ROUTES_CONFIG = { ...formationSchemas, @@ -12,4 +13,5 @@ export const ROUTES_CONFIG = { ...authSchemas, ...intentionsSchemas, ...pilotageTransformationSchemas, + ...restitutionIntentionsSchemas, }; diff --git a/shared/client/client.ts b/shared/client/client.ts index d73d6a482..8e6e9dd8c 100644 --- a/shared/client/client.ts +++ b/shared/client/client.ts @@ -6,6 +6,7 @@ import { createFormationClient } from "./formations/formation.client"; import { createIntentionsClient } from "./intentions/intentions.client"; import { createPilotageReformeClient } from "./pilotageReforme/pilotageReforme.client"; import { createPilotageTransformationClient } from "./pilotageTransfo/pilotageTransfo.client"; +import { createRestitutionIntentionsClient } from "./restitutionIntentions/restitutionIntentions.client"; export const createClient = (instance: AxiosInstance) => ({ ...createFormationClient(instance), @@ -14,4 +15,5 @@ export const createClient = (instance: AxiosInstance) => ({ ...createAuthClient(instance), ...createIntentionsClient(instance), ...createPilotageTransformationClient(instance), + ...createRestitutionIntentionsClient(instance), }); diff --git a/shared/client/etablissements/etablissements.schema.ts b/shared/client/etablissements/etablissements.schema.ts index 9596fbeaf..6530b2f0a 100644 --- a/shared/client/etablissements/etablissements.schema.ts +++ b/shared/client/etablissements/etablissements.schema.ts @@ -29,6 +29,7 @@ const EtablissementLineSchema = Type.Object({ tauxRemplissage: Type.Optional(Type.Number()), tauxPoursuiteEtudes: Type.Optional(Type.Number()), tauxInsertion6mois: Type.Optional(Type.Number()), + tauxDevenirFavorable: Type.Optional(Type.Number()), valeurAjoutee: Type.Optional(Type.Number()), CPC: Type.Optional(Type.String()), CPCSecteur: Type.Optional(Type.String()), @@ -115,6 +116,8 @@ export const etablissementSchemas = { tauxPression: Type.Optional(Type.Number()), tauxInsertion6mois: Type.Optional(Type.Number()), tauxPoursuiteEtudes: Type.Optional(Type.Number()), + tauxDevenirFavorable: Type.Optional(Type.Number()), + positionCadran: Type.Optional(Type.String()), CPC: Type.Optional(Type.String()), CPCSecteur: Type.Optional(Type.String()), CPCSousSecteur: Type.Optional(Type.String()), diff --git a/shared/client/formations/formation.schema.ts b/shared/client/formations/formation.schema.ts index ec044ff83..39be2d4f1 100644 --- a/shared/client/formations/formation.schema.ts +++ b/shared/client/formations/formation.schema.ts @@ -24,6 +24,7 @@ const FormationLineSchema = Type.Object({ tauxPression: Type.Optional(Type.Number()), tauxInsertion6mois: Type.Optional(Type.Number()), tauxPoursuiteEtudes: Type.Optional(Type.Number()), + tauxDevenirFavorable: Type.Optional(Type.Number()), CPC: Type.Optional(Type.String()), CPCSecteur: Type.Optional(Type.String()), CPCSousSecteur: Type.Optional(Type.String()), @@ -34,6 +35,7 @@ const FormationLineSchema = Type.Object({ libelle: Type.Optional(Type.String()), }) ), + positionCadran: Type.Optional(Type.String()), }); const FiltersSchema = Type.Object({ @@ -109,6 +111,7 @@ export const formationSchemas = { tauxPoursuiteEtudes: Type.Number(), tauxPoursuiteEtudesPrecedent: Type.Optional(Type.Number()), tauxDevenirFavorable: Type.Number(), + positionCadran: Type.Optional(Type.String()), CPC: Type.Optional(Type.String()), CPCSecteur: Type.Optional(Type.String()), CPCSousSecteur: Type.Optional(Type.String()), diff --git a/shared/client/intentions/intentions.client.ts b/shared/client/intentions/intentions.client.ts index db3ada9f4..2ad15769a 100644 --- a/shared/client/intentions/intentions.client.ts +++ b/shared/client/intentions/intentions.client.ts @@ -51,16 +51,4 @@ export const createIntentionsClient = (instance: AxiosInstance) => ({ url: "/demandes/count", instance, }), - getStatsDemandes: createClientMethod({ - method: "GET", - url: "/demandes/stats", - instance, - }), - countStatsDemandes: createClientMethod< - typeof ROUTES_CONFIG.countStatsDemandes - >({ - method: "GET", - url: "/demandes/stats/count", - instance, - }), }); diff --git a/shared/client/intentions/intentions.schema.ts b/shared/client/intentions/intentions.schema.ts index dceb32e48..26ceaac2a 100644 --- a/shared/client/intentions/intentions.schema.ts +++ b/shared/client/intentions/intentions.schema.ts @@ -103,11 +103,6 @@ const DemandesItem = Type.Object({ codeAcademie: Type.Optional(Type.String()), }); -const OptionSchema = Type.Object({ - label: Type.String(), - value: Type.String(), -}); - const FiltersSchema = Type.Object({ status: Type.Optional( Type.Union([Type.Literal("draft"), Type.Literal("submitted")]) @@ -116,93 +111,6 @@ const FiltersSchema = Type.Object({ orderBy: Type.Optional(Type.KeyOf(DemandesItem)), }); -const StatsDemandesItem = Type.Object({ - id: Type.String(), - cfd: Type.Optional(Type.String()), - libelleDiplome: Type.Optional(Type.String()), - niveauDiplome: Type.Optional(Type.String()), - 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()), - createdAt: Type.String(), - 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()), - compensationRentreeScolaire: Type.Optional(Type.Number()), - idCompensation: Type.Optional(Type.String()), - 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()), - devenirFavorable: Type.Optional(Type.Number()), - pression: Type.Optional(Type.Number()), - nbEtablissement: Type.Optional(Type.Number()), - commentaire: Type.Optional(Type.String()), -}); - -const StatsFiltersSchema = Type.Object({ - codeRegion: Type.Optional(Type.Array(Type.String())), - codeAcademie: Type.Optional(Type.Array(Type.String())), - codeDepartement: Type.Optional(Type.Array(Type.String())), - commune: Type.Optional(Type.Array(Type.String())), - uai: Type.Optional(Type.Array(Type.String())), - rentreeScolaire: Type.Optional(Type.String()), - typeDemande: Type.Optional(Type.Array(Type.String())), - motif: Type.Optional(Type.Array(Type.String())), - status: Type.Optional( - Type.Union([ - Type.Literal("draft"), - Type.Literal("submitted"), - Type.Undefined(), - ]) - ), - codeNiveauDiplome: Type.Optional(Type.Array(Type.String())), - cfd: Type.Optional(Type.Array(Type.String())), - dispositif: Type.Optional(Type.Array(Type.String())), - filiere: Type.Optional(Type.Array(Type.String())), - cfdFamille: Type.Optional(Type.Array(Type.String())), - coloration: Type.Optional(Type.String()), - amiCMA: Type.Optional(Type.String()), - secteur: Type.Optional(Type.String()), - compensation: Type.Optional(Type.String()), - order: Type.Optional(Type.Union([Type.Literal("asc"), Type.Literal("desc")])), - orderBy: Type.Optional(Type.KeyOf(StatsDemandesItem)), -}); - -const CountCapaciteStatsDemandesSchema = Type.Object({ - total: Type.Number(), - scolaire: Type.Number(), - apprentissage: Type.Number(), - coloration: Type.Optional(Type.Number()), -}); - export const intentionsSchemas = { searchEtab: { params: Type.Object({ @@ -316,52 +224,4 @@ export const intentionsSchemas = { }), }, }, - getStatsDemandes: { - querystring: Type.Intersect([ - StatsFiltersSchema, - Type.Object({ - offset: Type.Optional(Type.Number()), - limit: Type.Optional(Type.Number()), - }), - ]), - response: { - 200: Type.Object({ - filters: Type.Object({ - rentreesScolaires: Type.Array(OptionSchema), - statuts: Type.Array(OptionSchema), - regions: Type.Array(OptionSchema), - academies: Type.Array(OptionSchema), - departements: Type.Array(OptionSchema), - communes: Type.Array(OptionSchema), - etablissements: Type.Array(OptionSchema), - typesDemande: Type.Array(OptionSchema), - motifs: Type.Array(OptionSchema), - status: Type.Array(OptionSchema), - diplomes: Type.Array(OptionSchema), - formations: Type.Array(OptionSchema), - filieres: Type.Array(OptionSchema), - familles: Type.Array(OptionSchema), - dispositifs: Type.Array(OptionSchema), - secteurs: Type.Array(OptionSchema), - amiCMAs: Type.Array(OptionSchema), - colorations: Type.Array(OptionSchema), - compensations: Type.Array(OptionSchema), - }), - demandes: Type.Array(StatsDemandesItem), - count: Type.Number(), - }), - }, - }, - countStatsDemandes: { - querystring: StatsFiltersSchema, - response: { - 200: Type.Object({ - total: CountCapaciteStatsDemandesSchema, - ouvertures: CountCapaciteStatsDemandesSchema, - fermetures: CountCapaciteStatsDemandesSchema, - amiCMAs: CountCapaciteStatsDemandesSchema, - FCILs: CountCapaciteStatsDemandesSchema, - }), - }, - }, } as const; diff --git a/shared/client/pilotageTransfo/pilotageTransfo.schema.ts b/shared/client/pilotageTransfo/pilotageTransfo.schema.ts index 6dfac182a..afe5be353 100644 --- a/shared/client/pilotageTransfo/pilotageTransfo.schema.ts +++ b/shared/client/pilotageTransfo/pilotageTransfo.schema.ts @@ -69,6 +69,7 @@ const FormationTransformationStatsSchema = Type.Object({ placesOuvertes: Type.Number(), placesFermees: Type.Number(), placesTransformees: Type.Number(), + positionCadran: Type.Optional(Type.String()), continuum: Type.Optional( Type.Object({ cfd: Type.String(), diff --git a/shared/client/restitutionIntentions/restitutionIntentions.client.ts b/shared/client/restitutionIntentions/restitutionIntentions.client.ts new file mode 100644 index 000000000..f07afef9a --- /dev/null +++ b/shared/client/restitutionIntentions/restitutionIntentions.client.ts @@ -0,0 +1,21 @@ +import { AxiosInstance } from "axios"; + +import { createClientMethod } from "../clientFactory"; +import { ROUTES_CONFIG } from "../ROUTES_CONFIG"; + +export const createRestitutionIntentionsClient = (instance: AxiosInstance) => ({ + getRestitutionIntentionsStats: createClientMethod< + typeof ROUTES_CONFIG.getRestitutionIntentionsStats + >({ + method: "GET", + url: "/intentions/stats", + instance, + }), + countRestitutionIntentionsStats: createClientMethod< + typeof ROUTES_CONFIG.countRestitutionIntentionsStats + >({ + method: "GET", + url: "/intentions/stats/count", + instance, + }), +}); diff --git a/shared/client/restitutionIntentions/restitutionIntentions.schema.ts b/shared/client/restitutionIntentions/restitutionIntentions.schema.ts new file mode 100644 index 000000000..5732f09a5 --- /dev/null +++ b/shared/client/restitutionIntentions/restitutionIntentions.schema.ts @@ -0,0 +1,143 @@ +import { Type } from "@sinclair/typebox"; + +const OptionSchema = Type.Object({ + label: Type.String(), + value: Type.String(), +}); + +const StatsDemandesItem = Type.Object({ + id: Type.String(), + cfd: Type.Optional(Type.String()), + libelleDiplome: Type.Optional(Type.String()), + dispositifId: Type.Optional(Type.String()), + libelleDispositif: Type.Optional(Type.String()), + niveauDiplome: Type.Optional(Type.String()), + uai: Type.Optional(Type.String()), + libelleEtablissement: Type.Optional(Type.String()), + commune: Type.Optional(Type.String()), + rentreeScolaire: Type.Optional(Type.Number()), + typeDemande: Type.Optional(Type.String()), + motif: Type.Optional(Type.Array(Type.String())), + autreMotif: Type.Optional(Type.String()), + coloration: Type.Optional(Type.Boolean()), + libelleColoration: Type.Optional(Type.String()), + libelleFCIL: Type.Optional(Type.String()), + amiCma: Type.Optional(Type.Boolean()), + poursuitePedagogique: Type.Optional(Type.Boolean()), + commentaire: Type.Optional(Type.String()), + libelleFiliere: Type.Optional(Type.String()), + status: 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()), + createdAt: Type.String(), + compensationCfd: Type.Optional(Type.String()), + compensationDispositifId: Type.Optional(Type.String()), + compensationUai: Type.Optional(Type.String()), + differenceCapaciteScolaire: Type.Optional(Type.Number()), + capaciteScolaireActuelle: Type.Optional(Type.Number()), + capaciteScolaire: Type.Optional(Type.Number()), + capaciteScolaireColoree: Type.Optional(Type.Number()), + differenceCapaciteApprentissage: Type.Optional(Type.Number()), + capaciteApprentissageActuelle: Type.Optional(Type.Number()), + capaciteApprentissage: Type.Optional(Type.Number()), + capaciteApprentissageColoree: Type.Optional(Type.Number()), + insertion: Type.Optional(Type.Number()), + poursuite: Type.Optional(Type.Number()), + devenirFavorable: Type.Optional(Type.Number()), + pression: Type.Optional(Type.Number()), + nbEtablissement: Type.Optional(Type.Number()), + positionCadran: Type.Optional(Type.String()), + tauxInsertionMoyen: Type.Optional(Type.Number()), + tauxPoursuiteMoyen: Type.Optional(Type.Number()), +}); + +const StatsFiltersSchema = Type.Object({ + codeRegion: Type.Optional(Type.Array(Type.String())), + codeAcademie: Type.Optional(Type.Array(Type.String())), + codeDepartement: Type.Optional(Type.Array(Type.String())), + commune: Type.Optional(Type.Array(Type.String())), + uai: Type.Optional(Type.Array(Type.String())), + rentreeScolaire: Type.Optional(Type.String()), + typeDemande: Type.Optional(Type.Array(Type.String())), + motif: Type.Optional(Type.Array(Type.String())), + status: Type.Optional( + Type.Union([ + Type.Literal("draft"), + Type.Literal("submitted"), + Type.Undefined(), + ]) + ), + codeNiveauDiplome: Type.Optional(Type.Array(Type.String())), + cfd: Type.Optional(Type.Array(Type.String())), + dispositif: Type.Optional(Type.Array(Type.String())), + filiere: Type.Optional(Type.Array(Type.String())), + cfdFamille: Type.Optional(Type.Array(Type.String())), + coloration: Type.Optional(Type.String()), + amiCMA: Type.Optional(Type.String()), + secteur: Type.Optional(Type.String()), + compensation: Type.Optional(Type.String()), + positionCadran: Type.Optional(Type.String()), + order: Type.Optional(Type.Union([Type.Literal("asc"), Type.Literal("desc")])), + orderBy: Type.Optional(Type.KeyOf(Type.Omit(StatsDemandesItem, []))), +}); + +const CountCapaciteStatsDemandesSchema = Type.Object({ + total: Type.Number(), + scolaire: Type.Number(), + apprentissage: Type.Number(), + coloration: Type.Optional(Type.Number()), +}); + +export const restitutionIntentionsSchemas = { + getRestitutionIntentionsStats: { + querystring: Type.Intersect([ + StatsFiltersSchema, + Type.Object({ + offset: Type.Optional(Type.Number()), + limit: Type.Optional(Type.Number()), + }), + ]), + response: { + 200: Type.Object({ + filters: Type.Object({ + rentreesScolaires: Type.Array(OptionSchema), + statuts: Type.Array(OptionSchema), + regions: Type.Array(OptionSchema), + academies: Type.Array(OptionSchema), + departements: Type.Array(OptionSchema), + communes: Type.Array(OptionSchema), + etablissements: Type.Array(OptionSchema), + typesDemande: Type.Array(OptionSchema), + motifs: Type.Array(OptionSchema), + status: Type.Array(OptionSchema), + diplomes: Type.Array(OptionSchema), + formations: Type.Array(OptionSchema), + filieres: Type.Array(OptionSchema), + familles: Type.Array(OptionSchema), + dispositifs: Type.Array(OptionSchema), + secteurs: Type.Array(OptionSchema), + amiCMAs: Type.Array(OptionSchema), + colorations: Type.Array(OptionSchema), + compensations: Type.Array(OptionSchema), + }), + demandes: Type.Array(StatsDemandesItem), + count: Type.Number(), + }), + }, + }, + countRestitutionIntentionsStats: { + querystring: StatsFiltersSchema, + response: { + 200: Type.Object({ + total: CountCapaciteStatsDemandesSchema, + ouvertures: CountCapaciteStatsDemandesSchema, + fermetures: CountCapaciteStatsDemandesSchema, + amiCMAs: CountCapaciteStatsDemandesSchema, + FCILs: CountCapaciteStatsDemandesSchema, + }), + }, + }, +} as const; diff --git a/ui/app/(wrapped)/console/etablissements/components/LineContent.tsx b/ui/app/(wrapped)/console/etablissements/components/LineContent.tsx index 2b13ec304..93b037857 100644 --- a/ui/app/(wrapped)/console/etablissements/components/LineContent.tsx +++ b/ui/app/(wrapped)/console/etablissements/components/LineContent.tsx @@ -83,6 +83,12 @@ export const EtablissementLineContent = ({ value={line.tauxPoursuiteEtudes} /> + + + {line.valeurAjoutee ?? "-"} {line.secteur ?? "-"} {line.UAI ?? "-"} diff --git a/ui/app/(wrapped)/console/etablissements/page.tsx b/ui/app/(wrapped)/console/etablissements/page.tsx index 0e8016a67..eb1e18ca9 100644 --- a/ui/app/(wrapped)/console/etablissements/page.tsx +++ b/ui/app/(wrapped)/console/etablissements/page.tsx @@ -56,6 +56,7 @@ const ETABLISSEMENTS_COLUMNS = { tauxRemplissage: "Tx de remplissage", tauxInsertion6mois: "Tx d'emploi 6 mois régional", tauxPoursuiteEtudes: "Tx de poursuite d'études régional", + tauxDevenirFavorable: "Tx de devenir favorable régional", valeurAjoutee: "Valeur ajoutée", secteur: "Secteur", UAI: "UAI", @@ -526,6 +527,17 @@ export default function Etablissements() { label="Tout élève inscrit à N+1 (réorientation et redoublement compris)." /> + handleOrder("tauxDevenirFavorable")} + > + + {ETABLISSEMENTS_COLUMNS.tauxDevenirFavorable} + + + + + {line.libelleDispositif ?? "-"} {line.libelleOfficielFamille ?? "-"} {line.codeFormationDiplome ?? "-"} @@ -101,6 +107,7 @@ export const FormationLineContent = ({ {line.CPCSecteur ?? "-"} {line.CPCSousSecteur ?? "-"} {line.libelleFiliere ?? "-"} + {line.positionCadran} ); }; diff --git a/ui/app/(wrapped)/console/formations/page.tsx b/ui/app/(wrapped)/console/formations/page.tsx index ec4f893ed..2ca94ed29 100644 --- a/ui/app/(wrapped)/console/formations/page.tsx +++ b/ui/app/(wrapped)/console/formations/page.tsx @@ -51,6 +51,7 @@ const FORMATIONS_COLUMNS = { tauxRemplissage: "Tx de remplissage", tauxInsertion6mois: "Tx d'emploi 6 mois régional", tauxPoursuiteEtudes: "Tx de poursuite d'études régional", + tauxDevenirFavorable: "Tx de devenir favorable régional", libelleDispositif: "Dispositif", libelleOfficielFamille: " Famille de métiers", codeFormationDiplome: "Code diplôme", @@ -60,6 +61,7 @@ const FORMATIONS_COLUMNS = { libelleFiliere: "Secteur d’activité", "continuum.libelle": "Diplôme historique", "continuum.cfd": "Code diplôme historique", + positionCadran: "Position dans le cadran", } satisfies ExportColumns< ApiType["formations"][number] >; @@ -439,6 +441,17 @@ export default function Formations() { label="Tout élève inscrit à N+1 (réorientation et redoublement compris)." /> + handleOrder("tauxDevenirFavorable")} + > + + {FORMATIONS_COLUMNS.tauxDevenirFavorable} + + handleOrder("libelleDispositif")} @@ -482,6 +495,7 @@ export default function Formations() { {FORMATIONS_COLUMNS.libelleFiliere} + {FORMATIONS_COLUMNS.positionCadran} diff --git a/ui/app/(wrapped)/intentions/pilotage/components/CadranSection.tsx b/ui/app/(wrapped)/intentions/pilotage/components/CadranSection.tsx index 81bd35ce1..eb82081af 100644 --- a/ui/app/(wrapped)/intentions/pilotage/components/CadranSection.tsx +++ b/ui/app/(wrapped)/intentions/pilotage/components/CadranSection.tsx @@ -15,42 +15,41 @@ import { Select, Skeleton, Stack, - Table, - TableContainer, - Tbody, - Td, Text, - Th, - Thead, - Tr, } from "@chakra-ui/react"; import { useQuery } from "@tanstack/react-query"; +import _ from "lodash"; 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 { TableCadran } from "../../../../../components/TableCadran"; import { createParametrizedUrl } from "../../../../../utils/createParametrizedUrl"; import { downloadCsv } from "../../../../../utils/downloadCsv"; import { useStateParams } from "../../../../../utils/useFilters"; -import { Filters, OrderFormationsTransformationStats, Scope } from "../types"; +import { + Filters, + OrderFormationsTransformationStats, + PilotageTransformationStats, + Scope, +} from "../types"; export const CadranSection = ({ scope, parentFilters, + scopeFilters, }: { scope?: { type: Scope; value: string | undefined; }; - rentreeScolaire?: string; parentFilters: Partial; + scopeFilters?: PilotageTransformationStats["filters"]; }) => { const trackEvent = usePlausible(); const [typeVue, setTypeVue] = useState<"cadran" | "tableau">("cadran"); @@ -106,34 +105,14 @@ 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] + const getLibelleTerritoire = ( + territoires?: Array<{ label: string; value: string }>, + code?: string ) => { - 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"; + if (scope?.value && scopeFilters) + return _.find(territoires, (territoire) => territoire.value === code) + ?.label; + return undefined; }; const handleOrder = ( @@ -179,13 +158,44 @@ export const CadranSection = ({ variant="solid" onClick={async () => { if (!formations) return; - downloadCsv("formations_transformees.csv", formations, { - libelleDiplome: "Formation", - libelleDispositif: "Dispositif", - tauxInsertion: "Tx emploi", - tauxPoursuite: "Tx poursuite", - tauxPression: "Tx pression", - }); + downloadCsv( + "formations_transformees.csv", + formations.map((formation) => ({ + ...formation, + libelleRegion: + scope?.type === "regions" + ? getLibelleTerritoire( + scopeFilters?.regions, + scope.value + ) + : undefined, + libelleAcademie: + scope?.type === "academies" + ? getLibelleTerritoire( + scopeFilters?.academies, + scope.value + ) + : undefined, + libelleDepartement: + scope?.type === "departements" + ? getLibelleTerritoire( + scopeFilters?.departements, + scope.value + ) + : undefined, + })), + { + libelleDiplome: "Formation", + libelleDispositif: "Dispositif", + tauxInsertion: "Taux d'emploi", + tauxPoursuite: "Taux de poursuite", + tauxPression: "Taux de pression", + positionCadran: "Position dans le cadran", + libelleRegion: "Région", + libelleAcademie: "Académie", + libelleDepartement: "Département", + } + ); }} > @@ -364,116 +374,17 @@ export const CadranSection = ({ ]} /> ) : ( - - - - - - - - - - - - - {formations.map((formation, index) => ( - setFormationId(formation.cfd)} - cursor="pointer" - > - - - - - - ))} - -
handleOrder("libelleDiplome")} - > - - FORMATION - handleOrder("tauxPression")} - > - - TX PRESSION - handleOrder("tauxInsertion")} - > - - TX EMPLOI - handleOrder("tauxPoursuite")} - > - - TX POURSUITE -
- {formation.libelleDiplome} - - {formation.tauxPression - ? `${formation.tauxPression} %` - : "-"} - {`${formation.tauxInsertion} %`}{`${formation.tauxPoursuite} %`}
-
-
+ + handleOrder( + column as OrderFormationsTransformationStats["orderBy"] + ) + } + /> ))} {!formations && } diff --git a/ui/app/(wrapped)/intentions/pilotage/components/FiltersSection.tsx b/ui/app/(wrapped)/intentions/pilotage/components/FiltersSection.tsx index f17be1bc4..3787dfacb 100644 --- a/ui/app/(wrapped)/intentions/pilotage/components/FiltersSection.tsx +++ b/ui/app/(wrapped)/intentions/pilotage/components/FiltersSection.tsx @@ -19,7 +19,8 @@ export const FiltersSection = ({ activeTerritoiresFilters: TerritoiresFilters; handleTerritoiresFilters: ( type: keyof TerritoiresFilters, - value: TerritoiresFilters[keyof TerritoiresFilters] + value: TerritoiresFilters[keyof TerritoiresFilters], + label?: string ) => void; activeFilters: Filters; handleFilters: (type: keyof Filters, value: Filters[keyof Filters]) => void; @@ -59,7 +60,11 @@ export const FiltersSection = ({ variant="newInput" value={activeTerritoiresFilters.regions ?? ""} onChange={(e) => - handleTerritoiresFilters("regions", e.target.value) + handleTerritoiresFilters( + "regions", + e.target.value, + e.target[e.target.selectedIndex].textContent ?? "" + ) } placeholder="TOUTES" > @@ -78,7 +83,11 @@ export const FiltersSection = ({ variant="newInput" value={activeTerritoiresFilters.academies ?? ""} onChange={(e) => - handleTerritoiresFilters("academies", e.target.value) + handleTerritoiresFilters( + "academies", + e.target.value, + e.target[e.target.selectedIndex].textContent ?? "" + ) } placeholder="TOUTES" > @@ -97,7 +106,11 @@ export const FiltersSection = ({ variant="newInput" value={activeTerritoiresFilters.departements ?? ""} onChange={(e) => - handleTerritoiresFilters("departements", e.target.value) + handleTerritoiresFilters( + "departements", + e.target.value, + e.target[e.target.selectedIndex].textContent ?? "" + ) } placeholder="TOUS" > diff --git a/ui/app/(wrapped)/intentions/pilotage/page.tsx b/ui/app/(wrapped)/intentions/pilotage/page.tsx index 705e26ad8..f869b6529 100644 --- a/ui/app/(wrapped)/intentions/pilotage/page.tsx +++ b/ui/app/(wrapped)/intentions/pilotage/page.tsx @@ -166,7 +166,11 @@ export default withAuth( /> - + {STATS_DEMANDES_COLUMNS.libelleColoration} {STATS_DEMANDES_COLUMNS.libelleFCIL} {STATS_DEMANDES_COLUMNS.commentaire} + {STATS_DEMANDES_COLUMNS.positionCadran} {STATS_DEMANDES_COLUMNS.id} - {data?.demandes.map( - ( - demande: ApiType["demandes"][0] - ) => { - return ( - - - - - - ); - } - )} + {data?.demandes.map((demande: StatsDemandes["demandes"][0]) => { + return ( + + + + + + ); + })} diff --git a/ui/app/(wrapped)/intentions/restitution/ConsoleSection/LineContent.tsx b/ui/app/(wrapped)/intentions/restitution/ConsoleSection/LineContent.tsx index 9f33c7d90..1309abb7d 100644 --- a/ui/app/(wrapped)/intentions/restitution/ConsoleSection/LineContent.tsx +++ b/ui/app/(wrapped)/intentions/restitution/ConsoleSection/LineContent.tsx @@ -1,16 +1,15 @@ import { Td } from "@chakra-ui/react"; -import { ApiType } from "shared"; -import { api } from "../../../../../api.client"; import { GraphWrapper } from "../../../../../components/GraphWrapper"; import { TableBadge } from "../../../../../components/TableBadge"; import { getTauxPressionStyle } from "../../../../../utils/getBgScale"; import { getMotifLabel, MotifLabel } from "../../../utils/motifDemandeUtils"; import { getTypeDemandeLabel } from "../../../utils/typeDemandeUtils"; +import { StatsDemandes } from "../types"; export const LineContent = ({ demande, }: { - demande: ApiType["demandes"][0]; + demande: StatsDemandes["demandes"][0]; }) => { const handleMotifLabel = (motif?: string[], autreMotif?: string) => { return motif ? ( @@ -84,6 +83,7 @@ export const LineContent = ({ {demande.commentaire} + {demande.positionCadran} {demande.id} ); diff --git a/ui/app/(wrapped)/intentions/restitution/HeaderSection/CountersSection.tsx b/ui/app/(wrapped)/intentions/restitution/HeaderSection/CountersSection.tsx index ec0021c31..de641c51b 100644 --- a/ui/app/(wrapped)/intentions/restitution/HeaderSection/CountersSection.tsx +++ b/ui/app/(wrapped)/intentions/restitution/HeaderSection/CountersSection.tsx @@ -83,7 +83,7 @@ const CountCard = ({ export const CountersSection = ({ countData, }: { - countData?: ApiType; + countData?: ApiType; }) => { return ( <> diff --git a/ui/app/(wrapped)/intentions/restitution/STATS_DEMANDES_COLUMN.ts b/ui/app/(wrapped)/intentions/restitution/STATS_DEMANDES_COLUMN.ts index 30b5b3111..89a61eb31 100644 --- a/ui/app/(wrapped)/intentions/restitution/STATS_DEMANDES_COLUMN.ts +++ b/ui/app/(wrapped)/intentions/restitution/STATS_DEMANDES_COLUMN.ts @@ -8,8 +8,8 @@ export const STATS_DEMANDES_COLUMNS = { cfd: "CFD", libelleDiplome: "Formation", dispositifId: "DispositifId", - niveauDiplome: "Diplôme", libelleDispositif: "Dispositif", + niveauDiplome: "Diplôme", uai: "UAI", libelleEtablissement: "Établissement", commune: "Commune", @@ -26,13 +26,13 @@ export const STATS_DEMANDES_COLUMNS = { status: "Status", codeRegion: "CodeRegion", libelleRegion: "Région", + codeAcademie: "CodeAcadémie", codeDepartement: "CodeDepartement", libelleDepartement: "Département", - codeAcademie: "CodeAcadémie", createdAt: "Date de création", compensationCfd: "CFD compensé", - compensationUai: "UAI compensé", compensationDispositifId: "Dispositif compensé", + compensationUai: "UAI compensé", differenceCapaciteScolaire: "Nombre de places en voie scolaire", capaciteScolaireActuelle: "Capacité scolaire actuelle", capaciteScolaire: "Capacité scolaire", @@ -44,8 +44,9 @@ export const STATS_DEMANDES_COLUMNS = { insertion: "Tx d'emploi à 6 mois régional", poursuite: "Tx de poursuite d'études régional", devenirFavorable: "Tx de devenir favorable régional", + positionCadran: "Position dans le cadran", pression: "Tx de pression régional", nbEtablissement: "Nb établissement", } satisfies ExportColumns< - ApiType["demandes"][number] + ApiType["demandes"][number] >; diff --git a/ui/app/(wrapped)/intentions/restitution/page.tsx b/ui/app/(wrapped)/intentions/restitution/page.tsx index 1ecad82dc..61ceb256f 100644 --- a/ui/app/(wrapped)/intentions/restitution/page.tsx +++ b/ui/app/(wrapped)/intentions/restitution/page.tsx @@ -124,8 +124,8 @@ export default () => { const { data, isLoading: isLoading } = useQuery({ keepPreviousData: true, staleTime: 10000000, - queryKey: ["statsDemandes", filters, order, page], - queryFn: api.getStatsDemandes({ + queryKey: ["restitutionIntentionsStats", filters, order, page], + queryFn: api.getRestitutionIntentionsStats({ query: { ...filters, ...order, @@ -138,10 +138,10 @@ export default () => { const { data: countData, isLoading: isLoadingCount } = useQuery({ keepPreviousData: true, staleTime: 10000000, - queryKey: ["countStatsDemandes", filters], + queryKey: ["countRestitutionIntentionsStats", filters], queryFn: async () => api - .countStatsDemandes({ + .countRestitutionIntentionsStats({ query: { ...filters, }, @@ -173,7 +173,7 @@ export default () => { onExport={async () => { trackEvent("restitution-demandes:export"); const data = await api - .getStatsDemandes({ + .getRestitutionIntentionsStats({ query: { ...filters, ...order, limit: 10000000 }, }) .call(); diff --git a/ui/app/(wrapped)/intentions/restitution/types.ts b/ui/app/(wrapped)/intentions/restitution/types.ts index ff5819eef..51fb147fd 100644 --- a/ui/app/(wrapped)/intentions/restitution/types.ts +++ b/ui/app/(wrapped)/intentions/restitution/types.ts @@ -3,7 +3,7 @@ import { ApiType } from "shared"; import { api } from "../../../../api.client"; export type StatsDemandesQuery = Parameters< - typeof api.getStatsDemandes + typeof api.getRestitutionIntentionsStats >[0]["query"]; export type Filters = Pick< @@ -30,12 +30,14 @@ export type Filters = Pick< export type Order = Pick; -export type StatsDemandes = ApiType; +export type StatsDemandes = ApiType; export type IndicateurType = "insertion" | "poursuite"; export type CountStatsDemandesQuery = Parameters< - typeof api.countStatsDemandes + typeof api.countRestitutionIntentionsStats >[0]["query"]; -export type CountStatsDemandes = ApiType; +export type CountStatsDemandes = ApiType< + typeof api.countRestitutionIntentionsStats +>; diff --git a/ui/app/(wrapped)/panorama/etablissement/[uai]/CadranSection.tsx b/ui/app/(wrapped)/panorama/etablissement/[uai]/CadranSection.tsx index 0a88a0ef6..7ebe4b603 100644 --- a/ui/app/(wrapped)/panorama/etablissement/[uai]/CadranSection.tsx +++ b/ui/app/(wrapped)/panorama/etablissement/[uai]/CadranSection.tsx @@ -1,6 +1,8 @@ +import { ViewIcon } from "@chakra-ui/icons"; import { AspectRatio, Box, + Button, Container, Flex, Heading, @@ -8,11 +10,12 @@ import { Text, VStack, } from "@chakra-ui/react"; -import { useMemo } from "react"; +import { useMemo, useState } from "react"; import { ApiType } from "shared"; import { api } from "../../../../../api.client"; import { Cadran } from "../../../../../components/Cadran"; +import { TableCadran } from "../../../../../components/TableCadran"; import { FormationTooltipContent } from "./FormationTooltipContent"; type RequiredFields = T & Required>; @@ -37,6 +40,12 @@ export const CadranSection = ({ codeNiveauDiplome?: string[]; rentreeScolaire?: string; }) => { + const [typeVue, setTypeVue] = useState<"cadran" | "tableau">("cadran"); + + const toggleTypeVue = () => { + if (typeVue === "cadran") setTypeVue("tableau"); + else setTypeVue("cadran"); + }; const filteredFormations = useMemo( () => cadranFormations?.filter( @@ -65,31 +74,52 @@ export const CadranSection = ({ - - - {filteredFormations?.length ?? "-"} certifications - - - {filteredFormations?.reduce( - (acc, { effectif }) => acc + (effectif ?? 0), - 0 - ) ?? "-"}{" "} - élèves - + + + + + + + {filteredFormations?.length ?? "-"} certifications + + + {filteredFormations?.reduce( + (acc, { effectif }) => acc + (effectif ?? 0), + 0 + ) ?? "-"}{" "} + élèves + + - + <> - {filteredFormations && ( - item.cfd + item.dispositifId} - effectifSizes={effectifSizes} - /> - )} + {filteredFormations && + (typeVue === "cadran" ? ( + ({ + ...formation, + tauxInsertion: formation.tauxInsertion6mois, + tauxPoursuite: formation.tauxPoursuiteEtudes, + }))} + itemId={(item) => item.cfd + item.dispositifId} + effectifSizes={effectifSizes} + /> + ) : ( + ({ + ...formation, + tauxInsertion: formation.tauxInsertion6mois, + tauxPoursuite: formation.tauxPoursuiteEtudes, + }))} + /> + ))} {!filteredFormations && } diff --git a/ui/app/(wrapped)/panorama/region/[codeRegion]/CadranSection.tsx b/ui/app/(wrapped)/panorama/region/[codeRegion]/CadranSection.tsx index fd9069db8..a4dc190d3 100644 --- a/ui/app/(wrapped)/panorama/region/[codeRegion]/CadranSection.tsx +++ b/ui/app/(wrapped)/panorama/region/[codeRegion]/CadranSection.tsx @@ -1,7 +1,8 @@ -import { SmallCloseIcon } from "@chakra-ui/icons"; +import { SmallCloseIcon, ViewIcon } from "@chakra-ui/icons"; import { AspectRatio, Box, + Button, Center, chakra, Container, @@ -26,6 +27,7 @@ import { import { ReactNode, useMemo, useState } from "react"; import { Cadran } from "../../../../../components/Cadran"; +import { TableCadran } from "../../../../../components/TableCadran"; import { FormationTooltipContent } from "./FormationTooltipContent"; import { PanoramaFormations } from "./type"; @@ -132,6 +134,12 @@ export const CadranSection = ({ }) => { const [effectifMin, setEffectifMin] = useState(0); const [tendance, setTendance] = useState(); + const [typeVue, setTypeVue] = useState<"cadran" | "tableau">("cadran"); + + const toggleTypeVue = () => { + if (typeVue === "cadran") setTypeVue("tableau"); + else setTypeVue("cadran"); + }; const filteredFormations = useMemo( () => @@ -247,33 +255,54 @@ export const CadranSection = ({ - - - {filteredFormations?.length ?? "-"} certifications - - - {filteredFormations?.reduce( - (acc, { effectif }) => acc + (effectif ?? 0), - 0 - ) ?? "-"}{" "} - élèves - + + + + + + + {filteredFormations?.length ?? "-"} certifications + + + {filteredFormations?.reduce( + (acc, { effectif }) => acc + (effectif ?? 0), + 0 + ) ?? "-"}{" "} + élèves + + - + <> - {filteredFormations && ( - - item.codeFormationDiplome + item.dispositifId - } - InfoTootipContent={InfoTooltipContent} - effectifSizes={effectifSizes} - /> - )} + {filteredFormations && + (typeVue === "cadran" ? ( + ({ + ...formation, + tauxInsertion: formation.tauxInsertion6mois, + tauxPoursuite: formation.tauxPoursuiteEtudes, + }))} + TooltipContent={FormationTooltipContent} + itemId={(item) => + item.codeFormationDiplome + item.dispositifId + } + InfoTootipContent={InfoTooltipContent} + effectifSizes={effectifSizes} + /> + ) : ( + ({ + ...formation, + tauxInsertion: formation.tauxInsertion6mois, + tauxPoursuite: formation.tauxPoursuiteEtudes, + }))} + /> + ))} {!filteredFormations && } diff --git a/ui/components/Cadran.tsx b/ui/components/Cadran.tsx index 502d0f0f7..cbe31dfac 100644 --- a/ui/components/Cadran.tsx +++ b/ui/components/Cadran.tsx @@ -34,8 +34,9 @@ const cadranLabelStyle = { export const Cadran = function < F extends { effectif?: number; - tauxPoursuiteEtudes: number; - tauxInsertion6mois: number; + tauxPoursuite: number; + tauxInsertion: number; + positionCadran?: string; }, >({ className, @@ -98,7 +99,7 @@ export const Cadran = function < const series = useMemo(() => { return data.map((formation) => ({ - value: [formation.tauxPoursuiteEtudes, formation.tauxInsertion6mois], + value: [formation.tauxPoursuite, formation.tauxInsertion], name: itemId(formation), })); }, [data]); @@ -106,26 +107,10 @@ export const Cadran = function < const repartitionCadrans = useMemo(() => { if (!meanInsertion || !meanPoursuite) return; return { - q1: data.filter( - (item) => - item.tauxInsertion6mois >= meanInsertion && - item.tauxPoursuiteEtudes < meanPoursuite - ).length, - q2: data.filter( - (item) => - item.tauxInsertion6mois >= meanInsertion && - item.tauxPoursuiteEtudes > meanPoursuite - ).length, - q3: data.filter( - (item) => - item.tauxInsertion6mois < meanInsertion && - item.tauxPoursuiteEtudes >= meanPoursuite - ).length, - q4: data.filter( - (item) => - item.tauxInsertion6mois < meanInsertion && - item.tauxPoursuiteEtudes < meanPoursuite - ).length, + q1: data.filter((item) => item.positionCadran === "Q1").length, + q2: data.filter((item) => item.positionCadran === "Q2").length, + q3: data.filter((item) => item.positionCadran === "Q3").length, + q4: data.filter((item) => item.positionCadran === "Q4").length, }; }, [data, meanInsertion, meanPoursuite]); @@ -244,7 +229,7 @@ export const Cadran = function < { coord: [meanPoursuite, meanInsertion], itemStyle: { color: "#C8F6D6" }, - name: `Q2 - ${repartitionCadrans?.q2} formations`, + name: `Q1 - ${repartitionCadrans?.q1} formations`, label: { ...cadranLabelStyle, position: "insideTopRight", @@ -256,7 +241,7 @@ export const Cadran = function < { coord: [0, meanInsertion], itemStyle: { color: "rgba(0,0,0,0.04)" }, - name: `Q1 - ${repartitionCadrans?.q1} formations`, + name: `Q2 - ${repartitionCadrans?.q2} formations`, label: { ...cadranLabelStyle, position: "insideTopLeft", @@ -327,23 +312,25 @@ export const Cadran = function < bottom="0" > - {InfoTootipContent && ( - - - - )} + + {InfoTootipContent && ( + + + + )} - {displayedDetail && TooltipContent && ( - setDisplayedDetail(undefined)} - {...popperInstance.getPopperProps()} - > - {TooltipContent && displayedDetail && ( - - )} - - )} + {displayedDetail && TooltipContent && ( + setDisplayedDetail(undefined)} + {...popperInstance.getPopperProps()} + > + {TooltipContent && displayedDetail && ( + + )} + + )} + ); }; diff --git a/ui/components/TableCadran.tsx b/ui/components/TableCadran.tsx new file mode 100644 index 000000000..276c4b66b --- /dev/null +++ b/ui/components/TableCadran.tsx @@ -0,0 +1,169 @@ +import { + Box, + Flex, + Table, + TableContainer, + Tbody, + Td, + Th, + Thead, + Tr, +} from "@chakra-ui/react"; + +import { TauxPressionScale } from "../app/(wrapped)/components/TauxPressionScale"; +import { getTauxPressionStyle } from "../utils/getBgScale"; +import { GraphWrapper } from "./GraphWrapper"; +import { OrderIcon } from "./OrderIcon"; +import { TableBadge } from "./TableBadge"; +import { TooltipIcon } from "./TooltipIcon"; + +type Formation = { + libelleDiplome?: string; + tauxPoursuite?: number; + tauxInsertion?: number; + tauxPression?: number; + positionCadran?: string; + cfd?: string; +}; + +export const TableCadran = ({ + formations, + handleOrder, + handleClick, + currentCfd, + order, +}: { + formations: Formation[]; + handleOrder?: (column?: string) => void; + handleClick?: (value?: string) => void; + currentCfd?: string; + order?: { + order?: "asc" | "desc"; + orderBy?: string; + }; +}) => { + const getTdColor = (formation: Formation) => { + if (currentCfd && formation.cfd === currentCfd) return "white !important"; + return ""; + }; + + const getTrBgColor = (formation: Formation) => { + if (currentCfd && formation.cfd === currentCfd) + return "blue.main !important"; + switch (formation.positionCadran) { + case "Q1": + return "#C8F6D6"; + case "Q4": + return "#ffe2e1"; + default: + return "inherit"; + } + }; + return ( + + + + + + + + + + + + + {formations.map((formation, index) => ( + handleClick && handleClick(formation.cfd)} + cursor={handleClick ? "pointer" : "default"} + > + + + + + + ))} + +
handleOrder && handleOrder("libelleDiplome")} + > + {handleOrder && ( + + )} + FORMATION + handleOrder && handleOrder("tauxPression")} + > + {handleOrder && } + TX PRESSION + + + Le ratio entre le nombre de premiers voeux et la + capacité de la formation au niveau régional. + + + + } + /> + handleOrder && handleOrder("tauxInsertion")} + > + {handleOrder && } + TX EMPLOI + handleOrder && handleOrder("tauxPoursuite")} + > + {handleOrder && } + TX POURSUITE +
+ {formation.libelleDiplome} + + + {formation.tauxPression !== undefined + ? formation.tauxPression / 100 + : "-"} + + + + + +
+
+
+ ); +};