Checkpoint: Ajout du module Masse salariale SANTINOVA : tables de salariés, rémunérations et évolutions manuelles sécurisées par rôle administrateur ; import contrôlé des bulletins 2025 et janvier–août 2026 ; écran tableau 2025–2027 ; tests d'autorisations, d'agrégats et des quatre états UI ; documentation des sources et de la politique d'import.

This commit is contained in:
Manus
2026-08-27 13:10:47 +00:00
parent 2f4b1f2e8e
commit da407db4de
21 changed files with 4390 additions and 4 deletions

View File

@@ -5,11 +5,17 @@ import {
etablissements,
InsertCapexLigne,
InsertEtablissement,
InsertMasseSalarialeEvolution,
InsertMasseSalarialeRemuneration,
InsertMasseSalarialeSalarie,
InsertOpexMontantEtab,
InsertOpexPoste,
InsertUser,
inventaireMeta,
inventairePostes,
masseSalarialeEvolutions,
masseSalarialeRemunerations,
masseSalarialeSalaries,
opexBasesRepartition,
opexMontantsEtab,
opexPostes,
@@ -448,3 +454,108 @@ export async function importOpexBasesRepartition(
);
});
}
// ─────────────────────────────────────────────────────────────────────────────
// MASSE SALARIALE — accès exclusivement appelés par des procédures admin
// ─────────────────────────────────────────────────────────────────────────────
export type MasseSalarialeImportSalarie = Pick<
InsertMasseSalarialeSalarie,
"matricule" | "nom" | "prenom" | "poste" | "dateEmbauche"
>;
export type MasseSalarialeImportRemuneration = Omit<
InsertMasseSalarialeRemuneration,
"id" | "salarieId" | "createdAt" | "updatedAt"
> & { matricule: string };
/**
* Retourne les trois sources normalisées séparément. Le routeur réalise la
* jointure logique afin d'éviter une matrice SQL qui dupliquerait les années.
*/
export async function getMasseSalariale() {
const db = await getDb();
if (!db) throw new Error("Database not available");
const [salaries, remunerations, evolutions] = await Promise.all([
db.select().from(masseSalarialeSalaries).orderBy(asc(masseSalarialeSalaries.nom), asc(masseSalarialeSalaries.prenom)),
db.select().from(masseSalarialeRemunerations),
db.select().from(masseSalarialeEvolutions),
]);
return { salaries, remunerations, evolutions };
}
/**
* Import idempotent : les salaires et leurs rémunérations annuelles sont
* remplacés dans une transaction, sans modifier les évolutions saisies à la
* main qui résident dans une table distincte.
*/
export async function importMasseSalariale(
salaries: MasseSalarialeImportSalarie[],
remunerations: MasseSalarialeImportRemuneration[],
) {
const db = await getDb();
if (!db) throw new Error("Database not available");
if (salaries.length === 0) return { salaries: 0, remunerations: 0 };
return db.transaction(async (tx) => {
for (const salarie of salaries) {
await tx
.insert(masseSalarialeSalaries)
.values({ ...salarie, actif: true })
.onDuplicateKeyUpdate({
set: {
nom: salarie.nom,
prenom: salarie.prenom,
poste: salarie.poste,
dateEmbauche: salarie.dateEmbauche,
actif: true,
},
});
}
const persistedSalaries = await tx
.select({ id: masseSalarialeSalaries.id, matricule: masseSalarialeSalaries.matricule })
.from(masseSalarialeSalaries);
const idsByMatricule = new Map(persistedSalaries.map((row) => [row.matricule, row.id]));
for (const remuneration of remunerations) {
const salarieId = idsByMatricule.get(remuneration.matricule);
if (!salarieId) {
throw new Error(`Salarié introuvable lors de l'import : ${remuneration.matricule}`);
}
const { matricule: _matricule, ...values } = remuneration;
await tx
.insert(masseSalarialeRemunerations)
.values({ ...values, salarieId })
.onDuplicateKeyUpdate({
set: {
salaireAnnuelBrutHorsPrimesCents: values.salaireAnnuelBrutHorsPrimesCents,
salaireAnnuelBrutAvecPrimesCents: values.salaireAnnuelBrutAvecPrimesCents,
salaireMensuelBrutHorsPrimesCents: values.salaireMensuelBrutHorsPrimesCents,
tauxChargeBps: values.tauxChargeBps,
periodeReference: values.periodeReference,
statut: values.statut,
source: values.source,
},
});
}
return { salaries: salaries.length, remunerations: remunerations.length };
});
}
/** Sauvegarde l'évolution manuelle sans dépendre de l'existence d'un bulletin. */
export async function setMasseSalarialeEvolution(
input: Pick<InsertMasseSalarialeEvolution, "salarieId" | "annee" | "evolutionSalarialeBps" | "createdBy">,
) {
const db = await getDb();
if (!db) throw new Error("Database not available");
await db
.insert(masseSalarialeEvolutions)
.values(input)
.onDuplicateKeyUpdate({
set: {
evolutionSalarialeBps: input.evolutionSalarialeBps,
createdBy: input.createdBy,
},
});
}