/** * Calculs purs de la fenêtre Masse salariale. Les montants sont conservés en * centimes afin que les projections et cumuls ne subissent aucun arrondi * intermédiaire. Les réels ne sont jamais extrapolés : ils cumulent seulement * les liasses mensuelles effectivement archivées. */ export type AnnualForecastInput = { annee: number; brutHorsAstreinteReferenceCents: number; brutAvecPrimesReferenceCents: number; primeAstreinteCents: number; primeCents: number; primeReferenceCents: number; evolutionCents: number; evolutionReferenceCents: number; tauxChargeBps: number | null; }; export type AnnualProjection = { brutHorsAstreinteCents: number; brutAvecPrimesCents: number; brutMensuelHorsAstreinteCents: number; primeAstreinteCents: number; primeComplementaireCents: number; evolutionCents: number; tauxChargeBps: number | null; }; export type PayrollMonthlyInput = { annee: number; mois: number; matricule: string; brutMensuelCents: number; brutAvecPrimesCents: number; primeAstreinteCents: number; }; export type PayrollAnnualActual = Omit & { moisDisponibles: number[]; }; /** * Restitue exactement la référence importée, puis applique seulement les * écarts manuels des primes et évolutions. Ainsi, un import Excel conserve ses * calculs initiaux tandis que toute correction autorisée est automatiquement * répercutée sur le brut prévisionnel dérivé. */ export function buildAnnualForecasts(inputs: AnnualForecastInput[]): Map { const forecasts = new Map(); for (const current of [...inputs].sort((a, b) => a.annee - b.annee)) { const adjustmentDelta = (current.primeCents - current.primeReferenceCents) + (current.evolutionCents - current.evolutionReferenceCents); const brutHorsAstreinteCents = current.brutHorsAstreinteReferenceCents + adjustmentDelta; const brutAvecPrimesCents = current.brutAvecPrimesReferenceCents + adjustmentDelta; forecasts.set(current.annee, { brutHorsAstreinteCents, brutAvecPrimesCents, brutMensuelHorsAstreinteCents: Math.round(brutHorsAstreinteCents / 12), primeAstreinteCents: current.primeAstreinteCents, primeComplementaireCents: current.primeCents, evolutionCents: current.evolutionCents, tauxChargeBps: current.tauxChargeBps, }); } return forecasts; } /** * Agrège les bulletins archivés par salarié et année. Le brut hors astreinte * ne retire que les primes d'astreinte : les autres éléments compris dans le * brut (activité, responsabilité, vacances…) sont donc préservés. */ export function buildAnnualPayrollActuals(inputs: PayrollMonthlyInput[]): Map> { const grouped = new Map>(); for (const input of inputs) { const byYear = grouped.get(input.matricule) ?? new Map(); byYear.set(input.annee, [...(byYear.get(input.annee) ?? []), input]); grouped.set(input.matricule, byYear); } const actuals = new Map>(); for (const [matricule, byYear] of Array.from(grouped.entries())) { const projections = new Map(); for (const [annee, monthlyRows] of Array.from(byYear.entries())) { const brutAvecPrimesCents = monthlyRows.reduce((sum, row) => sum + row.brutAvecPrimesCents, 0); const primeAstreinteCents = monthlyRows.reduce((sum, row) => sum + row.primeAstreinteCents, 0); const brutHorsAstreinteCents = brutAvecPrimesCents - primeAstreinteCents; const primeComplementaireCents = monthlyRows.reduce( (sum, row) => sum + (row.brutAvecPrimesCents - row.brutMensuelCents - row.primeAstreinteCents), 0, ); const moisDisponibles = Array.from(new Set(monthlyRows.map((row) => row.mois))).sort((a, b) => a - b); projections.set(annee, { brutHorsAstreinteCents, brutAvecPrimesCents, brutMensuelHorsAstreinteCents: Math.round(brutHorsAstreinteCents / moisDisponibles.length), primeAstreinteCents, primeComplementaireCents, moisDisponibles, }); } actuals.set(matricule, projections); } return actuals; } /** Les ajustements de prévision sont bloqués sur les exercices clôturés. */ export function isForecastAdjustmentEditable(annee: number, currentYear: number): boolean { return annee >= currentYear; }