Checkpoint: Migration complète vers authentification locale (JWT + bcrypt) : schéma DB (10 tables), routes tRPC (auth, users, etablissements, inventaire, capex, opex, parametres), page de login avec branding Itinova/Santinova, protection des routes, onglets Établissements et Utilisateurs branchés sur tRPC, import inventaire/établissements/utilisateurs via tRPC, 14 tests Vitest passants.

This commit is contained in:
Manus
2026-06-10 12:17:07 +00:00
parent fe14ad99e1
commit caf95da1c1
59 changed files with 9739 additions and 399 deletions

162
drizzle/schema.ts Normal file
View File

@@ -0,0 +1,162 @@
import {
boolean,
decimal,
int,
mysqlEnum,
mysqlTable,
text,
timestamp,
varchar,
} from "drizzle-orm/mysql-core";
// ─────────────────────────────────────────────────────────────────────────────
// USERS — Auth locale (email/password), 3 profils : admin | standard | readonly
// ─────────────────────────────────────────────────────────────────────────────
export const users = mysqlTable("users", {
id: int("id").autoincrement().primaryKey(),
/** Identifiant de connexion (email ou login court comme adminItinova) */
login: varchar("login", { length: 255 }).notNull().unique(),
email: varchar("email", { length: 320 }),
passwordHash: varchar("passwordHash", { length: 255 }).notNull(),
firstName: varchar("firstName", { length: 100 }),
lastName: varchar("lastName", { length: 100 }),
role: mysqlEnum("role", ["admin", "standard", "readonly"]).default("standard").notNull(),
isActive: boolean("isActive").default(true).notNull(),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
lastSignedIn: timestamp("lastSignedIn"),
});
export type User = typeof users.$inferSelect;
export type InsertUser = typeof users.$inferInsert;
// ─────────────────────────────────────────────────────────────────────────────
// ÉTABLISSEMENTS
// ─────────────────────────────────────────────────────────────────────────────
export const etablissements = mysqlTable("etablissements", {
id: int("id").autoincrement().primaryKey(),
code: varchar("code", { length: 50 }).notNull().unique(),
nom: varchar("nom", { length: 255 }).notNull(),
groupe: varchar("groupe", { length: 100 }),
ville: varchar("ville", { length: 100 }),
actif: boolean("actif").default(true).notNull(),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type Etablissement = typeof etablissements.$inferSelect;
export type InsertEtablissement = typeof etablissements.$inferInsert;
// Rattachement utilisateur ↔ établissements (pour profil standard/readonly)
export const userEtablissements = mysqlTable("user_etablissements", {
id: int("id").autoincrement().primaryKey(),
userId: int("userId").notNull(),
etablissementCode: varchar("etablissementCode", { length: 50 }).notNull(),
});
// ─────────────────────────────────────────────────────────────────────────────
// PARAMÈTRES APPLICATION
// ─────────────────────────────────────────────────────────────────────────────
export const parametresApp = mysqlTable("parametres_app", {
id: int("id").autoincrement().primaryKey(),
cle: varchar("cle", { length: 100 }).notNull().unique(),
valeur: text("valeur"),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
// ─────────────────────────────────────────────────────────────────────────────
// OPEX — Postes budgétaires
// ─────────────────────────────────────────────────────────────────────────────
export const opexPostes = mysqlTable("opex_postes", {
id: int("id").autoincrement().primaryKey(),
annee: int("annee").notNull(),
colIdx: int("colIdx").notNull(),
libelle: varchar("libelle", { length: 255 }).notNull(),
libelleCourt: varchar("libelleCourt", { length: 100 }),
libelleDetail: varchar("libelleDetail", { length: 255 }),
fournisseur: varchar("fournisseur", { length: 255 }),
categorie: varchar("categorie", { length: 100 }),
type: varchar("type", { length: 100 }),
facturation: varchar("facturation", { length: 100 }),
modeVentilation: varchar("modeVentilation", { length: 50 }).default("Prorata C 6"),
compte: varchar("compte", { length: 50 }),
detail: text("detail"),
budgetN1: decimal("budgetN1", { precision: 12, scale: 2 }),
montant: decimal("montant", { precision: 12, scale: 2 }),
isCustom: boolean("isCustom").default(false),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type OpexPoste = typeof opexPostes.$inferSelect;
export type InsertOpexPoste = typeof opexPostes.$inferInsert;
// Overrides manuels par établissement × poste × année
export const opexMontantsEtab = mysqlTable("opex_montants_etab", {
id: int("id").autoincrement().primaryKey(),
annee: int("annee").notNull(),
etablissementCode: varchar("etablissementCode", { length: 50 }).notNull(),
libellePoste: varchar("libellePoste", { length: 255 }).notNull(),
montant: decimal("montant", { precision: 12, scale: 2 }),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type OpexMontantEtab = typeof opexMontantsEtab.$inferSelect;
export type InsertOpexMontantEtab = typeof opexMontantsEtab.$inferInsert;
// Validation définitive par année
export const opexValidated = mysqlTable("opex_validated", {
id: int("id").autoincrement().primaryKey(),
annee: int("annee").notNull().unique(),
validatedAt: timestamp("validatedAt").defaultNow().notNull(),
validatedBy: int("validatedBy"),
});
// ─────────────────────────────────────────────────────────────────────────────
// INVENTAIRE PC
// ─────────────────────────────────────────────────────────────────────────────
export const inventairePostes = mysqlTable("inventaire_postes", {
id: int("id").autoincrement().primaryKey(),
annee: int("annee").notNull(),
etablissementCode: varchar("etablissementCode", { length: 50 }).notNull(),
libelle: varchar("libelle", { length: 255 }),
typePoste: mysqlEnum("typePoste", ["fixe", "portable"]).notNull(),
dateRef: varchar("dateRef", { length: 20 }),
ageAns: decimal("ageAns", { precision: 5, scale: 2 }),
modele: varchar("modele", { length: 255 }),
fabricant: varchar("fabricant", { length: 100 }),
createdAt: timestamp("createdAt").defaultNow().notNull(),
});
export type InventairePoste = typeof inventairePostes.$inferSelect;
export type InsertInventairePoste = typeof inventairePostes.$inferInsert;
// Métadonnées d'import inventaire
export const inventaireMeta = mysqlTable("inventaire_meta", {
id: int("id").autoincrement().primaryKey(),
annee: int("annee").notNull().unique(),
filename: varchar("filename", { length: 255 }),
dateImport: timestamp("dateImport").defaultNow().notNull(),
nbEtablissements: int("nbEtablissements").default(0),
nbFixes: int("nbFixes").default(0),
nbPortables: int("nbPortables").default(0),
});
// ─────────────────────────────────────────────────────────────────────────────
// CAPEX — Lignes budgétaires par établissement
// ─────────────────────────────────────────────────────────────────────────────
export const capexLignes = mysqlTable("capex_lignes", {
id: int("id").autoincrement().primaryKey(),
annee: int("annee").notNull(),
etablissementCode: varchar("etablissementCode", { length: 50 }).notNull(),
cle: varchar("cle", { length: 100 }).notNull(),
// cles possibles: renouvellement_2027 | machines_supplementaires | appel_malade
// | telephonie | wifi | video_surveillance | copieurs | visio | autres | commentaires
montant: decimal("montant", { precision: 12, scale: 2 }),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type CapexLigne = typeof capexLignes.$inferSelect;
export type InsertCapexLigne = typeof capexLignes.$inferInsert;