Checkpoint: v4 : Ajout pages Mes Solutions Numériques et Solutions Logicielles, onglets Import Établissements et Import Contacts avec CSV dans Admin, correction moteur de recherche (filtres solutionId/editeurId/blocFonctionnelId), CGU réaffichée à chaque session. 33 tests passés, 0 erreur TypeScript.

This commit is contained in:
Manus
2026-04-16 12:20:42 -04:00
parent b86977eae4
commit a86806fbb7
9 changed files with 1051 additions and 22 deletions

View File

@@ -1,4 +1,4 @@
import { and, desc, eq, ilike, like, or, sql } from "drizzle-orm";
import { and, desc, eq, ilike, inArray, like, or, sql } from "drizzle-orm";
import { drizzle } from "drizzle-orm/mysql2";
import {
InsertUser,
@@ -177,19 +177,40 @@ export async function searchEtablissements(filters: {
}) {
const db = await getDb();
if (!db) return [];
const conditions = [];
const conditions: any[] = [];
// Visibilité : si pas gestionnaire, on ne montre que les fiches "tous"
if (filters.sonumRole !== "gestionnaire") {
conditions.push(eq(etablissements.visibilite, "tous"));
}
if (filters.region) conditions.push(eq(etablissements.region, filters.region));
if (filters.typeActivite) conditions.push(eq(etablissements.typeActivite, filters.typeActivite));
if (filters.tailleEffectifs) conditions.push(eq(etablissements.tailleEffectifs, filters.tailleEffectifs));
let query = db
// Filtres sur les logiciels (nécessitent une jointure)
const needsJoin = filters.solutionId || filters.editeurId || filters.blocFonctionnelId || filters.etatDeploiement;
if (needsJoin) {
// Filtres sur la table logiciels_etablissements
const leConditions: any[] = [eq(logicielsEtablissements.etablissementId, etablissements.id)];
if (filters.solutionId) leConditions.push(eq(logicielsEtablissements.solutionId, filters.solutionId));
if (filters.etatDeploiement) leConditions.push(eq(logicielsEtablissements.etatDeploiement, filters.etatDeploiement as any));
// Filtres sur les solutions (editeurId, blocFonctionnelId)
const solConditions: any[] = [eq(solutions.id, logicielsEtablissements.solutionId)];
if (filters.editeurId) solConditions.push(eq(solutions.editeurId, filters.editeurId));
if (filters.blocFonctionnelId) solConditions.push(eq(solutions.blocFonctionnelId, filters.blocFonctionnelId));
const subquery = db
.select({ etablissementId: logicielsEtablissements.etablissementId })
.from(logicielsEtablissements)
.innerJoin(solutions, and(...solConditions))
.where(and(...leConditions));
conditions.push(inArray(etablissements.id, subquery));
}
const result = await db
.select({
id: etablissements.id,
finess: etablissements.finess,
@@ -205,8 +226,7 @@ export async function searchEtablissements(filters: {
.from(etablissements)
.where(conditions.length > 0 ? and(...conditions) : undefined)
.orderBy(etablissements.nom);
return query;
return result;
}
// ─── Logiciels par Établissement ─────────────────────────────────────────────
@@ -595,3 +615,140 @@ export async function getAllUsersWithAffectations() {
hasLocalCredentials: false, // sera enrichi côté router si besoin
}));
}
// ─── Mes Solutions Numériques ─────────────────────────────────────────────────
/**
* Retourne toutes les solutions utilisées par les établissements dont l'utilisateur est référent/adhérent,
* groupées par solution avec la liste des établissements équipés.
*/
export async function getMesSolutionsGroupees(userId: number, sonumRole: string) {
const db = await getDb();
if (!db) return [];
// Récupérer les établissements accessibles selon le rôle
let etablissementIds: number[] = [];
if (sonumRole === "gestionnaire") {
const all = await db.select({ id: etablissements.id }).from(etablissements);
etablissementIds = all.map((e) => e.id);
} else if (sonumRole === "adherent") {
etablissementIds = await getAffectationsByUser(userId);
} else {
// référent : établissements dont il est référent
const refs = await db
.select({ id: etablissements.id })
.from(etablissements)
.where(eq(etablissements.referentId, userId));
etablissementIds = refs.map((e) => e.id);
}
if (etablissementIds.length === 0) return [];
const rows = await db
.select({
solutionId: solutions.id,
solutionNom: solutions.nom,
editeurNom: editeurs.nom,
blocFonctionnelNom: blocsFonctionnels.nom,
etablissementId: etablissements.id,
etablissementNom: etablissements.nom,
etablissementRegion: etablissements.region,
etatDeploiement: logicielsEtablissements.etatDeploiement,
})
.from(logicielsEtablissements)
.innerJoin(solutions, eq(logicielsEtablissements.solutionId, solutions.id))
.innerJoin(editeurs, eq(solutions.editeurId, editeurs.id))
.leftJoin(blocsFonctionnels, eq(solutions.blocFonctionnelId, blocsFonctionnels.id))
.innerJoin(etablissements, eq(logicielsEtablissements.etablissementId, etablissements.id))
.where(inArray(logicielsEtablissements.etablissementId, etablissementIds))
.orderBy(solutions.nom, etablissements.nom);
// Grouper par solution
const map = new Map<number, {
solutionId: number;
solutionNom: string;
editeurNom: string;
blocFonctionnelNom: string | null;
etablissements: { id: number; nom: string; region: string | null; etatDeploiement: string }[];
}>();
for (const row of rows) {
if (!map.has(row.solutionId)) {
map.set(row.solutionId, {
solutionId: row.solutionId,
solutionNom: row.solutionNom ?? "",
editeurNom: row.editeurNom ?? "",
blocFonctionnelNom: row.blocFonctionnelNom ?? null,
etablissements: [],
});
}
map.get(row.solutionId)!.etablissements.push({
id: row.etablissementId,
nom: row.etablissementNom ?? "",
region: row.etablissementRegion ?? null,
etatDeploiement: row.etatDeploiement ?? "",
});
}
return Array.from(map.values());
}
/**
* Retourne toutes les solutions du référentiel avec les établissements équipés (vue globale).
* Accessible à tous les utilisateurs connectés.
*/
export async function getToutesLesSolutionsGroupees() {
const db = await getDb();
if (!db) return [];
const rows = await db
.select({
solutionId: solutions.id,
solutionNom: solutions.nom,
editeurNom: editeurs.nom,
blocFonctionnelNom: blocsFonctionnels.nom,
etablissementId: etablissements.id,
etablissementNom: etablissements.nom,
etablissementRegion: etablissements.region,
etatDeploiement: logicielsEtablissements.etatDeploiement,
})
.from(solutions)
.leftJoin(editeurs, eq(solutions.editeurId, editeurs.id))
.leftJoin(blocsFonctionnels, eq(solutions.blocFonctionnelId, blocsFonctionnels.id))
.leftJoin(logicielsEtablissements, eq(logicielsEtablissements.solutionId, solutions.id))
.leftJoin(etablissements, eq(logicielsEtablissements.etablissementId, etablissements.id))
.orderBy(solutions.nom, etablissements.nom);
const map = new Map<number, {
solutionId: number;
solutionNom: string;
editeurNom: string;
blocFonctionnelNom: string | null;
nbEtablissements: number;
etablissements: { id: number; nom: string; region: string | null; etatDeploiement: string }[];
}>();
for (const row of rows) {
if (!map.has(row.solutionId)) {
map.set(row.solutionId, {
solutionId: row.solutionId,
solutionNom: row.solutionNom ?? "",
editeurNom: row.editeurNom ?? "",
blocFonctionnelNom: row.blocFonctionnelNom ?? null,
nbEtablissements: 0,
etablissements: [],
});
}
if (row.etablissementId) {
map.get(row.solutionId)!.etablissements.push({
id: row.etablissementId,
nom: row.etablissementNom ?? "",
region: row.etablissementRegion ?? null,
etatDeploiement: row.etatDeploiement ?? "",
});
map.get(row.solutionId)!.nbEtablissements++;
}
}
return Array.from(map.values());
}

View File

@@ -25,6 +25,8 @@ import {
getEtablissementsByAdherent,
getEtablissementsByReferent,
getLogicielsByEtablissement,
getMesSolutionsGroupees,
getToutesLesSolutionsGroupees,
getSolutions,
recordConsultation,
removeEtablissementFromUser,
@@ -300,6 +302,12 @@ export const appRouter = router({
await deleteLogicielEtablissement(input.id);
return { success: true };
}),
mesSolutions: protectedProcedure
.query(({ ctx }) =>
getMesSolutionsGroupees(ctx.user.id, ctx.user.sonumRole ?? "referent")
),
toutesLesSolutions: protectedProcedure
.query(() => getToutesLesSolutionsGroupees()),
}),
// ─── Traçabilité ───────────────────────────────────────────────────────────