/** * Calculs purs des tendances OPEX utilisées par les vignettes de catégories. * * Cette fonction ne dépend volontairement ni de React, ni des fichiers JSON * historiques : la comparaison est toujours faite avec les montants réellement * chargés pour l'exercice précédent. */ export type OpexCategoryLine = { categorie: string | null | undefined; montant: number; }; export type OpexCategoryTrend = { montantN1: number | null; variationPct: number | null; tendance: "hausse" | "baisse" | "stable" | "new"; }; /** Toute variation strictement inférieure à ce seuil est considérée stable. */ export const STABLE_VARIATION_THRESHOLD_PERCENT = 2; function toCategoryTotals(lines: OpexCategoryLine[]): Record { return lines.reduce>((totals, line) => { const category = line.categorie?.trim() || "Autre"; totals[category] = (totals[category] ?? 0) + line.montant; return totals; }, {}); } /** * Retourne les tendances par catégorie pour l'exercice courant. * * Les pourcentages sont arrondis à une décimale pour correspondre à l'affichage * et éviter des différences entre le badge et les données fournies au composant. */ export function calculateCategoryTrends( currentLines: OpexCategoryLine[], previousLines: OpexCategoryLine[], ): Record { const currentTotals = toCategoryTotals(currentLines); const previousTotals = toCategoryTotals(previousLines); return Object.fromEntries( Object.entries(currentTotals).map(([category, currentAmount]) => { const previousAmount = previousTotals[category] ?? 0; if (previousAmount === 0) { return [ category, { montantN1: null, variationPct: null, tendance: "new" as const, }, ]; } const rawVariation = ((currentAmount - previousAmount) / previousAmount) * 100; const variationPct = Math.round(rawVariation * 10) / 10; const tendance = Math.abs(variationPct) < STABLE_VARIATION_THRESHOLD_PERCENT ? "stable" : variationPct > 0 ? "hausse" : "baisse"; return [ category, { montantN1: previousAmount, variationPct, tendance }, ]; }), ); }