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:
111
server/db.ts
111
server/db.ts
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user