import { and, desc, eq, ilike, inArray, like, or, sql } from "drizzle-orm"; import { drizzle } from "drizzle-orm/mysql2"; import { InsertUser, blocsFonctionnels, consultations, demandesContact, editeurs, etablissements, logicielsEtablissements, solutions, users, } from "../drizzle/schema"; import { ENV } from "./_core/env"; let _db: ReturnType | null = null; export async function getDb() { if (!_db && process.env.DATABASE_URL) { try { _db = drizzle(process.env.DATABASE_URL); } catch (error) { console.warn("[Database] Failed to connect:", error); _db = null; } } return _db; } // ─── Utilisateurs ───────────────────────────────────────────────────────────── export async function upsertUser(user: InsertUser): Promise { if (!user.openId) throw new Error("User openId is required for upsert"); const db = await getDb(); if (!db) { console.warn("[Database] Cannot upsert user: database not available"); return; } const values: InsertUser = { openId: user.openId }; const updateSet: Record = {}; const textFields = ["name", "email", "loginMethod"] as const; for (const field of textFields) { const value = user[field]; if (value === undefined) continue; const normalized = value ?? null; values[field] = normalized; updateSet[field] = normalized; } if (user.lastSignedIn !== undefined) { values.lastSignedIn = user.lastSignedIn; updateSet.lastSignedIn = user.lastSignedIn; } if (user.role !== undefined) { values.role = user.role; updateSet.role = user.role; } else if (user.openId === ENV.ownerOpenId) { values.role = "admin"; updateSet.role = "admin"; } if (!values.lastSignedIn) values.lastSignedIn = new Date(); if (Object.keys(updateSet).length === 0) updateSet.lastSignedIn = new Date(); await db.insert(users).values(values).onDuplicateKeyUpdate({ set: updateSet }); } export async function getUserByOpenId(openId: string) { const db = await getDb(); if (!db) return undefined; const result = await db.select().from(users).where(eq(users.openId, openId)).limit(1); return result.length > 0 ? result[0] : undefined; } export async function updateUserCgu(userId: number) { const db = await getDb(); if (!db) return; await db.update(users).set({ cguAccepted: true, cguAcceptedAt: new Date() }).where(eq(users.id, userId)); } export async function updateUserSonumRole(userId: number, sonumRole: "referent" | "gestionnaire" | "adherent") { const db = await getDb(); if (!db) return; await db.update(users).set({ sonumRole }).where(eq(users.id, userId)); } export async function getAllUsers() { const db = await getDb(); if (!db) return []; return db.select().from(users).orderBy(desc(users.createdAt)); } // ─── Référentiel ────────────────────────────────────────────────────────────── export async function getEditeurs() { const db = await getDb(); if (!db) return []; return db.select().from(editeurs).where(eq(editeurs.estValide, true)).orderBy(editeurs.nom); } export async function createEditeur(nom: string, estValide = false) { const db = await getDb(); if (!db) return null; const result = await db.insert(editeurs).values({ nom, estValide }); return result[0]; } export async function getBlocsFonctionnels() { const db = await getDb(); if (!db) return []; return db.select().from(blocsFonctionnels).orderBy(blocsFonctionnels.nom); } export async function createBlocFonctionnel(nom: string, estValide = false) { const db = await getDb(); if (!db) return null; const result = await db.insert(blocsFonctionnels).values({ nom, estValide }); return result[0]; } export async function getSolutions(search?: string) { const db = await getDb(); if (!db) return []; const query = db .select({ id: solutions.id, nom: solutions.nom, editeurId: solutions.editeurId, editeurNom: editeurs.nom, blocFonctionnelId: solutions.blocFonctionnelId, blocFonctionnelNom: blocsFonctionnels.nom, estValide: solutions.estValide, }) .from(solutions) .leftJoin(editeurs, eq(solutions.editeurId, editeurs.id)) .leftJoin(blocsFonctionnels, eq(solutions.blocFonctionnelId, blocsFonctionnels.id)) .where( search ? and(eq(solutions.estValide, true), or(like(solutions.nom, `%${search}%`), like(editeurs.nom, `%${search}%`))) : eq(solutions.estValide, true) ) .orderBy(solutions.nom); return query; } export async function createSolution(nom: string, editeurId: number, blocFonctionnelId?: number | null, estValide = false) { const db = await getDb(); if (!db) return null; const result = await db.insert(solutions).values({ nom, editeurId, blocFonctionnelId: blocFonctionnelId ?? null, estValide }); // result[0] est un ResultSetHeader MySQL, on retourne l'insertId directement const insertId = (result[0] as any).insertId as number; return insertId; } // ─── Établissements ─────────────────────────────────────────────────────────── export async function getEtablissementsByReferent(referentId: number) { const db = await getDb(); if (!db) return []; return db.select().from(etablissements).where(eq(etablissements.referentId, referentId)).orderBy(etablissements.nom); } export async function getAllEtablissements() { const db = await getDb(); if (!db) return []; return db.select().from(etablissements).orderBy(etablissements.nom); } export async function getEtablissementById(id: number) { const db = await getDb(); if (!db) return null; const result = await db.select().from(etablissements).where(eq(etablissements.id, id)).limit(1); return result[0] ?? null; } export async function searchEtablissements(filters: { solutionId?: number; editeurId?: number; blocFonctionnelId?: number; region?: string; typeActivite?: string; tailleEffectifs?: string; etatDeploiement?: string; userId?: number; sonumRole?: string; }) { const db = await getDb(); if (!db) return []; 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)); // 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, 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; } // ─── Logiciels par Établissement ───────────────────────────────────────────── export async function getLogicielsByEtablissement(etablissementId: number) { const db = await getDb(); if (!db) return []; return db .select({ id: logicielsEtablissements.id, etablissementId: logicielsEtablissements.etablissementId, solutionId: logicielsEtablissements.solutionId, solutionNom: solutions.nom, editeurNom: editeurs.nom, blocFonctionnelNom: blocsFonctionnels.nom, etatDeploiement: logicielsEtablissements.etatDeploiement, modeHebergement: logicielsEtablissements.modeHebergement, modeFacturation: logicielsEtablissements.modeFacturation, interoperabilite: logicielsEtablissements.interoperabilite, versionMajeure: logicielsEtablissements.versionMajeure, commentaire: logicielsEtablissements.commentaire, contactNom: logicielsEtablissements.contactNom, contactFonction: logicielsEtablissements.contactFonction, contactEmail: logicielsEtablissements.contactEmail, createdAt: logicielsEtablissements.createdAt, updatedAt: logicielsEtablissements.updatedAt, }) .from(logicielsEtablissements) .leftJoin(solutions, eq(logicielsEtablissements.solutionId, solutions.id)) .leftJoin(editeurs, eq(solutions.editeurId, editeurs.id)) .leftJoin(blocsFonctionnels, eq(solutions.blocFonctionnelId, blocsFonctionnels.id)) .where(eq(logicielsEtablissements.etablissementId, etablissementId)) .orderBy(logicielsEtablissements.createdAt); } export async function upsertLogicielEtablissement(data: { id?: number; etablissementId: number; solutionId: number; etatDeploiement: "demarrage" | "en_cours" | "operationnel" | "en_remplacement"; modeHebergement?: "hds" | "on_premise" | "hybride" | null; modeFacturation?: "saas" | "achat_maintenance" | "location" | null; interoperabilite?: "non" | "oui_interface" | "oui_eai" | null; versionMajeure?: string | null; commentaire?: string | null; contactNom?: string | null; contactFonction?: string | null; contactEmail?: string | null; saisiePar?: number; }) { const db = await getDb(); if (!db) return null; if (data.id) { await db.update(logicielsEtablissements).set({ ...data, updatedAt: new Date() }).where(eq(logicielsEtablissements.id, data.id)); return data.id; } const result = await db.insert(logicielsEtablissements).values(data); return result[0]; } export async function deleteLogicielEtablissement(id: number) { const db = await getDb(); if (!db) return; await db.delete(logicielsEtablissements).where(eq(logicielsEtablissements.id, id)); } // ─── Traçabilité ────────────────────────────────────────────────────────────── export async function recordConsultation(etablissementId: number, userId: number, userName: string) { const db = await getDb(); if (!db) return; await db.insert(consultations).values({ etablissementId, consultePar: userId, consultéParNom: userName }); } export async function getConsultationCount(etablissementId: number) { const db = await getDb(); if (!db) return 0; const result = await db .select({ count: sql`count(*)` }) .from(consultations) .where(eq(consultations.etablissementId, etablissementId)); return Number(result[0]?.count ?? 0); } export async function getConsultationsList(etablissementId: number) { const db = await getDb(); if (!db) return []; return db .select() .from(consultations) .where(eq(consultations.etablissementId, etablissementId)) .orderBy(desc(consultations.createdAt)) .limit(50); } // ─── Demandes de Contact ────────────────────────────────────────────────────── export async function createDemandeContact(data: { etablissementCibleId: number; demandeurId: number; demandeurNom: string; demandeurEmail: string; message: string; }) { const db = await getDb(); if (!db) return null; const result = await db.insert(demandesContact).values(data); return result[0]; } export async function getDemandesByDemandeur(demandeurId: number) { const db = await getDb(); if (!db) return []; return db .select({ id: demandesContact.id, etablissementNom: etablissements.nom, message: demandesContact.message, statut: demandesContact.statut, reponse: demandesContact.reponse, reponduAt: demandesContact.reponduAt, createdAt: demandesContact.createdAt, }) .from(demandesContact) .leftJoin(etablissements, eq(demandesContact.etablissementCibleId, etablissements.id)) .where(eq(demandesContact.demandeurId, demandeurId)) .orderBy(desc(demandesContact.createdAt)); } export async function getDemandesRecuesParEtablissement(referentId: number) { const db = await getDb(); if (!db) return []; return db .select({ id: demandesContact.id, etablissementNom: etablissements.nom, demandeurNom: demandesContact.demandeurNom, demandeurEmail: demandesContact.demandeurEmail, message: demandesContact.message, statut: demandesContact.statut, reponse: demandesContact.reponse, reponduAt: demandesContact.reponduAt, createdAt: demandesContact.createdAt, }) .from(demandesContact) .leftJoin(etablissements, eq(demandesContact.etablissementCibleId, etablissements.id)) .where(eq(etablissements.referentId, referentId)) .orderBy(desc(demandesContact.createdAt)); } export async function getAllDemandes() { const db = await getDb(); if (!db) return []; return db .select({ id: demandesContact.id, etablissementNom: etablissements.nom, demandeurNom: demandesContact.demandeurNom, demandeurEmail: demandesContact.demandeurEmail, message: demandesContact.message, statut: demandesContact.statut, reponse: demandesContact.reponse, reponduAt: demandesContact.reponduAt, createdAt: demandesContact.createdAt, }) .from(demandesContact) .leftJoin(etablissements, eq(demandesContact.etablissementCibleId, etablissements.id)) .orderBy(desc(demandesContact.createdAt)); } export async function repondreDemandeContact(id: number, reponse: string, reponsePar: number) { const db = await getDb(); if (!db) return; await db .update(demandesContact) .set({ reponse, reponsePar, statut: "repondu", reponduAt: new Date(), updatedAt: new Date() }) .where(eq(demandesContact.id, id)); } export async function getDemandeById(id: number) { const db = await getDb(); if (!db) return null; const result = await db.select().from(demandesContact).where(eq(demandesContact.id, id)).limit(1); return result[0] ?? null; } // ─── Auth locale ────────────────────────────────────────────────────────────── import { localCredentials, userEtablissements } from "../drizzle/schema"; import bcrypt from "bcryptjs"; import { nanoid } from "nanoid"; /** Crée un utilisateur local (sans openId OAuth) avec un mot de passe hashé. */ export async function createLocalUser(data: { name: string; email: string; sonumRole: "referent" | "gestionnaire" | "adherent"; password: string; }) { const db = await getDb(); if (!db) throw new Error("Database not available"); // Vérifier unicité email const existing = await db.select().from(users).where(eq(users.email, data.email)).limit(1); if (existing.length > 0) throw new Error("EMAIL_EXISTS"); // openId synthétique pour les comptes locaux const syntheticOpenId = `local_${nanoid(16)}`; const passwordHash = await bcrypt.hash(data.password, 12); const insertResult = await db.insert(users).values({ openId: syntheticOpenId, name: data.name, email: data.email, loginMethod: "local", sonumRole: data.sonumRole, cguAccepted: false, lastSignedIn: new Date(), }); const userId = Number((insertResult as any)[0]?.insertId ?? 0); if (!userId) throw new Error("Failed to create user"); await db.insert(localCredentials).values({ userId, passwordHash }); return userId; } /** Authentifie un utilisateur par email + mot de passe. Retourne l'utilisateur ou null. */ export async function authenticateLocalUser(email: string, password: string) { const db = await getDb(); if (!db) return null; const result = await db .select({ user: users, passwordHash: localCredentials.passwordHash, }) .from(users) .innerJoin(localCredentials, eq(localCredentials.userId, users.id)) .where(eq(users.email, email)) .limit(1); if (!result.length) return null; const { user, passwordHash } = result[0]; const valid = await bcrypt.compare(password, passwordHash); if (!valid) return null; // Mettre à jour lastSignedIn await db.update(users).set({ lastSignedIn: new Date() }).where(eq(users.id, user.id)); return user; } /** Vérifie si un utilisateur possède des credentials locaux. */ export async function hasLocalCredentials(userId: number) { const db = await getDb(); if (!db) return false; const result = await db.select().from(localCredentials).where(eq(localCredentials.userId, userId)).limit(1); return result.length > 0; } /** Met à jour le mot de passe d'un utilisateur. */ export async function updateLocalPassword(userId: number, newPassword: string) { const db = await getDb(); if (!db) return; const passwordHash = await bcrypt.hash(newPassword, 12); const existing = await db.select().from(localCredentials).where(eq(localCredentials.userId, userId)).limit(1); if (existing.length > 0) { await db.update(localCredentials).set({ passwordHash, updatedAt: new Date() }).where(eq(localCredentials.userId, userId)); } else { await db.insert(localCredentials).values({ userId, passwordHash }); } } /** Met à jour les informations d'un utilisateur. */ export async function updateUser(userId: number, data: { name?: string; email?: string; sonumRole?: "referent" | "gestionnaire" | "adherent"; }) { const db = await getDb(); if (!db) return; await db.update(users).set({ ...data, updatedAt: new Date() }).where(eq(users.id, userId)); } /** Supprime un utilisateur et ses credentials locaux. */ export async function deleteUser(userId: number) { const db = await getDb(); if (!db) return; await db.delete(localCredentials).where(eq(localCredentials.userId, userId)); await db.delete(userEtablissements).where(eq(userEtablissements.userId, userId)); await db.delete(users).where(eq(users.id, userId)); } // ─── Affectations Adhérents ↔ Établissements ───────────────────────────────── /** Retourne les établissements affectés à un adhérent. */ export async function getEtablissementsByAdherent(userId: number) { const db = await getDb(); if (!db) return []; return 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(userEtablissements) .innerJoin(etablissements, eq(userEtablissements.etablissementId, etablissements.id)) .where(eq(userEtablissements.userId, userId)) .orderBy(etablissements.nom); } /** Retourne les IDs des établissements affectés à un adhérent. */ export async function getAffectationsByUser(userId: number) { const db = await getDb(); if (!db) return []; const result = await db .select({ etablissementId: userEtablissements.etablissementId }) .from(userEtablissements) .where(eq(userEtablissements.userId, userId)); return result.map((r) => r.etablissementId); } /** Affecte un établissement à un adhérent (idempotent). */ export async function assignEtablissementToUser(userId: number, etablissementId: number) { const db = await getDb(); if (!db) return; const existing = await db .select() .from(userEtablissements) .where(and(eq(userEtablissements.userId, userId), eq(userEtablissements.etablissementId, etablissementId))) .limit(1); if (existing.length === 0) { await db.insert(userEtablissements).values({ userId, etablissementId }); } } /** Retire un établissement d'un adhérent. */ export async function removeEtablissementFromUser(userId: number, etablissementId: number) { const db = await getDb(); if (!db) return; await db .delete(userEtablissements) .where(and(eq(userEtablissements.userId, userId), eq(userEtablissements.etablissementId, etablissementId))); } /** Remplace toutes les affectations d'un adhérent par une nouvelle liste. */ export async function setAffectationsForUser(userId: number, etablissementIds: number[]) { const db = await getDb(); if (!db) return; await db.delete(userEtablissements).where(eq(userEtablissements.userId, userId)); if (etablissementIds.length > 0) { await db.insert(userEtablissements).values(etablissementIds.map((eid) => ({ userId, etablissementId: eid }))); } } /** Retourne tous les utilisateurs avec leurs affectations. */ export async function getAllUsersWithAffectations() { const db = await getDb(); if (!db) return []; const allUsers = await db.select().from(users).orderBy(users.name); const allAffectations = await db .select({ userId: userEtablissements.userId, etablissementId: userEtablissements.etablissementId, etablissementNom: etablissements.nom, }) .from(userEtablissements) .innerJoin(etablissements, eq(userEtablissements.etablissementId, etablissements.id)); return allUsers.map((u) => ({ ...u, etablissements: allAffectations .filter((a) => a.userId === u.id) .map((a) => ({ id: a.etablissementId, nom: a.etablissementNom })), 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(); for (const row of rows) { if (!map.has(row.solutionId)) { map.set(row.solutionId, { solutionId: row.solutionId, solutionNom: row.solutionNom ?? "", editeurId: null, editeurNom: row.editeurNom ?? "", blocFonctionnelId: null, blocFonctionnelNom: row.blocFonctionnelNom ?? null, nbEtablissements: 0, etablissements: [], }); } const entry = map.get(row.solutionId)!; if (row.etablissementId) { entry.etablissements.push({ id: row.etablissementId, nom: row.etablissementNom ?? "", region: row.etablissementRegion ?? null, etatDeploiement: row.etatDeploiement ?? "", }); entry.nbEtablissements = entry.etablissements.length; } } 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, editeurId: solutions.editeurId, editeurNom: editeurs.nom, blocFonctionnelId: solutions.blocFonctionnelId, 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(); for (const row of rows) { if (!map.has(row.solutionId)) { map.set(row.solutionId, { solutionId: row.solutionId, solutionNom: row.solutionNom ?? "", editeurId: row.editeurId ?? null, editeurNom: row.editeurNom ?? "", blocFonctionnelId: row.blocFonctionnelId ?? null, 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()); } export async function updateSolution(id: number, nom: string, editeurId: number, blocFonctionnelId: number | null) { const db = await getDb(); if (!db) throw new Error("DB unavailable"); await db.update(solutions).set({ nom, editeurId, blocFonctionnelId }).where(eq(solutions.id, id)); return { id }; } export async function deleteSolution(id: number) { const db = await getDb(); if (!db) throw new Error("DB unavailable"); 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`COUNT(*)` }) .from(etablissements); // Total solutions distinctes utilisées const [{ total: totalSolutions }] = await db .select({ total: sql`COUNT(DISTINCT solutionId)` }) .from(logicielsEtablissements); // Total fiches logiciels (lignes logiciels_etablissements) const [{ total: totalFiches }] = await db .select({ total: sql`COUNT(*)` }) .from(logicielsEtablissements); // Établissements avec au moins un logiciel const [{ total: etabAvecLogiciel }] = await db .select({ total: sql`COUNT(DISTINCT etablissementId)` }) .from(logicielsEtablissements); // Répartition par bloc fonctionnel const parBloc = await db .select({ blocNom: blocsFonctionnels.nom, count: sql`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`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`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`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), })), }; }