Checkpoint: Pagination serveur des établissements et des messages, tri serveur, navigation utilisateur, limitation de débit sur connexion locale, messages et demandes, avec 46 tests Vitest, typage TypeScript et build validés.

This commit is contained in:
Manus
2026-08-18 07:34:59 +00:00
parent 7c97bec6c1
commit 9d9ae68b18
8 changed files with 429 additions and 51 deletions

View File

@@ -1,4 +1,4 @@
import { and, desc, eq, ilike, inArray, like, or, sql } from "drizzle-orm";
import { and, asc, desc, eq, ilike, inArray, like, or, sql } from "drizzle-orm";
import { drizzle } from "drizzle-orm/mysql2";
import {
InsertUser,
@@ -179,9 +179,13 @@ export async function searchEtablissements(filters: {
etatDeploiement?: string;
userId?: number;
sonumRole?: string;
page: number;
pageSize: number;
sortBy: "nom" | "region" | "typeActivite" | "tailleEffectifs";
sortDirection: "asc" | "desc";
}) {
const db = await getDb();
if (!db) return [];
if (!db) return { items: [], total: 0, page: filters.page, pageSize: filters.pageSize };
const conditions: any[] = [];
// Visibilité : si pas gestionnaire, on ne montre que les fiches "tous"
@@ -193,7 +197,7 @@ export async function searchEtablissements(filters: {
if (filters.sonumRole === "adherent" && filters.userId) {
const affectations = await getAffectationsByUser(filters.userId);
if (affectations.length === 0) {
return []; // Aucun établissement affecté
return { items: [], total: 0, page: filters.page, pageSize: filters.pageSize };
}
conditions.push(inArray(etablissements.id, affectations));
}
@@ -230,23 +234,44 @@ export async function searchEtablissements(filters: {
conditions.push(inArray(etablissements.id, subquery));
}
const result = await db
.select({
id: etablissements.id,
finess: etablissements.finess,
nom: etablissements.nom,
region: etablissements.region,
departement: etablissements.departement,
typeActivite: etablissements.typeActivite,
tailleEffectifs: etablissements.tailleEffectifs,
referentId: etablissements.referentId,
visibilite: etablissements.visibilite,
accepteMiseEnRelation: etablissements.accepteMiseEnRelation,
})
.from(etablissements)
.where(conditions.length > 0 ? and(...conditions) : undefined)
.orderBy(etablissements.nom);
return result;
const whereClause = conditions.length > 0 ? and(...conditions) : undefined;
const sortColumn = {
nom: etablissements.nom,
region: etablissements.region,
typeActivite: etablissements.typeActivite,
tailleEffectifs: etablissements.tailleEffectifs,
}[filters.sortBy];
const orderBy = filters.sortDirection === "desc" ? desc(sortColumn) : asc(sortColumn);
// Les requêtes sont indépendantes : total global et page courante partent en parallèle.
const [items, totalRows] = await Promise.all([
db
.select({
id: etablissements.id,
finess: etablissements.finess,
nom: etablissements.nom,
region: etablissements.region,
departement: etablissements.departement,
typeActivite: etablissements.typeActivite,
tailleEffectifs: etablissements.tailleEffectifs,
referentId: etablissements.referentId,
visibilite: etablissements.visibilite,
accepteMiseEnRelation: etablissements.accepteMiseEnRelation,
})
.from(etablissements)
.where(whereClause)
.orderBy(orderBy, asc(etablissements.id))
.limit(filters.pageSize)
.offset((filters.page - 1) * filters.pageSize),
db.select({ total: sql<number>`COUNT(*)` }).from(etablissements).where(whereClause),
]);
return {
items,
total: Number(totalRows[0]?.total ?? 0),
page: filters.page,
pageSize: filters.pageSize,
};
}
// ─── Logiciels par Établissement ─────────────────────────────────────────────
@@ -1137,14 +1162,35 @@ export async function getCanauxForUser(userId: number, isGestionnaire: boolean)
/**
* Récupère les messages d'un canal.
*/
export async function getMessagesCanal(canalId: number) {
export async function getMessagesCanal(canalId: number, page: number, pageSize: number) {
const db = await getDb();
if (!db) return [];
return db
.select()
.from(messagesCanaux)
.where(eq(messagesCanaux.canalId, canalId))
.orderBy(messagesCanaux.createdAt);
if (!db) return { items: [], total: 0, page, pageSize, hasMore: false };
/**
* La base lit les messages les plus récents en premier pour rendre le premier
* affichage rapide, puis la page est renversée avant l'affichage chronologique.
*/
const [newestFirst, totalRows] = await Promise.all([
db
.select()
.from(messagesCanaux)
.where(eq(messagesCanaux.canalId, canalId))
.orderBy(desc(messagesCanaux.createdAt), desc(messagesCanaux.id))
.limit(pageSize)
.offset((page - 1) * pageSize),
db
.select({ total: sql<number>`COUNT(*)` })
.from(messagesCanaux)
.where(eq(messagesCanaux.canalId, canalId)),
]);
const total = Number(totalRows[0]?.total ?? 0);
return {
items: newestFirst.reverse(),
total,
page,
pageSize,
hasMore: page * pageSize < total,
};
}
/**