From aa9397dd3901ce5fc5c382190cffce93df9132b2 Mon Sep 17 00:00:00 2001 From: Paul Gaucher Date: Wed, 11 Dec 2024 13:45:47 +0100 Subject: [PATCH 1/8] fix: ajout de la mission locale dans les organisations --- .../validationSchema.test.ts.snap | 37 +++++++++++++++++++ shared/models/data/organisations.model.ts | 15 ++++++++ 2 files changed, 52 insertions(+) diff --git a/server/src/common/mongodb/__snapshots__/validationSchema.test.ts.snap b/server/src/common/mongodb/__snapshots__/validationSchema.test.ts.snap index ab010a555..7fdcdb3f0 100644 --- a/server/src/common/mongodb/__snapshots__/validationSchema.test.ts.snap +++ b/server/src/common/mongodb/__snapshots__/validationSchema.test.ts.snap @@ -8947,6 +8947,43 @@ exports[`validation-schema > should create validation schema for opcos > opcos 1 exports[`validation-schema > should create validation schema for organisations > organisations 1`] = ` { "anyOf": [ + { + "additionalProperties": false, + "bsonType": "object", + "properties": { + "_id": { + "bsonType": "objectId", + }, + "created_at": { + "bsonType": "date", + "description": "Date de création en base de données", + }, + "ml_id": { + "bsonType": "number", + "description": "Identifiant de la mission locale", + }, + "nom": { + "bsonType": "string", + "description": "Nom de la mission locale", + }, + "siret": { + "bsonType": "string", + "description": "N° SIRET", + "pattern": "^[0-9]{14}$", + }, + "type": { + "bsonType": "string", + }, + }, + "required": [ + "_id", + "created_at", + "type", + "nom", + "siret", + "ml_id", + ], + }, { "additionalProperties": false, "bsonType": "object", diff --git a/shared/models/data/organisations.model.ts b/shared/models/data/organisations.model.ts index feb31935b..1183a89b2 100644 --- a/shared/models/data/organisations.model.ts +++ b/shared/models/data/organisations.model.ts @@ -26,6 +26,13 @@ const zOrganisationBase = z.object({ created_at: z.date({ description: "Date de création en base de données" }), }); +const zOrganisationMissionLocaleCreate = z.object({ + type: z.literal("MISSION_LOCALE"), + nom: z.string({ description: "Nom de la mission locale" }), + siret: z.string({ description: "N° SIRET" }).regex(SIRET_REGEX), + ml_id: z.number({ description: "Identifiant de la mission locale" }), +}); + const zOrganisationOrganismeCreate = z.object({ type: z.literal("ORGANISME_FORMATION"), siret: z.string({ description: "N° SIRET" }).regex(SIRET_REGEX), @@ -75,6 +82,7 @@ const zOrganisationAdminCreate = z.object({ type: z.literal("ADMINISTRATEUR"), }); +const zOrganisationMissionLocale = zOrganisationBase.merge(zOrganisationMissionLocaleCreate); const zOrganisationOrganisme = zOrganisationBase.merge(zOrganisationOrganismeCreate); const zOrganisationReaseau = zOrganisationBase.merge(zOrganisationReaseauCreate); const zOrganisationRegional = zOrganisationBase.merge(zOrganisationRegionalCreate); @@ -85,6 +93,7 @@ const zOrganisationCarifOref = zOrganisationBase.merge(zOrganisationCarifOrefCre const zOrganisationAdmin = zOrganisationBase.merge(zOrganisationAdminCreate); const zOrganisation = z.discriminatedUnion("type", [ + zOrganisationMissionLocale, zOrganisationOrganisme, zOrganisationReaseau, zOrganisationRegional, @@ -96,6 +105,7 @@ const zOrganisation = z.discriminatedUnion("type", [ ]); export const zOrganisationCreate = z.discriminatedUnion("type", [ + zOrganisationMissionLocaleCreate, zOrganisationOrganismeCreate, zOrganisationReaseauCreate, zOrganisationRegionalCreate, @@ -105,6 +115,7 @@ export const zOrganisationCreate = z.discriminatedUnion("type", [ zOrganisationCarifOrefCreate, zOrganisationAdminCreate, ]); +export type IOrganisationMissionLocale = z.output; export type IOrganisationOrganismeFormation = z.output; @@ -131,6 +142,7 @@ export const TYPES_ORGANISATION = [ { key: "DDETS", nom: "DDETS" }, { key: "DRAAF", nom: "DRAAF" }, { key: "DREETS", nom: "DREETS" }, + { key: "MISSION_LOCALE", nom: "Mission locale" }, { key: "OPERATEUR_PUBLIC_NATIONAL", nom: "Opérateur public national" }, { key: "ORGANISME_FORMATION", nom: "Organisme de formation" }, { key: "TETE_DE_RESEAU", nom: "Tête de réseau" }, @@ -138,6 +150,9 @@ export const TYPES_ORGANISATION = [ export function getOrganisationLabel(organisation: IOrganisationCreate): string { switch (organisation.type) { + case "MISSION_LOCALE": + return `Mission locale ${organisation.nom}`; + case "ORGANISME_FORMATION": { return `OFA UAI : ${organisation.uai || "Inconnu"} - SIRET : ${organisation.siret}`; } From 6e10266f9a55baa21240bbca234f680126947334 Mon Sep 17 00:00:00 2001 From: Paul Gaucher Date: Thu, 12 Dec 2024 18:10:21 +0100 Subject: [PATCH 2/8] feat: ajour des api de missions locale --- .../actions/helpers/permissions-organisme.ts | 20 +++++++++++++ .../indicateurs/indicateurs-mission-locale.ts | 29 +++++++++++++++++++ .../indicateurs/indicateurs.actions.ts | 2 +- .../common/actions/organisations.actions.ts | 2 ++ server/src/http/middlewares/helpers.ts | 15 +++++++++- .../mission-locale/mission-locale.routes.ts | 18 ++++++++++++ server/src/http/server.ts | 12 ++++---- 7 files changed, 89 insertions(+), 9 deletions(-) create mode 100644 server/src/common/actions/indicateurs/indicateurs-mission-locale.ts create mode 100644 server/src/http/routes/organisations.routes/mission-locale/mission-locale.routes.ts diff --git a/server/src/common/actions/helpers/permissions-organisme.ts b/server/src/common/actions/helpers/permissions-organisme.ts index 947671c65..f353816c5 100644 --- a/server/src/common/actions/helpers/permissions-organisme.ts +++ b/server/src/common/actions/helpers/permissions-organisme.ts @@ -24,6 +24,26 @@ export type OrganismeWithPermissions = IOrganisme & { permissions: PermissionsOr }; export async function getAcl(organisation: IOrganisation): Promise { switch (organisation.type) { + // Tout a false pour les missions locales + // Cela assure aucun accès potentiels aux autres apis + + case "MISSION_LOCALE": { + return { + viewContacts: false, + infoTransmissionEffectifs: false, + indicateursEffectifs: false, + effectifsNominatifs: { + apprenant: false, + apprenti: false, + inscritSansContrat: false, + rupturant: false, + abandon: false, + inconnu: false, + }, + manageEffectifs: false, + configurerModeTransmission: false, + }; + } case "ORGANISME_FORMATION": { const userOrganisme = await organismesDb().findOne({ siret: organisation.siret, diff --git a/server/src/common/actions/indicateurs/indicateurs-mission-locale.ts b/server/src/common/actions/indicateurs/indicateurs-mission-locale.ts new file mode 100644 index 000000000..aaede3455 --- /dev/null +++ b/server/src/common/actions/indicateurs/indicateurs-mission-locale.ts @@ -0,0 +1,29 @@ +import { ObjectId } from "bson"; + +import { effectifsDb } from "@/common/model/collections"; + +import { DateFilters } from "../helpers/filters"; + +import { buildIndicateursEffectifsPipeline } from "./indicateurs.actions"; + +export const getEffectifIndicateursForMissionLocaleId = async (filters: DateFilters, missionLocaleId: string) => { + const aggregation = [ + { + $match: { + missionLocaleId: new ObjectId(missionLocaleId), + }, + }, + ...buildIndicateursEffectifsPipeline(null, filters.date), + { + $project: { + _id: 0, + inscrits: 1, + abandons: 1, + rupturants: 1, + }, + }, + ]; + + const indicateurs = await effectifsDb().aggregate(aggregation).next(); + return indicateurs ?? { inscrits: 0, abandons: 0, rupturants: 0 }; +}; diff --git a/server/src/common/actions/indicateurs/indicateurs.actions.ts b/server/src/common/actions/indicateurs/indicateurs.actions.ts index e051bc324..477768bfe 100644 --- a/server/src/common/actions/indicateurs/indicateurs.actions.ts +++ b/server/src/common/actions/indicateurs/indicateurs.actions.ts @@ -30,7 +30,7 @@ import { buildEffectifMongoFilters } from "./effectifs/effectifs-filters"; import { buildDECAFilter } from "./indicateurs-with-deca.actions"; import { buildOrganismeMongoFilters } from "./organismes/organismes-filters"; -function buildIndicateursEffectifsPipeline(groupBy: string | null, currentDate: Date) { +export function buildIndicateursEffectifsPipeline(groupBy: string | null, currentDate: Date) { return [ { $addFields: { diff --git a/server/src/common/actions/organisations.actions.ts b/server/src/common/actions/organisations.actions.ts index ca4ac0fb2..4f9e3dd7c 100644 --- a/server/src/common/actions/organisations.actions.ts +++ b/server/src/common/actions/organisations.actions.ts @@ -323,6 +323,8 @@ async function getInvitationById(ctx: AuthContext, invitationId: ObjectId): Prom export async function buildOrganisationLabel(organisationId: ObjectId): Promise { const organisation = await getOrganisationById(organisationId); switch (organisation.type) { + case "MISSION_LOCALE": + return `Mission locale ${organisation.nom}`; case "ORGANISME_FORMATION": { const organisme = await organismesDb().findOne({ siret: organisation.siret, diff --git a/server/src/http/middlewares/helpers.ts b/server/src/http/middlewares/helpers.ts index f743ea66d..bd6837877 100644 --- a/server/src/http/middlewares/helpers.ts +++ b/server/src/http/middlewares/helpers.ts @@ -1,7 +1,7 @@ import Boom from "boom"; import { NextFunction, Request, RequestHandler, Response } from "express"; import { ObjectId } from "mongodb"; -import { IEffectif, ORGANISATION_TYPE, PermissionOrganisme } from "shared"; +import { IEffectif, IOrganisationMissionLocale, ORGANISATION_TYPE, PermissionOrganisme } from "shared"; import { IEffectifDECA } from "shared/models/data/effectifsDECA.model"; import { getOrganismePermission } from "@/common/actions/helpers/permissions-organisme"; @@ -41,6 +41,19 @@ export function requireAdministrator(req: Request, _res: Response, next: NextFun next(); } +export function requireMissionLocale(req: Request, res: Response, next: NextFunction) { + const user = req.user as AuthContext; + ensureValidUser(user); + if (user.organisation.type !== "MISSION_LOCALE") { + throw Boom.forbidden("Accès non autorisé"); + } + + const orga = user.organisation as IOrganisationMissionLocale; + + res.locals.missionLocale = orga; + next(); +} + interface MyLocals { organismeId: ObjectId; } diff --git a/server/src/http/routes/organisations.routes/mission-locale/mission-locale.routes.ts b/server/src/http/routes/organisations.routes/mission-locale/mission-locale.routes.ts new file mode 100644 index 000000000..6da223048 --- /dev/null +++ b/server/src/http/routes/organisations.routes/mission-locale/mission-locale.routes.ts @@ -0,0 +1,18 @@ +import express from "express"; + +import { dateFiltersSchema } from "@/common/actions/helpers/filters"; +import { getEffectifIndicateursForMissionLocaleId } from "@/common/actions/indicateurs/indicateurs-mission-locale"; +import { validateFullZodObjectSchema } from "@/common/utils/validationUtils"; +import { returnResult } from "@/http/middlewares/helpers"; + +export default () => { + const router = express.Router(); + router.get("/indicateurs", returnResult(getIndicateursMissionLocale)); + return router; +}; + +const getIndicateursMissionLocale = async (req, { locals }) => { + const { missionLocale } = locals; + const filters = await validateFullZodObjectSchema(req.query, dateFiltersSchema); + return await getEffectifIndicateursForMissionLocaleId(filters, missionLocale._id); +}; diff --git a/server/src/http/server.ts b/server/src/http/server.ts index b7484782b..49663c5ff 100644 --- a/server/src/http/server.ts +++ b/server/src/http/server.ts @@ -40,7 +40,6 @@ import { effectifsFiltersTerritoireSchema, fullEffectifsFiltersSchema, } from "@/common/actions/helpers/filters"; -import { getOrganismePermission } from "@/common/actions/helpers/permissions-organisme"; import { getIndicateursNational } from "@/common/actions/indicateurs/indicateurs-national.actions"; import { getEffectifsNominatifsWithoutId, @@ -131,6 +130,7 @@ import errorMiddleware from "./middlewares/errorMiddleware"; import { requireAdministrator, requireEffectifOrganismePermission, + requireMissionLocale, requireOrganismePermission, returnResult, } from "./middlewares/helpers"; @@ -150,6 +150,7 @@ import organismesAdmin from "./routes/admin.routes/organismes.routes"; import transmissionRoutesAdmin from "./routes/admin.routes/transmissions.routes"; import usersAdmin from "./routes/admin.routes/users.routes"; import emails from "./routes/emails.routes"; +import missionLocaleAuthentRoutes from "./routes/organisations.routes/mission-locale/mission-locale.routes"; import missionLocalePublicRoutes from "./routes/public.routes/mission-locale.routes"; import affelnetRoutes from "./routes/specific.routes/affelnet.routes"; import dossierApprenantRouter from "./routes/specific.routes/dossiers-apprenants.routes"; @@ -367,7 +368,7 @@ function setupRoutes(app: Application) { await rejectInvitation(req.params.token); }) ) - .use("/api/v1/ml", missionLocalePublicRoutes()); + .use("/api/v1/mission-locale", missionLocalePublicRoutes()); /***************************************************************************** * Ancien mécanisme de login pour ERP (devrait être supprimé prochainement) * @@ -511,14 +512,10 @@ function setupRoutes(app: Application) { ) .get( "/indicateurs/effectifs/:type", + requireOrganismePermission("effectifsNominatifs"), returnResult(async (req, res) => { const filters = await validateFullZodObjectSchema(req.query, fullEffectifsFiltersSchema); const type = await z.enum(typesEffectifNominatif).parseAsync(req.params.type); - const permissions = await getOrganismePermission(req.user, res.locals.organismeId, "effectifsNominatifs"); - if (!permissions || (permissions instanceof Array && !permissions.includes(type))) { - throw Boom.forbidden("Permissions invalides"); - } - const { effectifsWithoutIds, ids } = await getEffectifsNominatifsWithoutId( req.user, filters, @@ -924,6 +921,7 @@ function setupRoutes(app: Application) { await resendInvitationEmail(req.user, req.params.invitationId); }) ) + .use("/mission-locale", requireMissionLocale, missionLocaleAuthentRoutes()) ); /******************************** From 2f59b5951023835ab8c2a5e66660203e3e4fe8e7 Mon Sep 17 00:00:00 2001 From: Paul Gaucher Date: Fri, 13 Dec 2024 10:47:49 +0100 Subject: [PATCH 3/8] fix: ajout de la route indicateur pour les missions locales --- .../actions/indicateurs/indicateurs-mission-locale.ts | 7 ++----- .../mission-locale/mission-locale.routes.ts | 2 +- shared/models/data/effectifs.model.ts | 6 ++++++ 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/server/src/common/actions/indicateurs/indicateurs-mission-locale.ts b/server/src/common/actions/indicateurs/indicateurs-mission-locale.ts index aaede3455..be98f645c 100644 --- a/server/src/common/actions/indicateurs/indicateurs-mission-locale.ts +++ b/server/src/common/actions/indicateurs/indicateurs-mission-locale.ts @@ -1,5 +1,3 @@ -import { ObjectId } from "bson"; - import { effectifsDb } from "@/common/model/collections"; import { DateFilters } from "../helpers/filters"; @@ -10,7 +8,7 @@ export const getEffectifIndicateursForMissionLocaleId = async (filters: DateFilt const aggregation = [ { $match: { - missionLocaleId: new ObjectId(missionLocaleId), + "_computed.missionLocale.id": missionLocaleId, }, }, ...buildIndicateursEffectifsPipeline(null, filters.date), @@ -23,7 +21,6 @@ export const getEffectifIndicateursForMissionLocaleId = async (filters: DateFilt }, }, ]; - - const indicateurs = await effectifsDb().aggregate(aggregation).next(); + const indicateurs = await effectifsDb().aggregate(aggregation).toArray(); return indicateurs ?? { inscrits: 0, abandons: 0, rupturants: 0 }; }; diff --git a/server/src/http/routes/organisations.routes/mission-locale/mission-locale.routes.ts b/server/src/http/routes/organisations.routes/mission-locale/mission-locale.routes.ts index 6da223048..258973ed7 100644 --- a/server/src/http/routes/organisations.routes/mission-locale/mission-locale.routes.ts +++ b/server/src/http/routes/organisations.routes/mission-locale/mission-locale.routes.ts @@ -14,5 +14,5 @@ export default () => { const getIndicateursMissionLocale = async (req, { locals }) => { const { missionLocale } = locals; const filters = await validateFullZodObjectSchema(req.query, dateFiltersSchema); - return await getEffectifIndicateursForMissionLocaleId(filters, missionLocale._id); + return await getEffectifIndicateursForMissionLocaleId(filters, missionLocale._id.toString()); }; diff --git a/shared/models/data/effectifs.model.ts b/shared/models/data/effectifs.model.ts index a90c4f318..537ab918b 100644 --- a/shared/models/data/effectifs.model.ts +++ b/shared/models/data/effectifs.model.ts @@ -87,6 +87,7 @@ const indexes: [IndexSpecification, CreateIndexesOptions][] = [ [{ "_computed.organisme.fiable": 1, annee_scolaire: 1 }, {}], [{ "_computed.formation.codes_rome": 1 }, {}], [{ "_computed.formation.opcos": 1 }, {}], + [{ "_computed.missionLocale.id": 1 }, {}], ]; const StatutApprenantEnum = zodEnumFromArray( @@ -103,6 +104,10 @@ const zEffectifComputedStatut = z.object({ ), }); +const zEffectifComputedMissionLocale = z.object({ + id: z.string(), +}); + export const zEffectif = z.object({ _id: zObjectId.describe("Identifiant MongoDB de l'effectif"), organisme_id: zObjectId.describe("Organisme id (lieu de formation de l'apprenant pour la v3)"), @@ -227,6 +232,7 @@ export const zEffectif = z.object({ .nullish(), // @TODO: nullish en attendant la migration et passage en nullable ensuite (migration: 20240305085918-effectifs-types.ts) statut: zEffectifComputedStatut.nullish(), + missionLocale: zEffectifComputedMissionLocale.nullish(), }, { description: "Propriétés calculées ou récupérées d'autres collections", From 926033514ec63d42ddfc3b7d5e6b02a21589bc59 Mon Sep 17 00:00:00 2001 From: Paul Gaucher Date: Fri, 13 Dec 2024 16:15:59 +0100 Subject: [PATCH 4/8] =?UTF-8?q?feat:=20ajout=20de=20l'api=20de=20r=C3=A9cu?= =?UTF-8?q?p=C3=A9ration=20des=20effectifs=20de=20la=20mission=20locale?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/common/actions/account.actions.ts | 18 ++++-- .../src/common/actions/effectifs.actions.ts | 32 +++++++++- .../actions/effectifs/effectifs.actions.ts | 60 +++++++++++++++++++ ...919-ajout-organisme-id-sur-organisation.ts | 8 +++ .../mission-locale/mission-locale.routes.ts | 7 +++ server/src/jobs/jobs.ts | 21 ++++++- shared/models/data/organisations.model.ts | 2 +- 7 files changed, 139 insertions(+), 9 deletions(-) create mode 100644 server/src/db/migrations/20241213131919-ajout-organisme-id-sur-organisation.ts diff --git a/server/src/common/actions/account.actions.ts b/server/src/common/actions/account.actions.ts index 7039f8fbe..49e1d793a 100644 --- a/server/src/common/actions/account.actions.ts +++ b/server/src/common/actions/account.actions.ts @@ -1,5 +1,5 @@ import Boom from "boom"; -import { STATUT_FIABILISATION_ORGANISME } from "shared"; +import { IOrganisme, STATUT_FIABILISATION_ORGANISME } from "shared"; import logger from "@/common/logger"; import { auditLogsDb, invitationsDb, organisationsDb, usersMigrationDb } from "@/common/model/collections"; @@ -23,6 +23,7 @@ export async function register(registration: RegistrationSchema): Promise<{ account_status: "PENDING_EMAIL_VALIDATION" | "CONFIRMED"; }> { const alreadyExists = await getUserByEmail(registration.user.email); + let registrationExtraData = {}; if (alreadyExists) { throw Boom.conflict("Cet email est déjà utilisé."); } @@ -30,9 +31,10 @@ export async function register(registration: RegistrationSchema): Promise<{ // on s'assure que l'organisme existe pour un OF const type = registration.organisation.type; if (type === "ORGANISME_FORMATION") { + let organisme: IOrganisme | undefined; const { uai, siret } = registration.organisation; try { - await getOrganismeByUAIAndSIRET(uai, siret); + organisme = await getOrganismeByUAIAndSIRET(uai, siret); } catch (err) { // si pas d'organisme en base (et donc le référentiel), on en crée un à partir depuis API entreprise logger.warn({ action: "register", uai, siret }, "organisme inconnu créé"); @@ -40,7 +42,7 @@ export async function register(registration: RegistrationSchema): Promise<{ const data = await getOrganismeInfosFromSiret(siret); // si aucun champ ferme fourni en entrée on récupère celui de l'organisme trouvé par son id - await createOrganisme({ + organisme = await createOrganisme({ ...data, ...(uai ? { uai } : {}), siret, @@ -48,11 +50,17 @@ export async function register(registration: RegistrationSchema): Promise<{ organismesFormateurs: [], organismesResponsables: [], }); + } finally { + registrationExtraData = { organisme_id: organisme?._id }; } } - const organisation = await organisationsDb().findOne(registration.organisation); - const organisationId = organisation ? organisation._id : await createOrganisation(registration.organisation); + const organisationId = organisation + ? organisation._id + : await createOrganisation({ + ...registration.organisation, + ...registrationExtraData, + }); const userId = await createUser(registration.user, organisationId); diff --git a/server/src/common/actions/effectifs.actions.ts b/server/src/common/actions/effectifs.actions.ts index 1c34923d4..40e29df11 100644 --- a/server/src/common/actions/effectifs.actions.ts +++ b/server/src/common/actions/effectifs.actions.ts @@ -2,7 +2,7 @@ import type { ICertification } from "api-alternance-sdk"; import Boom from "boom"; import { cloneDeep, isObject, merge, mergeWith, reduce, set, uniqBy } from "lodash-es"; import { ObjectId, type WithoutId } from "mongodb"; -import { IOpcos, IRncp } from "shared/models"; +import { IOpcos, IOrganisation, IRncp, IUsersMigration } from "shared/models"; import { IEffectif } from "shared/models/data/effectifs.model"; import { IEffectifDECA } from "shared/models/data/effectifsDECA.model"; import { IOrganisme } from "shared/models/data/organismes.model"; @@ -466,3 +466,33 @@ export const updateEffectifComputedFromRNCP = async (rncp: IRncp, opco: IOpcos) ) ); }; + +export const buildEffectifForMissionLocale = ( + effectif: IEffectif & { organisation: IOrganisation } & { cfa_users: IUsersMigration } +) => { + const result = { + _id: effectif._id, + apprenant: { + nom: effectif.apprenant.nom, + prenom: effectif.apprenant.prenom, + date_de_naissance: effectif.apprenant.date_de_naissance, + adresse: effectif.apprenant.adresse, + telephone: effectif.apprenant.telephone, + courriel: effectif.apprenant.courriel, + rqth: effectif.apprenant.rqth, + }, + parcours: effectif._computed?.statut, + formation: effectif.formation, + organisme: effectif._computed?.organisme, + user: { + nom: effectif.cfa_users?.nom, + fonction: effectif.cfa_users?.prenom, + email: effectif.cfa_users?.email, + telephone: effectif.cfa_users?.telephone, + }, + organisme_id: effectif.organisme_id, + annee_scolaire: effectif.annee_scolaire, + source: effectif.source, + }; + return result; +}; diff --git a/server/src/common/actions/effectifs/effectifs.actions.ts b/server/src/common/actions/effectifs/effectifs.actions.ts index 56eefb634..fa468ed6c 100644 --- a/server/src/common/actions/effectifs/effectifs.actions.ts +++ b/server/src/common/actions/effectifs/effectifs.actions.ts @@ -1,8 +1,12 @@ import { ObjectId } from "mongodb"; +import { IEffectif, IOrganisation, IUsersMigration } from "shared/models"; +import { getAnneesScolaireListFromDate } from "shared/utils"; import { organismeLookup } from "@/common/actions/helpers/filters"; import { effectifsDb } from "@/common/model/collections"; +import { buildEffectifForMissionLocale } from "../effectifs.actions"; + // Méthode de récupération de la liste des effectifs en base export const getAllEffectifs = async ( query = {}, @@ -132,3 +136,59 @@ export const getDetailedEffectifById = async (_id: any) => { return organisme; }; + +export const getPaginatedEffectifsByMissionLocaleId = async ( + missionLocaleId: string, + page: number = 1, + limit: number = 20 +) => { + const aggregation = [ + { + $match: { + "_computed.missionLocale.id": missionLocaleId, + annee_scolaire: { $in: getAnneesScolaireListFromDate(new Date()) }, + }, + }, + { + $lookup: { + from: "organisations", + localField: "organisme_id", + foreignField: "organisme_id", + as: "organisation", + }, + }, + { + $unwind: { + path: "$organisation", + preserveNullAndEmptyArrays: true, + }, + }, + { + $lookup: { + from: "usersMigration", + localField: "organisation._id", + foreignField: "organisation_id", + as: "cfa_users", + }, + }, + { + $unwind: { + path: "$cfa_users", + preserveNullAndEmptyArrays: true, + }, + }, + { + $facet: { + pagination: [{ $count: "total" }, { $addFields: { page, limit } }], + data: [{ $skip: (page - 1) * limit }, { $limit: limit }], + }, + }, + { $unwind: { path: "$pagination" } }, + ]; + const { pagination, data } = (await effectifsDb().aggregate(aggregation).next()) as { + pagination: any; + data: Array; + }; + const effectifs = data.map((effectif) => buildEffectifForMissionLocale(effectif)); + return { pagination, data: effectifs }; +}; diff --git a/server/src/db/migrations/20241213131919-ajout-organisme-id-sur-organisation.ts b/server/src/db/migrations/20241213131919-ajout-organisme-id-sur-organisation.ts new file mode 100644 index 000000000..be81e5bf6 --- /dev/null +++ b/server/src/db/migrations/20241213131919-ajout-organisme-id-sur-organisation.ts @@ -0,0 +1,8 @@ +import { addJob } from "job-processor"; + +export const up = async () => { + await addJob({ + name: "tmp:migration:organisation-organisme", + queued: true, + }); +}; diff --git a/server/src/http/routes/organisations.routes/mission-locale/mission-locale.routes.ts b/server/src/http/routes/organisations.routes/mission-locale/mission-locale.routes.ts index 258973ed7..a8d3f7a26 100644 --- a/server/src/http/routes/organisations.routes/mission-locale/mission-locale.routes.ts +++ b/server/src/http/routes/organisations.routes/mission-locale/mission-locale.routes.ts @@ -1,5 +1,6 @@ import express from "express"; +import { getPaginatedEffectifsByMissionLocaleId } from "@/common/actions/effectifs/effectifs.actions"; import { dateFiltersSchema } from "@/common/actions/helpers/filters"; import { getEffectifIndicateursForMissionLocaleId } from "@/common/actions/indicateurs/indicateurs-mission-locale"; import { validateFullZodObjectSchema } from "@/common/utils/validationUtils"; @@ -8,6 +9,7 @@ import { returnResult } from "@/http/middlewares/helpers"; export default () => { const router = express.Router(); router.get("/indicateurs", returnResult(getIndicateursMissionLocale)); + router.get("/effectifs", returnResult(getEffectifsMissionLocale)); return router; }; @@ -16,3 +18,8 @@ const getIndicateursMissionLocale = async (req, { locals }) => { const filters = await validateFullZodObjectSchema(req.query, dateFiltersSchema); return await getEffectifIndicateursForMissionLocaleId(filters, missionLocale._id.toString()); }; + +const getEffectifsMissionLocale = async (_req, { locals }) => { + const { missionLocale } = locals; + return await getPaginatedEffectifsByMissionLocaleId(missionLocale._id.toString()); +}; diff --git a/server/src/jobs/jobs.ts b/server/src/jobs/jobs.ts index 65ebf6f85..b1707a277 100644 --- a/server/src/jobs/jobs.ts +++ b/server/src/jobs/jobs.ts @@ -1,12 +1,12 @@ import { addJob, initJobProcessor } from "job-processor"; import { MongoError } from "mongodb"; import { MOTIF_SUPPRESSION } from "shared/constants"; -import type { IEffectif } from "shared/models"; +import type { IEffectif, IOrganisation, IOrganisationOrganismeFormation } from "shared/models"; import { getAnneesScolaireListFromDate } from "shared/utils"; import { softDeleteEffectif } from "@/common/actions/effectifs.actions"; import logger from "@/common/logger"; -import { effectifsDb } from "@/common/model/collections"; +import { effectifsDb, organisationsDb, organismesDb } from "@/common/model/collections"; import { createCollectionIndexes } from "@/common/model/indexes/createCollectionIndexes"; import { getDatabase } from "@/common/mongodb"; import config from "@/config"; @@ -393,6 +393,23 @@ export async function setupJobProcessor() { }, resumable: true, }, + "tmp:migration:organisation-organisme": { + handler: async () => { + const organisations: Array = await organisationsDb() + .find({ + type: "ORGANISME_FORMATION", + }) + .toArray(); + + for (let i = 0; i < organisations.length; i++) { + const orga = organisations[i] as IOrganisationOrganismeFormation; + const organisme = await organismesDb().findOne({ siret: orga.siret, uai: orga.uai ?? undefined }); + if (organisme) { + await organisationsDb().updateOne({ _id: orga._id }, { $set: { organisme_id: organisme._id } }); + } + } + }, + }, }, }); } diff --git a/shared/models/data/organisations.model.ts b/shared/models/data/organisations.model.ts index 1183a89b2..dddabb248 100644 --- a/shared/models/data/organisations.model.ts +++ b/shared/models/data/organisations.model.ts @@ -42,7 +42,7 @@ const zOrganisationOrganismeCreate = z.object({ }) .regex(UAI_REGEX) .nullable(), - organisme_id: z.string({ description: "Identifiant de l'organisme" }).optional(), + organisme_id: zObjectId.describe("Identifiant de l'organisme").optional(), }); const zOrganisationReaseauCreate = z.object({ From 501ce96492fb48b7eeb231595775ccef27b97854 Mon Sep 17 00:00:00 2001 From: Paul Gaucher Date: Tue, 17 Dec 2024 10:42:05 +0100 Subject: [PATCH 5/8] =?UTF-8?q?fix:=20mise=20a=20jour=20de=20la=20r=C3=A9c?= =?UTF-8?q?up=C3=A9ration=20des=20users=20du=20cfa?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/common/actions/effectifs/effectifs.actions.ts | 3 ++- server/src/jobs/jobs.ts | 5 ++++- shared/models/data/organisations.model.ts | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/server/src/common/actions/effectifs/effectifs.actions.ts b/server/src/common/actions/effectifs/effectifs.actions.ts index fa468ed6c..839f87c1f 100644 --- a/server/src/common/actions/effectifs/effectifs.actions.ts +++ b/server/src/common/actions/effectifs/effectifs.actions.ts @@ -149,10 +149,11 @@ export const getPaginatedEffectifsByMissionLocaleId = async ( annee_scolaire: { $in: getAnneesScolaireListFromDate(new Date()) }, }, }, + { $addFields: { stringify_organisme_id: { $toString: "$organisme_id" } } }, { $lookup: { from: "organisations", - localField: "organisme_id", + localField: "stringify_organisme_id", foreignField: "organisme_id", as: "organisation", }, diff --git a/server/src/jobs/jobs.ts b/server/src/jobs/jobs.ts index b1707a277..38400431f 100644 --- a/server/src/jobs/jobs.ts +++ b/server/src/jobs/jobs.ts @@ -405,7 +405,10 @@ export async function setupJobProcessor() { const orga = organisations[i] as IOrganisationOrganismeFormation; const organisme = await organismesDb().findOne({ siret: orga.siret, uai: orga.uai ?? undefined }); if (organisme) { - await organisationsDb().updateOne({ _id: orga._id }, { $set: { organisme_id: organisme._id } }); + await organisationsDb().updateOne( + { _id: orga._id }, + { $set: { organisme_id: organisme._id.toString() } } + ); } } }, diff --git a/shared/models/data/organisations.model.ts b/shared/models/data/organisations.model.ts index dddabb248..1183a89b2 100644 --- a/shared/models/data/organisations.model.ts +++ b/shared/models/data/organisations.model.ts @@ -42,7 +42,7 @@ const zOrganisationOrganismeCreate = z.object({ }) .regex(UAI_REGEX) .nullable(), - organisme_id: zObjectId.describe("Identifiant de l'organisme").optional(), + organisme_id: z.string({ description: "Identifiant de l'organisme" }).optional(), }); const zOrganisationReaseauCreate = z.object({ From 2b3404eca783688f765a21d33193206ea30fcfdd Mon Sep 17 00:00:00 2001 From: Paul Gaucher Date: Tue, 17 Dec 2024 11:28:01 +0100 Subject: [PATCH 6/8] fix: ajout de valeur par defaut sur le type d'organisation --- ui/modules/dashboard/DashboardOrganisme.tsx | 2 ++ ui/modules/dashboard/DashboardTransverse.tsx | 2 ++ ui/modules/indicateurs/permissions-onglet-graphique.ts | 3 +++ 3 files changed, 7 insertions(+) diff --git a/ui/modules/dashboard/DashboardOrganisme.tsx b/ui/modules/dashboard/DashboardOrganisme.tsx index ee2c304be..d3513545c 100644 --- a/ui/modules/dashboard/DashboardOrganisme.tsx +++ b/ui/modules/dashboard/DashboardOrganisme.tsx @@ -1065,5 +1065,7 @@ function getIndicateursEffectifsPartielsMessage(ctx: AuthContext, organisme: Org return false; case "ADMINISTRATEUR": return false; + default: + return false; } } diff --git a/ui/modules/dashboard/DashboardTransverse.tsx b/ui/modules/dashboard/DashboardTransverse.tsx index 8a27e6756..05ba78bfa 100644 --- a/ui/modules/dashboard/DashboardTransverse.tsx +++ b/ui/modules/dashboard/DashboardTransverse.tsx @@ -36,6 +36,8 @@ function getPerimetreDescription(organisation: IOrganisationJson | null): string } switch (organisation.type) { + case "MISSION_LOCALE": + return `Votre périmètre correspond à la mission locale ${organisation.nom}`; case "ORGANISME_FORMATION": { return "Votre périmètre correspond à votre organisme et vos organismes formateurs"; } diff --git a/ui/modules/indicateurs/permissions-onglet-graphique.ts b/ui/modules/indicateurs/permissions-onglet-graphique.ts index 14a8134ec..9454b8a5b 100644 --- a/ui/modules/indicateurs/permissions-onglet-graphique.ts +++ b/ui/modules/indicateurs/permissions-onglet-graphique.ts @@ -17,5 +17,8 @@ export function canViewOngletIndicateursVueGraphique(organisationType: IOrganisa case "CARIF_OREF_NATIONAL": case "ADMINISTRATEUR": return true; + + default: + return false; } } From 7ca03be7c85e588aa43561423c976f88fcc24aa0 Mon Sep 17 00:00:00 2001 From: Paul Gaucher Date: Tue, 17 Dec 2024 11:40:25 +0100 Subject: [PATCH 7/8] fix: mise a jour du snapshot --- .../validationSchema.test.ts.snap | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/server/src/common/mongodb/__snapshots__/validationSchema.test.ts.snap b/server/src/common/mongodb/__snapshots__/validationSchema.test.ts.snap index 7fdcdb3f0..578011e87 100644 --- a/server/src/common/mongodb/__snapshots__/validationSchema.test.ts.snap +++ b/server/src/common/mongodb/__snapshots__/validationSchema.test.ts.snap @@ -162,6 +162,25 @@ exports[`validation-schema > should create validation schema for effectifs > eff }, ], }, + "missionLocale": { + "anyOf": [ + { + "additionalProperties": false, + "bsonType": "object", + "properties": { + "id": { + "bsonType": "string", + }, + }, + "required": [ + "id", + ], + }, + { + "bsonType": "null", + }, + ], + }, "organisme": { "anyOf": [ { @@ -2700,6 +2719,25 @@ exports[`validation-schema > should create validation schema for effectifsArchiv }, ], }, + "missionLocale": { + "anyOf": [ + { + "additionalProperties": false, + "bsonType": "object", + "properties": { + "id": { + "bsonType": "string", + }, + }, + "required": [ + "id", + ], + }, + { + "bsonType": "null", + }, + ], + }, "organisme": { "anyOf": [ { From 1b6ade70ee5d11190da2365643caf7296df5b9e1 Mon Sep 17 00:00:00 2001 From: Paul Gaucher Date: Tue, 17 Dec 2024 12:04:26 +0100 Subject: [PATCH 8/8] fix: mise a jour du type de organisme_id a la creation --- server/src/common/actions/account.actions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/common/actions/account.actions.ts b/server/src/common/actions/account.actions.ts index 49e1d793a..320f5d6ce 100644 --- a/server/src/common/actions/account.actions.ts +++ b/server/src/common/actions/account.actions.ts @@ -23,7 +23,7 @@ export async function register(registration: RegistrationSchema): Promise<{ account_status: "PENDING_EMAIL_VALIDATION" | "CONFIRMED"; }> { const alreadyExists = await getUserByEmail(registration.user.email); - let registrationExtraData = {}; + let registrationExtraData: { organisme_id?: string } = {}; if (alreadyExists) { throw Boom.conflict("Cet email est déjà utilisé."); } @@ -51,7 +51,7 @@ export async function register(registration: RegistrationSchema): Promise<{ organismesResponsables: [], }); } finally { - registrationExtraData = { organisme_id: organisme?._id }; + registrationExtraData = { organisme_id: organisme?._id.toString() }; } } const organisation = await organisationsDb().findOne(registration.organisation);