mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-11 17:26:32 +02:00
La colonne « Tendance » de la Grille était la seule information visible que
l'API ne fournissait pas : elle est calculée dans le navigateur par
computeNetworkTrend(), qui vivait dans un composant "use client" donc
inaccessible côté serveur. L'API livrait la série mensuelle brute et laissait
chaque appelant refaire la régression — au risque de diverger de l'affichage.
- Le calcul part dans src/lib/network-trend.ts (module pur, sans React) ;
le composant le réexporte, aucun import existant ne change.
- enrichRows ajoute `trend` : { direction, pct, label, monthsUsed }, calculé
sur la même série que la colonne de l'application.
- openapi.json : schéma Trend documenté, et il est dit explicitement que la
liste des propriétés de Product n'est pas exhaustive — la réponse porte la
ligne de grille entière.
Vérifié au passage que rien n'est perdu en chemin : upsertGridRows stocke le
ProductRow complet dans la colonne jsonb `payload` (les colonnes scalaires ne
servent qu'aux index), ne filtre que les lignes sans codein, et pickFields
renvoie tout tant que `fields` n'est pas précisé.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y26nRZxTR57K7h8yqsF675
68 lines
2.9 KiB
TypeScript
68 lines
2.9 KiB
TypeScript
/**
|
||
* CollectFlow — Tendance réseau (calcul pur, sans React).
|
||
*
|
||
* Isolé du composant d'affichage (`features/network/components/network-trend.tsx`)
|
||
* pour être utilisable **côté serveur** : l'API `/api/v1` expose ainsi la même
|
||
* tendance que celle affichée dans la Grille, au lieu de laisser chaque appelant
|
||
* la recalculer — et diverger.
|
||
*
|
||
* Source : `qteByMonth` ({ "YYYY-MM": quantité }) produit par la synchronisation Qlik.
|
||
*/
|
||
|
||
export type TrendDirection = "up" | "down" | "flat";
|
||
|
||
export type NetworkTrend = {
|
||
values: number[]; // quantités mensuelles, ordre chronologique
|
||
labels: string[]; // "YYYY-MM"
|
||
direction: TrendDirection;
|
||
pct: number | null; // variation modélisée (régression linéaire) sur 12 mois
|
||
hasData: boolean;
|
||
};
|
||
|
||
/** Seuil de "forte" variation (±25%) pour distinguer hausse/forte hausse. */
|
||
export const TREND_STRONG = 0.25;
|
||
|
||
/**
|
||
* Tendance réseau par RÉGRESSION LINÉAIRE (moindres carrés) sur les 12 derniers mois.
|
||
* Utilise tous les points (robuste au bruit d'un mois isolé). L'indicateur `pct` est la
|
||
* variation modélisée sur la période (pente × durée) rapportée à la moyenne.
|
||
* Direction : hausse >+8%, stable, baisse <−8%.
|
||
*/
|
||
export function computeNetworkTrend(qteByMonth?: Record<string, number> | null): NetworkTrend {
|
||
const empty: NetworkTrend = { values: [], labels: [], direction: "flat", pct: null, hasData: false };
|
||
if (!qteByMonth) return empty;
|
||
let labels = Object.keys(qteByMonth).sort(); // "YYYY-MM" trie chronologiquement
|
||
if (labels.length === 0) return empty;
|
||
labels = labels.slice(-12); // 12 derniers mois
|
||
const values = labels.map((l) => Number(qteByMonth[l]) || 0);
|
||
const n = values.length;
|
||
let pct: number | null = null;
|
||
let direction: TrendDirection = "flat";
|
||
if (n >= 3) {
|
||
const mx = (n - 1) / 2;
|
||
const my = values.reduce((s, v) => s + v, 0) / n;
|
||
let num = 0, den = 0;
|
||
for (let i = 0; i < n; i++) { num += (i - mx) * (values[i] - my); den += (i - mx) * (i - mx); }
|
||
const slope = den ? num / den : 0;
|
||
if (my > 0) {
|
||
pct = (slope * (n - 1)) / my; // variation modélisée sur toute la période / moyenne
|
||
direction = pct > 0.08 ? "up" : pct < -0.08 ? "down" : "flat";
|
||
} else if (slope > 0) {
|
||
direction = "up";
|
||
}
|
||
} else if (n === 2 && values[0] > 0) {
|
||
pct = (values[1] - values[0]) / values[0];
|
||
direction = pct > 0.08 ? "up" : pct < -0.08 ? "down" : "flat";
|
||
}
|
||
return { values, labels, direction, pct, hasData: true };
|
||
}
|
||
|
||
/** Libellé français de la tendance (« Forte hausse », « Stable »…). */
|
||
export function trendLabel(pct: number): string {
|
||
if (pct > TREND_STRONG) return "Forte hausse";
|
||
if (pct > 0.08) return "Hausse";
|
||
if (pct < -TREND_STRONG) return "Forte baisse";
|
||
if (pct < -0.08) return "Baisse";
|
||
return "Stable";
|
||
}
|