Checkpoint: Migration complète localStorage → BDD : useBudgetData, ParametresContext, Budget2027, DsiCapex, DsiOpex, ImportModal (indicateur inventaires + suppression double-écriture). Ajout procédures tRPC : inventaire.listMeta, inventaire.getStats, opex.initFromSource, opex.deleteAll, opex.cloneFromN1. 14 tests Vitest passants. 0 erreur TypeScript.
This commit is contained in:
18
server/db.ts
18
server/db.ts
@@ -201,6 +201,18 @@ export async function insertOpexMontantsEtab(rows: InsertOpexMontantEtab[]) {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -245,6 +257,12 @@ export async function getInventaireMeta(annee: number) {
|
||||
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[],
|
||||
|
||||
@@ -147,6 +147,41 @@ export const appRouter = router({
|
||||
validate: adminProcedure
|
||||
.input(z.object({ annee: z.number() }))
|
||||
.mutation(async ({ input, ctx }) => { await db.setOpexValidated(input.annee, ctx.user.id); return { success: true }; }),
|
||||
|
||||
// Initialiser depuis les données sources JSON (seed initial)
|
||||
initFromSource: writeProcedure
|
||||
.input(z.object({
|
||||
annee: z.number(),
|
||||
postes: z.array(z.object({
|
||||
colIdx: z.number(),
|
||||
libelle: z.string(),
|
||||
libelleCourt: z.string().optional().nullable(),
|
||||
libelleDetail: z.string().optional().nullable(),
|
||||
fournisseur: z.string().optional().nullable(),
|
||||
categorie: z.string().optional().nullable(),
|
||||
type: z.string().optional().nullable(),
|
||||
facturation: z.string().optional().nullable(),
|
||||
modeVentilation: z.string().optional().nullable(),
|
||||
compte: z.string().optional().nullable(),
|
||||
detail: z.string().optional().nullable(),
|
||||
budgetN1: z.string().optional().nullable(),
|
||||
montant: z.string().optional().nullable(),
|
||||
isCustom: z.boolean().optional(),
|
||||
}))
|
||||
}))
|
||||
.mutation(async ({ input }) => {
|
||||
await db.insertOpexPostes(input.postes.map(p => ({ ...p, annee: input.annee })));
|
||||
return { success: true };
|
||||
}),
|
||||
|
||||
// Supprimer toutes les lignes OPEX d'une année
|
||||
deleteAll: writeProcedure
|
||||
.input(z.object({ annee: z.number() }))
|
||||
.mutation(async ({ input }) => {
|
||||
await db.deleteOpexPostes(input.annee);
|
||||
await db.deleteOpexMontantsEtab(input.annee);
|
||||
return { success: true };
|
||||
}),
|
||||
}),
|
||||
|
||||
inventaire: router({
|
||||
@@ -158,6 +193,90 @@ export const appRouter = router({
|
||||
return { postes, meta };
|
||||
}),
|
||||
|
||||
// Retourne les stats calculées par établissement (pour useBudgetData)
|
||||
getStats: protectedProcedure
|
||||
.input(z.object({
|
||||
annee: z.number(),
|
||||
seuilFixesAns: z.number().default(5),
|
||||
seuilPortablesAns: z.number().default(4),
|
||||
coutFixe: z.number().default(850),
|
||||
coutPortable: z.number().default(950),
|
||||
}))
|
||||
.query(async ({ input }) => {
|
||||
const postes = await db.getInventaire(input.annee);
|
||||
const meta = await db.getInventaireMeta(input.annee);
|
||||
if (!meta) return { meta: null, etablissements: [] };
|
||||
|
||||
// Charger les noms des établissements
|
||||
const etabList = await db.listEtablissements();
|
||||
const etabNoms = new Map(etabList.map(e => [e.code, e.nom]));
|
||||
|
||||
// Grouper par établissement
|
||||
const byEtab = new Map<string, { fixes: typeof postes; portables: typeof postes }>();
|
||||
for (const p of postes) {
|
||||
if (!byEtab.has(p.etablissementCode)) byEtab.set(p.etablissementCode, { fixes: [], portables: [] });
|
||||
const e = byEtab.get(p.etablissementCode)!;
|
||||
if (p.typePoste === 'fixe') e.fixes.push(p);
|
||||
else e.portables.push(p);
|
||||
}
|
||||
|
||||
const etablissements = Array.from(byEtab.entries()).map(([code, { fixes, portables }]) => {
|
||||
const nbFixesRenouveler = fixes.filter(f => f.ageAns !== null && parseFloat(f.ageAns) >= input.seuilFixesAns).length;
|
||||
const nbPortablesRenouveler = portables.filter(p => p.ageAns !== null && parseFloat(p.ageAns) >= input.seuilPortablesAns).length;
|
||||
const nbFixesSansDate = fixes.filter(f => f.ageAns === null).length;
|
||||
const nbPortablesSansDate = portables.filter(p => p.ageAns === null).length;
|
||||
const budgetFixes = nbFixesRenouveler * input.coutFixe;
|
||||
const budgetPortables = nbPortablesRenouveler * input.coutPortable;
|
||||
return {
|
||||
code,
|
||||
nom: etabNoms.get(code) ?? code,
|
||||
fixes: fixes.map(f => ({ libelle: f.libelle ?? '', date_achat: f.dateRef ?? '', age_ans: f.ageAns !== null ? parseFloat(f.ageAns) : null, a_renouveler: f.ageAns !== null && parseFloat(f.ageAns) >= input.seuilFixesAns, modele: f.modele ?? '', fabricant: f.fabricant ?? '' })),
|
||||
portables: portables.map(p => ({ libelle: p.libelle ?? '', date_achat: p.dateRef ?? '', age_ans: p.ageAns !== null ? parseFloat(p.ageAns) : null, a_renouveler: p.ageAns !== null && parseFloat(p.ageAns) >= input.seuilPortablesAns, modele: p.modele ?? '', fabricant: p.fabricant ?? '' })),
|
||||
stats: {
|
||||
nb_fixes_total: fixes.length,
|
||||
nb_portables_total: portables.length,
|
||||
nb_fixes_renouveler: nbFixesRenouveler,
|
||||
nb_portables_renouveler: nbPortablesRenouveler,
|
||||
nb_fixes_sans_date: nbFixesSansDate,
|
||||
nb_portables_sans_date: nbPortablesSansDate,
|
||||
budget_fixes: budgetFixes,
|
||||
budget_portables: budgetPortables,
|
||||
budget_total: budgetFixes + budgetPortables,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const totalFixesR = etablissements.reduce((s, e) => s + e.stats.nb_fixes_renouveler, 0);
|
||||
const totalPortablesR = etablissements.reduce((s, e) => s + e.stats.nb_portables_renouveler, 0);
|
||||
const totalFixes = etablissements.reduce((s, e) => s + e.stats.nb_fixes_total, 0);
|
||||
const totalPortables = etablissements.reduce((s, e) => s + e.stats.nb_portables_total, 0);
|
||||
const budgetGlobal = etablissements.reduce((s, e) => s + e.stats.budget_total, 0);
|
||||
|
||||
return {
|
||||
meta: {
|
||||
annee: input.annee,
|
||||
dateImport: meta.dateImport,
|
||||
nb_etablissements: etablissements.length,
|
||||
total_fixes: totalFixes,
|
||||
total_portables: totalPortables,
|
||||
total_fixes_renouveler: totalFixesR,
|
||||
total_portables_renouveler: totalPortablesR,
|
||||
budget_global: budgetGlobal,
|
||||
seuil_fixes_ans: input.seuilFixesAns,
|
||||
seuil_portables_ans: input.seuilPortablesAns,
|
||||
cout_fixe: input.coutFixe,
|
||||
cout_portable: input.coutPortable,
|
||||
},
|
||||
etablissements,
|
||||
};
|
||||
}),
|
||||
|
||||
listMeta: protectedProcedure
|
||||
.query(async () => {
|
||||
const rows = await db.listInventaireMeta();
|
||||
return rows;
|
||||
}),
|
||||
|
||||
import: writeProcedure
|
||||
.input(z.object({ annee: z.number(), filename: z.string(), postes: z.array(z.object({ etablissementCode: z.string(), libelle: z.string().optional().nullable(), typePoste: z.enum(["fixe", "portable"]), dateRef: z.string().optional().nullable(), ageAns: z.string().optional().nullable(), modele: z.string().optional().nullable(), fabricant: z.string().optional().nullable() })) }))
|
||||
.mutation(async ({ input }) => {
|
||||
|
||||
Reference in New Issue
Block a user