import { boolean, decimal, int, mysqlEnum, mysqlTable, text, timestamp, uniqueIndex, 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(), }, (t) => ({ /** Empêche une initialisation répétée de créer le même poste pour une année. */ uniqAnneeLibelle: uniqueIndex("opex_postes_annee_libelle_idx").on(t.annee, t.libelle), })); 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(), }, (t) => ({ /** Une seule surcharge manuelle par année, établissement et poste. */ uniqAnneeEtabPoste: uniqueIndex("opex_montants_annee_etab_poste_idx").on( t.annee, t.etablissementCode, t.libellePoste, ), })); 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(), }, (t) => ({ /** La sauvegarde CAPEX reste idempotente pour chaque clé métier. */ uniqAnneeEtabCle: uniqueIndex("capex_lignes_annee_etab_cle_idx").on( t.annee, t.etablissementCode, t.cle, ), })); export type CapexLigne = typeof capexLignes.$inferSelect; export type InsertCapexLigne = typeof capexLignes.$inferInsert; // ─────────────────────────────────────────────────────────────────────────────── // OPEX — Bases de répartition par établissement et par année // (charges de classe 6 utilisées pour le prorata) // ─────────────────────────────────────────────────────────────────────────────── export const opexBasesRepartition = mysqlTable("opex_bases_repartition", { id: int("id").autoincrement().primaryKey(), annee: int("annee").notNull(), etablissementCode: varchar("etablissementCode", { length: 50 }).notNull(), etablissementNom: varchar("etablissementNom", { length: 255 }), /** Charges classe 6 — base de répartition standard (tous établissements) */ baseRepartition: decimal("baseRepartition", { precision: 20, scale: 6 }).notNull(), /** Charges classe 6 — base de répartition pôle HEP uniquement (0 pour les étblissements hors HEP) */ baseRepartitionHep: decimal("baseRepartitionHep", { precision: 20, scale: 6 }).default("0"), /** Si true, tous les montants OPEX de cet établissement sont saisis manuellement (pas de calcul prorata) */ modeManuel: boolean("modeManuel").default(false).notNull(), createdAt: timestamp("createdAt").defaultNow().notNull(), }, (t) => ({ uniqAnneeEtab: uniqueIndex("opex_bases_repartition_annee_etab_idx").on(t.annee, t.etablissementCode), })); export type OpexBaseRepartition = typeof opexBasesRepartition.$inferSelect; export type InsertOpexBaseRepartition = typeof opexBasesRepartition.$inferInsert; // ───────────────────────────────────────────────────────────────────────────── // MASSE SALARIALE SANTINOVA — Données sensibles, accès administrateur uniquement // ───────────────────────────────────────────────────────────────────────────── export const masseSalarialeSalaries = mysqlTable("masse_salariale_salaries", { id: int("id").autoincrement().primaryKey(), /** Identifiant paie stable, distinct des données personnelles superflues. */ matricule: varchar("matricule", { length: 32 }).notNull().unique(), nom: varchar("nom", { length: 150 }).notNull(), prenom: varchar("prenom", { length: 150 }).notNull(), poste: varchar("poste", { length: 255 }).notNull(), /** Date civile ISO (YYYY-MM-DD), sans fuseau horaire. */ dateEmbauche: varchar("dateEmbauche", { length: 10 }).notNull(), actif: boolean("actif").default(true).notNull(), createdAt: timestamp("createdAt").defaultNow().notNull(), updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(), }); export type MasseSalarialeSalarie = typeof masseSalarialeSalaries.$inferSelect; export type InsertMasseSalarialeSalarie = typeof masseSalarialeSalaries.$inferInsert; /** * Une ligne par salarié et exercice. Tous les montants sont en centimes et les * taux en points de base (ex. 42,75 % = 4 275) pour éviter les erreurs d'arrondi. */ export const masseSalarialeRemunerations = mysqlTable("masse_salariale_remunerations", { id: int("id").autoincrement().primaryKey(), salarieId: int("salarieId").notNull(), annee: int("annee").notNull(), salaireAnnuelBrutHorsPrimesCents: int("salaireAnnuelBrutHorsPrimesCents").notNull(), salaireAnnuelBrutAvecPrimesCents: int("salaireAnnuelBrutAvecPrimesCents").notNull(), salaireMensuelBrutHorsPrimesCents: int("salaireMensuelBrutHorsPrimesCents").notNull(), tauxChargeBps: int("tauxChargeBps").notNull(), /** Mois de paie ayant servi de référence, ex. 2025-12 ou 2026-08. */ periodeReference: varchar("periodeReference", { length: 7 }), /** `annuel` pour un exercice clos ; `cumul_provisoire` pour une année en cours. */ statut: varchar("statut", { length: 32 }).notNull().default("annuel"), source: varchar("source", { length: 64 }).notNull().default("bulletin"), createdAt: timestamp("createdAt").defaultNow().notNull(), updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(), }, (t) => ({ /** Rend les imports idempotents pour un salarié et un exercice. */ uniqSalarieAnnee: uniqueIndex("masse_salariale_salarie_annee_idx").on(t.salarieId, t.annee), })); export type MasseSalarialeRemuneration = typeof masseSalarialeRemunerations.$inferSelect; export type InsertMasseSalarialeRemuneration = typeof masseSalarialeRemunerations.$inferInsert; /** Évolution salariale manuelle, stockée même en l'absence de bulletin pour l'année concernée. */ export const masseSalarialeEvolutions = mysqlTable("masse_salariale_evolutions", { id: int("id").autoincrement().primaryKey(), salarieId: int("salarieId").notNull(), annee: int("annee").notNull(), /** Pourcentage exprimé en points de base, ex. 1,50 % = 150. */ evolutionSalarialeBps: int("evolutionSalarialeBps").notNull(), createdBy: int("createdBy").notNull(), createdAt: timestamp("createdAt").defaultNow().notNull(), updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(), }, (t) => ({ uniqSalarieAnnee: uniqueIndex("masse_salariale_evolution_salarie_annee_idx").on(t.salarieId, t.annee), })); export type MasseSalarialeEvolution = typeof masseSalarialeEvolutions.$inferSelect; export type InsertMasseSalarialeEvolution = typeof masseSalarialeEvolutions.$inferInsert;