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:
126
server/db.ts
126
server/db.ts
@@ -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),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user