diff --git a/packages/backend/src/app.js b/packages/backend/src/app.js index 351a4597a..6b4201981 100644 --- a/packages/backend/src/app.js +++ b/packages/backend/src/app.js @@ -93,6 +93,7 @@ app.use(`/geo`, routes.geo); // TODO(eig): unhide when ok //app.use(`/eig`, routes.eig); app.use(`/message`, routes.message); +app.use(`/territoire`, routes.territoire); app.use(`/healthz`, routes.healthz); if (config.sentry.environment !== "production") { diff --git a/packages/backend/src/controllers/bo-user/index.js b/packages/backend/src/controllers/bo-user/index.js index ef93e3172..5a0266f02 100644 --- a/packages/backend/src/controllers/bo-user/index.js +++ b/packages/backend/src/controllers/bo-user/index.js @@ -1,4 +1,5 @@ module.exports.list = require("./list"); +module.exports.listUsersTerritoire = require("./list-users-territoire"); module.exports.getExtract = require("./getExtract"); module.exports.getOne = require("./get-one"); module.exports.getMe = require("./get-me"); diff --git a/packages/backend/src/controllers/bo-user/list-users-territoire.js b/packages/backend/src/controllers/bo-user/list-users-territoire.js new file mode 100644 index 000000000..ded3d1732 --- /dev/null +++ b/packages/backend/src/controllers/bo-user/list-users-territoire.js @@ -0,0 +1,17 @@ +const BoUser = require("../../services/BoUser"); +const logger = require("../../utils/logger"); + +const log = logger(module.filename); + +module.exports = async function listUsersTerritoire(req, res, next) { + log.i("IN"); + const territoireCode = req.params.territoireCode; + try { + const result = await BoUser.readTerritoires(territoireCode); + log.d(result); + return res.status(200).json(result); + } catch (error) { + log.w("DONE with error"); + return next(error); + } +}; diff --git a/packages/backend/src/controllers/index.js b/packages/backend/src/controllers/index.js index 54e6651d4..fcdec79fb 100644 --- a/packages/backend/src/controllers/index.js +++ b/packages/backend/src/controllers/index.js @@ -10,3 +10,4 @@ module.exports.demandeSejourController = require("./demandeSejour"); module.exports.hebergementController = require("./hebergement"); module.exports.eigController = require("./eig"); module.exports.messageController = require("./message"); +module.exports.territoireController = require("./territoire"); diff --git a/packages/backend/src/controllers/territoire/get-one.js b/packages/backend/src/controllers/territoire/get-one.js new file mode 100644 index 000000000..cb5eab2ef --- /dev/null +++ b/packages/backend/src/controllers/territoire/get-one.js @@ -0,0 +1,30 @@ +const AppError = require("../../utils/error"); +const Territoire = require("../../services/Territoire"); + +const logger = require("../../utils/logger"); + +const log = logger(module.filename); + +module.exports = async function getOne(req, res, next) { + log.i("IN"); + const { idTerritoire } = req.params; + + if (!idTerritoire || isNaN(idTerritoire)) { + return next( + new AppError("Paramètre manquant id", { + statusCode: 400, + }), + ); + } + const territoire = await Territoire.readOne(idTerritoire); + if (!territoire) { + log.w("DONE with error"); + return next( + new AppError("Territoire non trouvée", { + statusCode: 404, + }), + ); + } + log.i("DONE"); + return res.json({ territoire }); +}; diff --git a/packages/backend/src/controllers/territoire/index.js b/packages/backend/src/controllers/territoire/index.js new file mode 100644 index 000000000..f2280d6a5 --- /dev/null +++ b/packages/backend/src/controllers/territoire/index.js @@ -0,0 +1,3 @@ +module.exports.getOne = require("./get-one"); +module.exports.list = require("./list"); +module.exports.update = require("./update"); diff --git a/packages/backend/src/controllers/territoire/list.js b/packages/backend/src/controllers/territoire/list.js new file mode 100644 index 000000000..9cfe767ce --- /dev/null +++ b/packages/backend/src/controllers/territoire/list.js @@ -0,0 +1,12 @@ +const Territoire = require("../../services/Territoire"); + +const logger = require("../../utils/logger"); + +const log = logger(module.filename); + +module.exports = async function list(_req, res) { + log.i("IN"); + const territoires = await Territoire.fetch(); + log.i("DONE"); + return res.json({ territoires }); +}; diff --git a/packages/backend/src/controllers/territoire/update.js b/packages/backend/src/controllers/territoire/update.js new file mode 100644 index 000000000..de83ad167 --- /dev/null +++ b/packages/backend/src/controllers/territoire/update.js @@ -0,0 +1,35 @@ +const AppError = require("../../utils/error"); +const Territoire = require("../../services/Territoire"); +const logger = require("../../utils/logger"); + +const log = logger(module.filename); + +module.exports = async function update(req, res, next) { + log.i("IN"); + const { id } = req.params; + const { territoire, parent } = req.body; + const { territoireCode } = req.decoded; + if (!id || !territoire || !parent || !territoireCode) { + return next( + new AppError("Paramètre manquant", { + statusCode: 400, + }), + ); + } + let response; + if ( + territoireCode === "FRA" || + territoireCode === territoire || + territoireCode === parent + ) { + response = await Territoire.update(id, req.body); + } else { + return next( + new AppError("Bad request unauthorized", { + statusCode: 401, + }), + ); + } + log.i("DONE"); + return res.json({ response }); +}; diff --git a/packages/backend/src/routes/bo-user.js b/packages/backend/src/routes/bo-user.js index cdce30363..f54aa6c8b 100644 --- a/packages/backend/src/routes/bo-user.js +++ b/packages/backend/src/routes/bo-user.js @@ -20,6 +20,12 @@ router.get( ); // Gère une connexion via mot de passe. router.get("/me", BOcheckJWT, BOUserController.getMe); +// Liste des utilisateurs BO Liés à un territoire et sous territoires +router.get( + "/territoires/:territoireCode", + BOcheckJWT, + BOUserController.listUsersTerritoire, +); // Renvoie les informations liées à l'utilisateur router.get("/:userId", BOcheckJWT, BOcheckRoleCompte, BOUserController.getOne); // Mise à jour de mes informations diff --git a/packages/backend/src/routes/index.js b/packages/backend/src/routes/index.js index 47c0e881e..d55f8fc92 100644 --- a/packages/backend/src/routes/index.js +++ b/packages/backend/src/routes/index.js @@ -11,6 +11,7 @@ module.exports.sejour = require("./sejour"); module.exports.hebergement = require("./hebergement"); module.exports.agrement = require("./agrement"); module.exports.message = require("./message"); +module.exports.territoire = require("./territoire"); module.exports.documents = require("./documents"); diff --git a/packages/backend/src/routes/territoire.js b/packages/backend/src/routes/territoire.js new file mode 100644 index 000000000..3a909bd7e --- /dev/null +++ b/packages/backend/src/routes/territoire.js @@ -0,0 +1,12 @@ +const express = require("express"); + +const router = express.Router(); + +const territoireController = require("../controllers/territoire"); +const BOcheckJWT = require("../middlewares/bo-check-JWT"); + +router.get("/list", BOcheckJWT, territoireController.list); +router.get("/get-one/:idTerritoire", BOcheckJWT, territoireController.getOne); +router.put("/:id", BOcheckJWT, territoireController.update); + +module.exports = router; diff --git a/packages/backend/src/services/BoUser.js b/packages/backend/src/services/BoUser.js index 4e330e3e3..61597e9cd 100644 --- a/packages/backend/src/services/BoUser.js +++ b/packages/backend/src/services/BoUser.js @@ -174,6 +174,21 @@ ${additionalParamsQuery} `, additionalParams, ], + listUsersTerritoire: ` + SELECT + us.mail as email, + us.blocked as "isBlocked", + us.nom as nom, + us.prenom as prenom, + us.ter_code as "territoireCode", + terp.label as "territoireLibelle" + FROM geo.territoires terp + LEFT JOIN geo.territoires ters ON ters.parent_code = terp.code + INNER JOIN back.users us ON (terp.code = us.ter_code OR ters.code = us.ter_code) AND us.ter_code <> 'FRA' + WHERE terp.code = $1 AND us.validated = true AND us.deleted = false + GROUP BY 1,2,3,4,5,6 + ORDER BY nom + `, login: ` SELECT us.id as id, @@ -407,7 +422,6 @@ module.exports.read = async ( let searchQuery = ""; let territoireSearchParamId = ""; const searchParams = []; - // Search management if (search?.nom && search.nom.length) { searchQuery += ` AND unaccent(us.nom) ILIKE unaccent($${searchParams.length + 1})\n`; @@ -424,16 +438,31 @@ module.exports.read = async ( if ( search?.territoire && search?.territoire !== "FRA" && - search.territoire.length + search?.territoire.length ) { searchQuery += ` AND ( unaccent(ter.label) ILIKE unaccent($${searchParams.length + 1}) - OR code ILIKE $${searchParams.length + 1} + OR ter.code ILIKE $${searchParams.length + 1} OR ter.parent_code IN (SELECT code FROM matched_elements) )\n`; territoireSearchParamId = searchParams.length + 1; searchParams.push(`%${search.territoire}%`); } + + // Filtre utilisé pour rechercher tous les users pour un territoire et ses enfants + if ( + search?.territoireParent && + search?.territoireParent !== "FRA" && + search.territoireParent.length + ) { + searchQuery += ` AND ( + ter.code = $${searchParams.length + 1} + OR ter.parent_code = $${searchParams.length + 1} + )\n`; + territoireSearchParamId = searchParams.length + 1; + searchParams.push(`%${search.territoireParent}%`); + } + if (search?.statut === "validated") { searchQuery += `AND us.validated = true\n`; } @@ -495,6 +524,7 @@ module.exports.read = async ( WHERE unaccent(label) ILIKE unaccent($${territoireSearchParamId}) OR code ILIKE $${territoireSearchParamId} )\n`; + const response = await pool.query( `${territoireSearchParamId ? territoirePreQuery : ""} ${queryPrepared[0]}`, @@ -515,6 +545,18 @@ module.exports.read = async ( }; }; +module.exports.readTerritoires = async (territoireParent) => { + log.i("readTerritoires - IN", territoireParent); + const userTerritoire = await pool.query(query.listUsersTerritoire, [ + territoireParent, + ]); + log.i("readTerritoires - DONE"); + return { + total: userTerritoire.rows.count, + users: userTerritoire.rows, + }; +}; + module.exports.readOne = async (id, territoireCode) => { log.i("readOne - IN", { id }); diff --git a/packages/backend/src/services/Territoire.js b/packages/backend/src/services/Territoire.js new file mode 100644 index 000000000..7bb9eab6d --- /dev/null +++ b/packages/backend/src/services/Territoire.js @@ -0,0 +1,98 @@ +const AppError = require("../utils/error"); +const logger = require("../utils/logger"); +const pool = require("../utils/pgpool").getPool(); + +const log = logger(module.filename); + +const query = { + getOne: ` + select + fte.id AS territoire_id, + CASE (ter.code ~ '[0-9]') + WHEN true THEN 'DEP' + ELSE 'REG' + END AS type, + ter.code AS value, + ter.label AS text, + ter.parent_code AS parent, + fte.service_mail AS service_mail, + fte.service_telephone AS service_telephone, + fte.corresp_vao_nom AS corresp_vao_nom, + fte.corresp_vao_prenom AS corresp_vao_prenom, + fte.edited_at AS edited_at + FROM back.fiche_territoire fte + INNER JOIN geo.territoires ter ON fte.ter_code = ter.code + WHERE fte.id = $1`, + select: ` + select + fte.id AS territoire_id, + CASE (ter.code ~ '[0-9]') + WHEN true THEN 'DEP' + ELSE 'REG' + END AS type, + ter.code AS value, + ter.label AS text, + fte.service_telephone AS service_telephone, + fte.corresp_vao_nom AS corresp_vao_nom, + fte.corresp_vao_prenom AS corresp_vao_prenom, + fte.service_mail as service_mail, + COUNT(distinct(usr.id)) as nbusersbo + FROM geo.territoires ter + INNER JOIN back.fiche_territoire fte ON fte.ter_code = ter.code + LEFT JOIN back.users usr ON usr.ter_code = ter.code + WHERE code <> 'FRA' + GROUP BY territoire_id,type,value,text,service_telephone,corresp_vao_nom,corresp_vao_prenom,service_mail + ORDER BY type, text ASC`, + update: ` + UPDATE back.fiche_territoire + SET + corresp_vao_nom = $2, + corresp_vao_prenom = $3, + service_mail = $4, + service_telephone = $5, + edited_at = NOW() + WHERE + id = $1 + RETURNING * + `, +}; + +module.exports.fetch = async (criterias = {}) => { + log.i("fetch - IN"); + const { rows } = await pool.query(query.select); + + return rows.filter((territoire) => { + return Object.entries(criterias).every( + ([key, value]) => territoire[key] == value, + ); + }); +}; + +module.exports.readOne = async (idTerritoire) => { + log.i("fetch - IN"); + const { rows } = await pool.query(query.getOne, [idTerritoire]); + log.i("fetch - DONE"); + return rows[0]; +}; + +module.exports.update = async (id, { nom, prenom, email, telephone }) => { + log.i("update - IN", { id }); + + const response = await pool.query(query.update, [ + id, + nom, + prenom, + email, + telephone, + ]); + + if (response.rows.Count === 0) { + log.d("update - DONE - fiche territoire inexistante"); + throw new AppError("Fiche territoire déjà inexistant", { + name: "FicheTerritoireNotFound", + }); + } + + log.i("update - DONE"); + return { territoire: response.rows[0] }; +}; diff --git a/packages/frontend-bo/src/components/user/Compte.vue b/packages/frontend-bo/src/components/user/Compte.vue index 4cb009fa9..328649e72 100644 --- a/packages/frontend-bo/src/components/user/Compte.vue +++ b/packages/frontend-bo/src/components/user/Compte.vue @@ -602,7 +602,6 @@ const closeModal = () => (popUpParams.value = null); const modalOpenCounter = ref(0); const openModal = (p) => { - console.log(p); modalOpenCounter.value++; popUpParams.value = { cb: () => { diff --git a/packages/frontend-bo/src/composables/useMenuNavItem.js b/packages/frontend-bo/src/composables/useMenuNavItem.js index b5ef9c3d2..ee3021c94 100644 --- a/packages/frontend-bo/src/composables/useMenuNavItem.js +++ b/packages/frontend-bo/src/composables/useMenuNavItem.js @@ -62,6 +62,10 @@ export const useMenuNavItems = () => { text: "Organismes", to: "/organismes/liste", }, + { + text: "Territoires", + to: "/territoires/liste", + }, ...(roles.includes("DemandeSejour_Lecture") || roles.includes("DemandeSejour_Ecriture") ? [ diff --git a/packages/frontend-bo/src/pages/territoires/[[territoireId]].vue b/packages/frontend-bo/src/pages/territoires/[[territoireId]].vue new file mode 100644 index 000000000..8773774da --- /dev/null +++ b/packages/frontend-bo/src/pages/territoires/[[territoireId]].vue @@ -0,0 +1,267 @@ + + diff --git a/packages/frontend-bo/src/pages/territoires/liste.vue b/packages/frontend-bo/src/pages/territoires/liste.vue new file mode 100644 index 000000000..464db55b4 --- /dev/null +++ b/packages/frontend-bo/src/pages/territoires/liste.vue @@ -0,0 +1,88 @@ + + + diff --git a/packages/frontend-bo/src/stores/territoire.js b/packages/frontend-bo/src/stores/territoire.js new file mode 100644 index 000000000..01151cd04 --- /dev/null +++ b/packages/frontend-bo/src/stores/territoire.js @@ -0,0 +1,72 @@ +import { defineStore } from "pinia"; +import { $fetchBackend, logger } from "#imports"; + +const log = logger("/store/territoire"); + +export const useTerritoireStore = defineStore("territoire", { + state: () => ({ + territoires: [], + territoire: {}, + }), + actions: { + async fetch() { + log.i("fetch - IN"); + try { + const { territoires } = await $fetchBackend("/territoire/list", { + method: "GET", + credentials: "include", + }); + if (territoires) { + this.territoires = territoires.map((territoire) => ({ + territoireId: territoire.territoire_id, + text: territoire.text, + value: territoire.value, + parent: territoire.parent, + typeTerritoire: territoire.type, + serviceMail: territoire.service_mail, + serviceTelephone: territoire.service_telephone, + correspVaoNom: territoire.corresp_vao_nom, + correspVaoPrenom: territoire.corresp_vao_prenom, + nbUsersBO: territoire.nbusersbo, + })); + } + } catch (err) { + log.w("fetch - DONE with error", err); + throw err; + } + log.i("fetch - DONE"); + }, + async get(idTerritoire) { + log.i("get - IN"); + try { + const row = await $fetchBackend(`/territoire/get-one/${idTerritoire}`, { + method: "GET", + credentials: "include", + }); + this.territoire = row.territoire; + log.i("get - DONE"); + } catch (err) { + log.w("get - DONE with error", err); + throw err; + } + }, + async update(idTerritoire, params) { + log.i("update - IN"); + try { + const response = await $fetchBackend(`/territoire/${idTerritoire}`, { + credentials: "include", + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: params, + }); + log.i("update - DONE"); + return response; + } catch (err) { + log.w("update - DONE with error", err); + throw err; + } + }, + }, +}); diff --git a/packages/frontend-bo/src/stores/user.js b/packages/frontend-bo/src/stores/user.js index 7d23dcbe5..dc36e0d22 100644 --- a/packages/frontend-bo/src/stores/user.js +++ b/packages/frontend-bo/src/stores/user.js @@ -8,6 +8,7 @@ export const useUserStore = defineStore("user", { user: null, userFO: null, users: [], + usersTerritoire: [], usersFO: [], total: 0, totalUsersFO: 0, @@ -67,7 +68,33 @@ export const useUserStore = defineStore("user", { log.w("fetchUsers - Erreur", { error }); } }, + async fetchUsersTerritoire(territoireCode) { + log.i("fetchUsers - IN"); + try { + // Appel du back pour la liste des utilisateurs + const { users, total } = await $fetchBackend( + `/bo-user/territoires/${territoireCode}`, + { + credentials: "include", + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }, + ); + log.d("fetchUsers - réponse", { users, total }); + this.users = users; + this.total = total; + log.i("fetchUsers - DONE"); + } catch (error) { + // Retour vide en cas d'erreur + this.users = []; + this.total = 0; + log.w("fetchUsers - Erreur", { error }); + throw error; + } + }, async exportUsers() { log.i("exportUsers - IN"); try { diff --git a/packages/frontend-bo/src/utils/territoires.js b/packages/frontend-bo/src/utils/territoires.js new file mode 100644 index 000000000..621144a55 --- /dev/null +++ b/packages/frontend-bo/src/utils/territoires.js @@ -0,0 +1,13 @@ +import { emailSchema } from "@vao/shared/src/schema/email"; +import { prenomSchema } from "@vao/shared/src/schema/prenom"; +import { nomSchema } from "@vao/shared/src/schema/nom"; +import { telephoneSchema } from "@vao/shared/src/schema/telephone"; + +const FicheTerritoireSchema = { + email: emailSchema(), + nom: nomSchema(), + prenom: prenomSchema(), + telephone: telephoneSchema(), +}; + +export default { FicheTerritoireSchema }; diff --git a/packages/migrations/src/migrations/20241021142316_back.__add__fiche_territoire.js b/packages/migrations/src/migrations/20241021142316_back.__add__fiche_territoire.js new file mode 100644 index 000000000..28e3d061e --- /dev/null +++ b/packages/migrations/src/migrations/20241021142316_back.__add__fiche_territoire.js @@ -0,0 +1,32 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.up = function (knex) { + return knex.raw(` + CREATE TABLE back.fiche_territoire ( + id SERIAL NOT NULL, + ter_code VARCHAR(4) NOT NULL REFERENCES geo.territoires(code), + service_mail VARCHAR(320) NULL, + service_telephone VARCHAR(12) NULL, + corresp_vao_nom VARCHAR(128) NULL, + corresp_vao_prenom VARCHAR(128) NULL, + edited_at TIMESTAMP DEFAULT current_timestamp NOT NULL, + CONSTRAINT pk_back_fiche_territoire PRIMARY KEY (id) + ); + GRANT ALL ON TABLE back.fiche_territoire TO vao_u; + INSERT INTO back.fiche_territoire (ter_code, edited_at) + SELECT code, NOW() + FROM geo.territoires; + `); +}; + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = function (knex) { + return knex.raw(` + DROP TABLE back.fiche_territoire; + `); +};