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 @@
+
+
+
+ Fiche territoire
+ {{ titleTerritoire }}
+
+
+
+
+
+
+
+
+
Enregistrer
+
+
+
Retour
+
+
+
+
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 @@
+
+
+
Liste des territoires
+
+
+
+
+
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;
+ `);
+};