Files
itinova-budget-si/server/db.ts

392 lines
13 KiB
TypeScript

import { and, eq } from "drizzle-orm";
import { drizzle } from "drizzle-orm/mysql2";
import {
capexLignes,
etablissements,
InsertCapexLigne,
InsertEtablissement,
InsertOpexMontantEtab,
InsertOpexPoste,
InsertUser,
inventaireMeta,
inventairePostes,
opexBasesRepartition,
opexMontantsEtab,
opexPostes,
opexValidated,
parametresApp,
userEtablissements,
users,
} from "../drizzle/schema";
let _db: ReturnType<typeof drizzle> | null = null;
// Lazily create the drizzle instance so local tooling can run without a DB.
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;
}
// ─────────────────────────────────────────────────────────────────────────────
// USERS
// ─────────────────────────────────────────────────────────────────────────────
export async function getUserByLogin(login: string) {
const db = await getDb();
if (!db) return undefined;
const result = await db
.select()
.from(users)
.where(eq(users.login, login))
.limit(1);
return result.length > 0 ? result[0] : undefined;
}
export async function getUserById(id: number) {
const db = await getDb();
if (!db) return undefined;
const result = await db.select().from(users).where(eq(users.id, id)).limit(1);
return result.length > 0 ? result[0] : undefined;
}
export async function createUser(user: InsertUser) {
const db = await getDb();
if (!db) throw new Error("Database not available");
const [result] = await db.insert(users).values(user);
return result;
}
export async function updateUser(id: number, data: Partial<InsertUser>) {
const db = await getDb();
if (!db) throw new Error("Database not available");
await db.update(users).set(data).where(eq(users.id, id));
}
export async function listUsers() {
const db = await getDb();
if (!db) return [];
return db.select().from(users);
}
export async function deleteUser(id: number) {
const db = await getDb();
if (!db) throw new Error("Database not available");
await db.delete(users).where(eq(users.id, id));
}
export async function updateLastSignedIn(id: number) {
const db = await getDb();
if (!db) return;
await db
.update(users)
.set({ lastSignedIn: new Date() })
.where(eq(users.id, id));
}
// ─────────────────────────────────────────────────────────────────────────────
// ÉTABLISSEMENTS
// ─────────────────────────────────────────────────────────────────────────────
export async function listEtablissements() {
const db = await getDb();
if (!db) return [];
return db.select().from(etablissements);
}
export async function upsertEtablissement(etab: InsertEtablissement) {
const db = await getDb();
if (!db) throw new Error("Database not available");
await db
.insert(etablissements)
.values(etab)
.onDuplicateKeyUpdate({
set: {
nom: etab.nom,
groupe: etab.groupe,
ville: etab.ville,
actif: etab.actif,
},
});
}
export async function deleteEtablissement(code: string) {
const db = await getDb();
if (!db) throw new Error("Database not available");
await db.delete(etablissements).where(eq(etablissements.code, code));
}
// ─────────────────────────────────────────────────────────────────────────────
// PARAMÈTRES
// ─────────────────────────────────────────────────────────────────────────────
export async function getParametres() {
const db = await getDb();
if (!db) return [];
return db.select().from(parametresApp);
}
export async function setParametre(cle: string, valeur: string) {
const db = await getDb();
if (!db) throw new Error("Database not available");
await db
.insert(parametresApp)
.values({ cle, valeur })
.onDuplicateKeyUpdate({ set: { valeur } });
}
// ─────────────────────────────────────────────────────────────────────────────
// OPEX
// ─────────────────────────────────────────────────────────────────────────────
export async function getOpexPostes(annee: number) {
const db = await getDb();
if (!db) return [];
return db
.select()
.from(opexPostes)
.where(eq(opexPostes.annee, annee));
}
export async function upsertOpexPoste(poste: InsertOpexPoste) {
const db = await getDb();
if (!db) throw new Error("Database not available");
if (poste.id) {
await db
.update(opexPostes)
.set(poste)
.where(eq(opexPostes.id, poste.id));
} else {
await db.insert(opexPostes).values(poste);
}
}
export async function insertOpexPostes(postes: InsertOpexPoste[]) {
const db = await getDb();
if (!db) throw new Error("Database not available");
if (postes.length === 0) return;
await db.insert(opexPostes).values(postes);
}
export async function getOpexMontantsEtab(annee: number) {
const db = await getDb();
if (!db) return [];
return db
.select()
.from(opexMontantsEtab)
.where(eq(opexMontantsEtab.annee, annee));
}
export async function setOpexMontantEtab(data: InsertOpexMontantEtab) {
const db = await getDb();
if (!db) throw new Error("Database not available");
await db
.insert(opexMontantsEtab)
.values(data)
.onDuplicateKeyUpdate({ set: { montant: data.montant } });
}
export async function insertOpexMontantsEtab(rows: InsertOpexMontantEtab[]) {
const db = await getDb();
if (!db) throw new Error("Database not available");
if (rows.length === 0) return;
// Insert par batch de 100
for (let i = 0; i < rows.length; i += 100) {
await db.insert(opexMontantsEtab).values(rows.slice(i, i + 100));
}
}
export async function deleteOpexPostes(annee: number) {
const db = await getDb();
if (!db) return;
await db.delete(opexPostes).where(eq(opexPostes.annee, annee));
}
export async function deleteOpexMontantsEtab(annee: number) {
const db = await getDb();
if (!db) return;
await db.delete(opexMontantsEtab).where(eq(opexMontantsEtab.annee, annee));
}
export async function getOpexValidated(annee: number) {
const db = await getDb();
if (!db) return null;
const result = await db
.select()
.from(opexValidated)
.where(eq(opexValidated.annee, annee))
.limit(1);
return result.length > 0 ? result[0] : null;
}
export async function setOpexValidated(annee: number, userId: number) {
const db = await getDb();
if (!db) throw new Error("Database not available");
await db
.insert(opexValidated)
.values({ annee, validatedBy: userId })
.onDuplicateKeyUpdate({ set: { validatedAt: new Date() } });
}
// ─────────────────────────────────────────────────────────────────────────────
// INVENTAIRE PC
// ─────────────────────────────────────────────────────────────────────────────
export async function getInventaire(annee: number) {
const db = await getDb();
if (!db) return [];
return db
.select()
.from(inventairePostes)
.where(eq(inventairePostes.annee, annee));
}
export async function getInventaireMeta(annee: number) {
const db = await getDb();
if (!db) return null;
const result = await db
.select()
.from(inventaireMeta)
.where(eq(inventaireMeta.annee, annee))
.limit(1);
return result.length > 0 ? result[0] : null;
}
export async function listInventaireMeta() {
const db = await getDb();
if (!db) return [];
return db.select().from(inventaireMeta).orderBy(inventaireMeta.annee);
}
export async function importInventaire(
annee: number,
postes: typeof inventairePostes.$inferInsert[],
meta: { filename: string; nbEtablissements: number; nbFixes: number; nbPortables: number }
) {
const db = await getDb();
if (!db) throw new Error("Database not available");
// Supprimer l'inventaire existant pour cette année
await db.delete(inventairePostes).where(eq(inventairePostes.annee, annee));
// Insérer les nouveaux postes par batch
if (postes.length > 0) {
for (let i = 0; i < postes.length; i += 200) {
await db.insert(inventairePostes).values(postes.slice(i, i + 200));
}
}
// Upsert meta
await db
.insert(inventaireMeta)
.values({ annee, ...meta })
.onDuplicateKeyUpdate({ set: { ...meta, dateImport: new Date() } });
}
// ─────────────────────────────────────────────────────────────────────────────
// CAPEX
// ─────────────────────────────────────────────────────────────────────────────
export async function getCapexLignes(annee: number, etablissementCode: string) {
const db = await getDb();
if (!db) return [];
return db
.select()
.from(capexLignes)
.where(
and(
eq(capexLignes.annee, annee),
eq(capexLignes.etablissementCode, etablissementCode)
)
);
}
export async function saveCapexLignes(
annee: number,
etablissementCode: string,
lignes: { cle: string; montant: string | null }[]
) {
const db = await getDb();
if (!db) throw new Error("Database not available");
for (const ligne of lignes) {
await db
.insert(capexLignes)
.values({ annee, etablissementCode, cle: ligne.cle, montant: ligne.montant })
.onDuplicateKeyUpdate({ set: { montant: ligne.montant } });
}
}
export async function insertCapexLignes(rows: InsertCapexLigne[]) {
const db = await getDb();
if (!db) throw new Error("Database not available");
if (rows.length === 0) return;
for (let i = 0; i < rows.length; i += 100) {
await db.insert(capexLignes).values(rows.slice(i, i + 100));
}
}
// ── OPEX Bases de répartition ────────────────────────────────────────────────
export async function getOpexBasesRepartition(annee: number) {
const db = await getDb();
if (!db) throw new Error("Database not available");
return db
.select()
.from(opexBasesRepartition)
.where(eq(opexBasesRepartition.annee, annee));
}
export async function upsertOpexBaseRepartition(input: {
annee: number;
etablissementCode: string;
etablissementNom?: string | null;
baseRepartition: number;
baseRepartitionHep?: number;
modeManuel?: boolean;
}) {
const db = await getDb();
if (!db) throw new Error("Database not available");
await db
.insert(opexBasesRepartition)
.values({
annee: input.annee,
etablissementCode: input.etablissementCode,
etablissementNom: input.etablissementNom ?? null,
baseRepartition: String(input.baseRepartition),
baseRepartitionHep: String(input.baseRepartitionHep ?? 0),
modeManuel: input.modeManuel ?? false,
})
.onDuplicateKeyUpdate({
set: {
baseRepartition: String(input.baseRepartition),
baseRepartitionHep: String(input.baseRepartitionHep ?? 0),
etablissementNom: input.etablissementNom ?? null,
modeManuel: input.modeManuel ?? false,
},
});
}
/** Import en masse des bases de répartition pour une année (supprime et réinsère) */
export async function importOpexBasesRepartition(
annee: number,
rows: Array<{ etablissementCode: string; etablissementNom?: string | null; baseRepartition: number; baseRepartitionHep?: number }>
) {
const db = await getDb();
if (!db) throw new Error("Database not available");
// Supprimer les données existantes pour l'année
await db.delete(opexBasesRepartition).where(eq(opexBasesRepartition.annee, annee));
if (rows.length === 0) return;
// Insérer en batch
await db.insert(opexBasesRepartition).values(
rows.map(r => ({
annee,
etablissementCode: r.etablissementCode,
etablissementNom: r.etablissementNom ?? null,
baseRepartition: String(r.baseRepartition),
baseRepartitionHep: String(r.baseRepartitionHep ?? 0),
}))
);
}