Checkpoint: Évolution v6 complète : onglets Éditeurs et Blocs fonctionnels dans Admin (CRUD complet), page Tableau de bord statistiques avec graphiques Recharts (KPIs, barres par bloc/région, camembert état déploiement, top 10 solutions), lien dans la sidebar réservé aux gestionnaires. 33 tests Vitest passés, 0 erreur TypeScript.

This commit is contained in:
Manus
2026-04-17 11:11:15 -04:00
parent cbd439bac7
commit 5801ba4add
7 changed files with 840 additions and 1 deletions

View File

@@ -777,3 +777,129 @@ export async function deleteSolution(id: number) {
await db.delete(solutions).where(eq(solutions.id, id));
return { success: true };
}
// ─── CRUD Éditeurs ────────────────────────────────────────────────────────────
export async function updateEditeur(id: number, nom: string) {
const db = await getDb();
if (!db) throw new Error("DB unavailable");
await db.update(editeurs).set({ nom }).where(eq(editeurs.id, id));
return { id };
}
export async function deleteEditeur(id: number) {
const db = await getDb();
if (!db) throw new Error("DB unavailable");
await db.delete(editeurs).where(eq(editeurs.id, id));
return { id };
}
// ─── CRUD Blocs Fonctionnels ──────────────────────────────────────────────────
export async function updateBlocFonctionnel(id: number, nom: string) {
const db = await getDb();
if (!db) throw new Error("DB unavailable");
await db.update(blocsFonctionnels).set({ nom }).where(eq(blocsFonctionnels.id, id));
return { id };
}
export async function deleteBlocFonctionnel(id: number) {
const db = await getDb();
if (!db) throw new Error("DB unavailable");
await db.delete(blocsFonctionnels).where(eq(blocsFonctionnels.id, id));
return { id };
}
// ─── Statistiques ─────────────────────────────────────────────────────────────
export async function getStatistiques() {
const db = await getDb();
if (!db) return null;
// Total établissements
const [{ total: totalEtablissements }] = await db
.select({ total: sql<number>`COUNT(*)` })
.from(etablissements);
// Total solutions distinctes utilisées
const [{ total: totalSolutions }] = await db
.select({ total: sql<number>`COUNT(DISTINCT solutionId)` })
.from(logicielsEtablissements);
// Total fiches logiciels (lignes logiciels_etablissements)
const [{ total: totalFiches }] = await db
.select({ total: sql<number>`COUNT(*)` })
.from(logicielsEtablissements);
// Établissements avec au moins un logiciel
const [{ total: etabAvecLogiciel }] = await db
.select({ total: sql<number>`COUNT(DISTINCT etablissementId)` })
.from(logicielsEtablissements);
// Répartition par bloc fonctionnel
const parBloc = await db
.select({
blocNom: blocsFonctionnels.nom,
count: sql<number>`COUNT(DISTINCT ${logicielsEtablissements.etablissementId})`,
})
.from(logicielsEtablissements)
.innerJoin(solutions, eq(logicielsEtablissements.solutionId, solutions.id))
.leftJoin(blocsFonctionnels, eq(solutions.blocFonctionnelId, blocsFonctionnels.id))
.groupBy(blocsFonctionnels.nom)
.orderBy(sql`COUNT(DISTINCT ${logicielsEtablissements.etablissementId}) DESC`);
// Répartition par région
const parRegion = await db
.select({
region: etablissements.region,
count: sql<number>`COUNT(DISTINCT ${etablissements.id})`,
})
.from(etablissements)
.groupBy(etablissements.region)
.orderBy(sql`COUNT(DISTINCT ${etablissements.id}) DESC`);
// Répartition par état de déploiement
const parEtat = await db
.select({
etat: logicielsEtablissements.etatDeploiement,
count: sql<number>`COUNT(*)`,
})
.from(logicielsEtablissements)
.groupBy(logicielsEtablissements.etatDeploiement)
.orderBy(sql`COUNT(*) DESC`);
// Top 10 solutions les plus utilisées
const topSolutions = await db
.select({
solutionNom: solutions.nom,
editeurNom: editeurs.nom,
count: sql<number>`COUNT(DISTINCT ${logicielsEtablissements.etablissementId})`,
})
.from(logicielsEtablissements)
.innerJoin(solutions, eq(logicielsEtablissements.solutionId, solutions.id))
.leftJoin(editeurs, eq(solutions.editeurId, editeurs.id))
.groupBy(solutions.id, solutions.nom, editeurs.nom)
.orderBy(sql`COUNT(DISTINCT ${logicielsEtablissements.etablissementId}) DESC`)
.limit(10);
// Taux de remplissage (% établissements avec au moins 1 logiciel)
const tauxRemplissage = totalEtablissements > 0
? Math.round((Number(etabAvecLogiciel) / Number(totalEtablissements)) * 100)
: 0;
return {
totalEtablissements: Number(totalEtablissements),
totalSolutions: Number(totalSolutions),
totalFiches: Number(totalFiches),
etabAvecLogiciel: Number(etabAvecLogiciel),
tauxRemplissage,
parBloc: parBloc.map((r) => ({ nom: r.blocNom ?? "Non renseigné", count: Number(r.count) })),
parRegion: parRegion.map((r) => ({ nom: r.region ?? "Non renseignée", count: Number(r.count) })),
parEtat: parEtat.map((r) => ({ nom: r.etat ?? "Inconnu", count: Number(r.count) })),
topSolutions: topSolutions.map((r) => ({
nom: r.solutionNom ?? "",
editeur: r.editeurNom ?? "",
count: Number(r.count),
})),
};
}