Some checks failed
Validation applicative / TypeScript, tests et build (push) Failing after 1m54s
329 lines
19 KiB
TypeScript
329 lines
19 KiB
TypeScript
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;
|
||
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
// SALAIRES MENSUELS — Liasses PDF chiffrées et données extraites
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
||
/**
|
||
* Une liasse représente un dépôt mensuel. Le fichier est chiffré avant envoi
|
||
* vers le stockage persistant ; la base ne conserve ni octet de PDF ni clé.
|
||
*/
|
||
export const salairesLiasses = mysqlTable("salaires_liasses", {
|
||
id: int("id").autoincrement().primaryKey(),
|
||
annee: int("annee").notNull(),
|
||
mois: int("mois").notNull(),
|
||
nomFichier: varchar("nomFichier", { length: 255 }).notNull(),
|
||
stockageKey: varchar("stockageKey", { length: 512 }).notNull().unique(),
|
||
empreinteSha256: varchar("empreinteSha256", { length: 64 }).notNull().unique(),
|
||
tailleOctets: int("tailleOctets").notNull(),
|
||
/** Paramètres AES-256-GCM nécessaires au déchiffrement côté serveur. */
|
||
ivBase64: varchar("ivBase64", { length: 32 }).notNull(),
|
||
authTagBase64: varchar("authTagBase64", { length: 32 }).notNull(),
|
||
versionCle: int("versionCle").notNull().default(1),
|
||
/** `ready`, `partiel` ou `a_controler` : une liasse est conservée même si l'extraction est incomplète. */
|
||
statutExtraction: varchar("statutExtraction", { length: 32 }).notNull().default("ready"),
|
||
erreurExtraction: text("erreurExtraction"),
|
||
importePar: int("importePar").notNull(),
|
||
importeLe: timestamp("importeLe").defaultNow().notNull(),
|
||
}, (t) => ({
|
||
/** Une liasse unique par mois : toutes les fiches mensuelles restent ainsi cohérentes. */
|
||
uniqPeriode: uniqueIndex("salaires_liasses_periode_idx").on(t.annee, t.mois),
|
||
/** Empêche de traiter deux fois le même fichier sous une période distincte. */
|
||
uniqPeriodeEmpreinte: uniqueIndex("salaires_liasses_periode_empreinte_idx").on(t.annee, t.mois, t.empreinteSha256),
|
||
}));
|
||
|
||
export type SalaireLiasse = typeof salairesLiasses.$inferSelect;
|
||
export type InsertSalaireLiasse = typeof salairesLiasses.$inferInsert;
|
||
|
||
/**
|
||
* Données utiles par bulletin. `explicationEcart` conserve uniquement les
|
||
* rubriques de rémunération nécessaires à expliquer une variation mensuelle.
|
||
*/
|
||
export const salairesBulletins = mysqlTable("salaires_bulletins", {
|
||
id: int("id").autoincrement().primaryKey(),
|
||
liasseId: int("liasseId").notNull(),
|
||
matricule: varchar("matricule", { length: 32 }).notNull(),
|
||
nom: varchar("nom", { length: 150 }).notNull(),
|
||
prenom: varchar("prenom", { length: 150 }).notNull(),
|
||
poste: varchar("poste", { length: 255 }).notNull(),
|
||
brutMensuelCents: int("brutMensuelCents").notNull(),
|
||
brutAvecPrimesCents: int("brutAvecPrimesCents").notNull(),
|
||
primeAstreinteCents: int("primeAstreinteCents").notNull().default(0),
|
||
explicationEcart: text("explicationEcart"),
|
||
numeroPage: int("numeroPage"),
|
||
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
||
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
||
}, (t) => ({
|
||
/** Un seul bulletin indexé par matricule dans une liasse donnée. */
|
||
uniqLiasseMatricule: uniqueIndex("salaires_bulletins_liasse_matricule_idx").on(t.liasseId, t.matricule),
|
||
}));
|
||
|
||
export type SalaireBulletin = typeof salairesBulletins.$inferSelect;
|
||
export type InsertSalaireBulletin = typeof salairesBulletins.$inferInsert;
|