952 lines
35 KiB
TypeScript
952 lines
35 KiB
TypeScript
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<typeof drizzle> | 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<void> {
|
|
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<string, unknown> = {};
|
|
|
|
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<number>`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;
|
|
firstName?: string;
|
|
lastName?: string;
|
|
login?: string;
|
|
email: string;
|
|
sonumRole: "referent" | "gestionnaire" | "adherent";
|
|
role?: "admin" | "standard" | "readonly";
|
|
isActive?: boolean;
|
|
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");
|
|
|
|
// Vérifier unicité login si fourni
|
|
if (data.login) {
|
|
const existingLogin = await db.select().from(users).where(eq(users.login, data.login)).limit(1);
|
|
if (existingLogin.length > 0) throw new Error("LOGIN_EXISTS");
|
|
}
|
|
|
|
// openId synthétique pour les comptes locaux
|
|
const syntheticOpenId = `local_${nanoid(16)}`;
|
|
const passwordHash = await bcrypt.hash(data.password, 12);
|
|
|
|
// Dériver name si non fourni
|
|
const fullName = data.name ??
|
|
(data.firstName && data.lastName ? `${data.firstName} ${data.lastName}` : data.email);
|
|
|
|
const insertResult = await db.insert(users).values({
|
|
openId: syntheticOpenId,
|
|
login: data.login ?? null,
|
|
name: fullName,
|
|
firstName: data.firstName ?? null,
|
|
lastName: data.lastName ?? null,
|
|
email: data.email,
|
|
loginMethod: "local",
|
|
sonumRole: data.sonumRole,
|
|
role: data.role ?? "standard",
|
|
isActive: data.isActive !== undefined ? data.isActive : true,
|
|
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 login (email ou login court) + mot de passe. Retourne l'utilisateur ou null. */
|
|
export async function authenticateLocalUser(loginOrEmail: string, password: string) {
|
|
const db = await getDb();
|
|
if (!db) return null;
|
|
|
|
// Chercher par email OU par login court
|
|
const byEmail = await db
|
|
.select({ user: users, passwordHash: localCredentials.passwordHash })
|
|
.from(users)
|
|
.innerJoin(localCredentials, eq(localCredentials.userId, users.id))
|
|
.where(eq(users.email, loginOrEmail))
|
|
.limit(1);
|
|
|
|
let result = byEmail;
|
|
if (!result.length) {
|
|
const byLogin = await db
|
|
.select({ user: users, passwordHash: localCredentials.passwordHash })
|
|
.from(users)
|
|
.innerJoin(localCredentials, eq(localCredentials.userId, users.id))
|
|
.where(eq(users.login, loginOrEmail))
|
|
.limit(1);
|
|
result = byLogin;
|
|
}
|
|
|
|
if (!result.length) return null;
|
|
|
|
const { user, passwordHash } = result[0];
|
|
|
|
// Vérifier que le compte est actif
|
|
if (!user.isActive) return null;
|
|
|
|
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;
|
|
firstName?: string;
|
|
lastName?: string;
|
|
login?: string;
|
|
email?: string;
|
|
sonumRole?: "referent" | "gestionnaire" | "adherent";
|
|
role?: "admin" | "standard" | "readonly";
|
|
isActive?: boolean;
|
|
}) {
|
|
const db = await getDb();
|
|
if (!db) return;
|
|
// Dériver name si firstName/lastName fournis
|
|
const updateData: Record<string, unknown> = { ...data, updatedAt: new Date() };
|
|
if (data.firstName !== undefined || data.lastName !== undefined) {
|
|
const current = await db.select().from(users).where(eq(users.id, userId)).limit(1);
|
|
const fn = data.firstName ?? current[0]?.firstName ?? "";
|
|
const ln = data.lastName ?? current[0]?.lastName ?? "";
|
|
if (fn || ln) updateData.name = `${fn} ${ln}`.trim();
|
|
}
|
|
await db.update(users).set(updateData as any).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<number, {
|
|
solutionId: number;
|
|
solutionNom: string;
|
|
editeurId: number | null;
|
|
editeurNom: string;
|
|
blocFonctionnelId: number | null;
|
|
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 ?? "",
|
|
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<number, {
|
|
solutionId: number;
|
|
solutionNom: string;
|
|
editeurId: number | null;
|
|
editeurNom: string;
|
|
blocFonctionnelId: number | null;
|
|
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 ?? "",
|
|
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<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),
|
|
})),
|
|
};
|
|
}
|